feat(theme): introduce dms classic interface style

- Expressive remains in place where Classic drops pills, shape morph, and setting sidebar color badges
- Classic defaults corner style to Fixed and stacks settings groups into continuous cards
- section dividers setting provides a slider to enhance the lines or remove them entirely for a clean look
Port 2.0
This commit is contained in:
purian23 committed 2026-10-28 06:38:43 -04:00
1 parent 59ede533d8
commit 7479d4bb79
40 files changed
+532 -181

No files matched your search

Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1793150560,
"narHash": "sha256-Vx8riKxgP2X/KZ/nzDn3WxC/wrYqAwa4yh9J6FurUEw=",
"lastModified": 1793183831,
"narHash": "sha256-23DNWShmRTKreSalQQZ7TE3fL3u+q7DprECU5EVEC5k=",
"owner": "duxlabs-dev",
"repo": "dux-qml-common",
"rev": "3d56a375f29b691074cb61411356966da8ad060e",
"rev": "b602c9e1b7521bab36e3b5fb0f9485dd5ef6f531",
"type": "github"
},
"original": {
+23 -3
View File
@@ -308,6 +308,7 @@ Singleton {
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property string density: Spec.SPEC.density.def
property string interfaceStyle: Spec.SPEC.interfaceStyle.def
property bool m3ElevationEnabled: Spec.SPEC.m3ElevationEnabled.def
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: Spec.SPEC.m3ElevationIntensity.def
@@ -337,6 +338,8 @@ Singleton {
property real foregroundLayerTransparency: Spec.SPEC.foregroundLayerTransparency.def
property real blurLayerOutlineOpacity: Spec.SPEC.blurLayerOutlineOpacity.def
onBlurLayerOutlineOpacityChanged: saveSettings()
property real sectionDividerOpacity: Spec.SPEC.sectionDividerOpacity.def
onSectionDividerOpacityChanged: saveSettings()
property string tooltipSurfaceColor: Spec.SPEC.tooltipSurfaceColor.def
onTooltipSurfaceColorChanged: saveSettings()
property string tooltipSurfaceCustomColor: Spec.SPEC.tooltipSurfaceCustomColor.def
@@ -1747,8 +1750,17 @@ Singleton {
}
}
function applyInterfaceStyle(who, key, previous) {
const from = Store.styleDefault("radiusMode", previous) ?? Spec.SPEC.radiusMode.def;
if (radiusMode !== from || isManaged("radiusMode"))
return;
radiusMode = presetDefault("radiusMode") ?? Spec.SPEC.radiusMode.def;
updateCompositorLayout();
}
readonly property var _hooks: ({
"applyDensityPresets": applyDensityPresets,
"applyInterfaceStyle": applyInterfaceStyle,
"applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes,
"updateCompositorLayout": updateCompositorLayout,
@@ -1772,6 +1784,10 @@ Singleton {
log.warn("Refusing an unknown density:", value);
return false;
}
if (key === "interfaceStyle" && !["expressive", "classic"].includes(value)) {
log.warn("Refusing an unknown interface style:", value);
return false;
}
if (key === "cornerRadius")
return setCornerRadius(value);
if (key === "radiusStrength")
@@ -1790,18 +1806,22 @@ Singleton {
return key in AdminPolicy.lockSettings;
}
function presetDefault(key) {
return DensityPresets.value("frame", key, density) ?? Store.styleDefault(key, interfaceStyle);
}
function specDefault(key) {
return DensityPresets.value("frame", key, density) ?? SpecUtil.cloneDef(Spec.SPEC[key]?.def);
return presetDefault(key) ?? SpecUtil.cloneDef(Spec.SPEC[key]?.def);
}
function isDefault(keys) {
return keys.every(key => !(key in Spec.SPEC) || SpecUtil.isDefault(root[key], DensityPresets.value("frame", key, density) ?? Spec.SPEC[key].def));
return keys.every(key => !(key in Spec.SPEC) || SpecUtil.isDefault(root[key], presetDefault(key) ?? Spec.SPEC[key].def));
}
function resetToDefault(keys) {
for (const key of keys) {
if (key in Spec.SPEC && !isManaged(key))
Spec.set(root, key, DensityPresets.value("frame", key, density) ?? null, saveSettings, _hooks);
Spec.set(root, key, presetDefault(key) ?? null, saveSettings, _hooks);
}
}
+14 -6
View File
@@ -1127,7 +1127,7 @@ Singleton {
readonly property real notificationExpandedIconSizeNormal: avatarSize
readonly property real notificationExpandedIconSizeCompact: avatarSize
readonly property real notificationButtonCornerRadius: cornerRadiusFull
readonly property real notificationButtonCornerRadius: pillControls ? cornerRadiusFull : cornerRadiusL
readonly property real notificationHoverRevealMargin: spacingXL
readonly property real notificationContentSpacing: spacingXS
readonly property real notificationCardPadding: spacingM
@@ -1184,12 +1184,20 @@ Singleton {
return Shape.scaledRadius(radius, limit, shapeScale, fixedRadius);
}
function innerRadius(token) {
return Shape.innerRadius(token, shapeScale, fixedRadius);
}
function fullRadius(width, height) {
return Shape.fullRadius(width, height, shapeScale, fixedRadius);
}
readonly property bool classicStyle: typeof SettingsData !== "undefined" && SettingsData.interfaceStyle === "classic"
readonly property bool shapeMorph: !classicStyle
readonly property bool pillControls: !classicStyle
function buttonRadius(width, height, sizeHeight, pressed, round) {
return Shape.buttonRadius(width, height, sizeHeight, pressed, round, shapeScale, fixedRadius);
return Shape.buttonRadius(width, height, sizeHeight, pressed, round, shapeScale, fixedRadius, shapeMorph, pillControls);
}
readonly property int densityLevel: {
@@ -1209,7 +1217,7 @@ Singleton {
readonly property real densityStepM: densityLevel * spacingS
readonly property real densityStepXS: densityLevel * spacingXXS
readonly property real groupedListGap: spacingXXS
readonly property real groupedListInnerRadius: cornerRadiusXS
readonly property real groupedListInnerRadius: innerRadius("xs")
readonly property real groupedListOuterRadius: cornerRadiusL
property string fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DCommon.Fonts.sans
@@ -1287,11 +1295,11 @@ Singleton {
readonly property real avatarSize: 36
readonly property real sliderTrackHeight: 16
readonly property real sliderHandleWidth: 4
readonly property real sliderHandleWidthPressed: 2
readonly property real sliderHandleWidthPressed: shapeMorph ? 2 : sliderHandleWidth
readonly property real sliderHandleHeight: 28
readonly property real sliderHandleGap: 6
readonly property real sliderTrackHeightS: 24
readonly property real sliderHandleHeightS: 36
readonly property real sliderTrackHeightS: classicStyle ? sliderTrackHeight : 24
readonly property real sliderHandleHeightS: classicStyle ? sliderHandleHeight : 36
readonly property real sliderTrackHeightM: 40
readonly property real sliderHandleHeightM: 52
readonly property real sliderTrackHeightL: 56
@@ -474,6 +474,10 @@ var LOCAL_SPEC = {
def: "comfortable",
onChange: "applyDensityPresets"
},
interfaceStyle: {
def: "expressive",
onChange: "applyInterfaceStyle"
},
m3ElevationEnabled: {
def: true
},
@@ -519,6 +523,10 @@ var LOCAL_SPEC = {
def: 0,
coerce: Util.percentToUnit
},
sectionDividerOpacity: {
def: 0.1,
coerce: Util.percentToUnit
},
focusRingEnabled: {
def: true
},
+10 -4
View File
@@ -173,9 +173,15 @@ function extractPins(obj) {
return pins;
}
function styleDefault(key, style) {
if (key === "radiusMode" && style === "classic")
return "fixed";
return undefined;
}
// Sizes absent from the file load at the saved density's preset, as a reset would; a save omits only that preset.
function densityDefault(key, density) {
var def = SpecModule.SPEC[key].def;
function densityDefault(key, density, style) {
var def = styleDefault(key, style) ?? SpecModule.SPEC[key].def;
if (key === "barConfigs" || key === "dockConfigs") {
var presets = DensityPresets.values(key === "barConfigs" ? "bar" : "dock", density);
return def.map(function (config) {
@@ -204,7 +210,7 @@ function parse(root, jsonObj) {
if (SPEC[k].persist === false)
continue;
if (!(k in jsonObj))
assign(root, k, Util.cloneDef(densityDefault(k, jsonObj.density)));
assign(root, k, Util.cloneDef(densityDefault(k, jsonObj.density, jsonObj.interfaceStyle)));
}
for (var k in jsonObj) {
@@ -243,7 +249,7 @@ function toJson(root, setKeys) {
value = withoutInstancePositions(value);
if (k === "builtInPluginSettings")
value = withoutSessionBackedPluginState(value);
if (!setKeys.has(k) && Util.isDefault(value, densityDefault(k, root.density)))
if (!setKeys.has(k) && Util.isDefault(value, densityDefault(k, root.density, root.interfaceStyle)))
continue;
out[k] = value;
}
+107 -77
View File
@@ -360,49 +360,64 @@ Item {
bottomPadding: SettingsMetrics.paneMargin
spacing: SettingsMetrics.sidebarGroupGap
Column {
id: searchResultsColumn
Item {
id: searchResultsFrame
width: parent.width - parent.leftPadding - parent.rightPadding
spacing: Theme.groupedListGap
height: searchResultsColumn.height
visible: root.searchActive
Repeater {
id: searchResultsRepeater
model: ScriptModel {
values: SettingsSearchService.results
Column {
id: searchResultsColumn
width: parent.width
spacing: SettingsMetrics.groupGap
Repeater {
id: searchResultsRepeater
model: ScriptModel {
values: SettingsSearchService.results
}
SettingsSidebarItem {
id: resultDelegate
required property int index
required property var modelData
onActiveFocusChanged: {
if (activeFocus)
root.ensureRowVisible(resultDelegate);
}
isFirstInGroup: index === 0
isLastInGroup: index === SettingsSearchService.results.length - 1
iconName: modelData.icon || "settings"
title: modelData.label
hint: modelData.category
accent: SettingsTabs.accentFor(modelData.page || SettingsTabs.pageForTabIndex(modelData.tabIndex))
active: root.searchSelectedIndex === index
onClicked: keyboard => {
root.searchSelectedIndex = index;
root.selectSearchResult(modelData, keyboard);
}
}
}
SettingsSidebarItem {
id: resultDelegate
required property int index
required property var modelData
onActiveFocusChanged: {
if (activeFocus)
root.ensureRowVisible(resultDelegate);
}
isFirstInGroup: index === 0
isLastInGroup: index === SettingsSearchService.results.length - 1
iconName: modelData.icon || "settings"
title: modelData.label
hint: modelData.category
accent: SettingsTabs.accentFor(modelData.page || SettingsTabs.pageForTabIndex(modelData.tabIndex))
active: root.searchSelectedIndex === index
onClicked: keyboard => {
root.searchSelectedIndex = index;
root.selectSearchResult(modelData, keyboard);
}
StyledText {
width: parent.width
text: I18n.tr("No matches")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: searchField.text.length > 0 && SettingsSearchService.results.length === 0
topPadding: Theme.spacingM
}
}
StyledText {
width: parent.width
text: I18n.tr("No matches")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: searchField.text.length > 0 && SettingsSearchService.results.length === 0
topPadding: Theme.spacingM
Rectangle {
anchors.fill: searchResultsColumn
radius: Theme.groupedListOuterRadius
color: "transparent"
border.width: SettingsMetrics.groupFrameWidth
border.color: Theme.outlineMedium
visible: border.width > 0
}
}
@@ -414,55 +429,70 @@ Item {
Repeater {
model: root.navGroups
Column {
id: groupColumn
Item {
id: groupFrame
required property var modelData
width: parent.width
spacing: Theme.groupedListGap
height: groupColumn.height
readonly property var visibleIds: {
const ids = [];
for (const entry of modelData) {
if (SettingsTabs.isVisible(SettingsTabs.page(entry.id)))
ids.push(entry.id);
Column {
id: groupColumn
width: parent.width
spacing: SettingsMetrics.groupGap
readonly property var visibleIds: {
const ids = [];
for (const entry of modelData) {
if (SettingsTabs.isVisible(SettingsTabs.page(entry.id)))
ids.push(entry.id);
}
return ids;
}
Repeater {
model: groupFrame.modelData
SettingsSidebarItem {
id: categoryRow
required property var modelData
onActiveFocusChanged: {
if (activeFocus)
root.ensureRowVisible(categoryRow);
}
readonly property bool isHighlighted: root.keyboardHighlightId === modelData.id
onIsHighlightedChanged: {
if (isHighlighted)
Qt.callLater(root.ensureRowVisible, categoryRow);
}
width: groupColumn.width
visible: SettingsTabs.isVisible(SettingsTabs.page(modelData.id))
isFirstInGroup: groupColumn.visibleIds[0] === modelData.id
isLastInGroup: groupColumn.visibleIds[groupColumn.visibleIds.length - 1] === modelData.id
iconName: modelData.icon || ""
title: modelData.text || ""
hint: SettingsTabs.hubHint(modelData)
accent: SettingsTabs.accentFor(modelData.id)
active: root.activeCategoryId === modelData.id && !SettingsTabs.isPluginPage(root.currentPage)
highlighted: isHighlighted
onClicked: keyboard => {
root.keyboardHighlightId = "";
root.pageRequested(modelData.id);
root.focusAfterNavigation(keyboard);
}
}
}
return ids;
}
Repeater {
model: groupColumn.modelData
SettingsSidebarItem {
id: categoryRow
required property var modelData
onActiveFocusChanged: {
if (activeFocus)
root.ensureRowVisible(categoryRow);
}
readonly property bool isHighlighted: root.keyboardHighlightId === modelData.id
onIsHighlightedChanged: {
if (isHighlighted)
Qt.callLater(root.ensureRowVisible, categoryRow);
}
width: groupColumn.width
visible: SettingsTabs.isVisible(SettingsTabs.page(modelData.id))
isFirstInGroup: groupColumn.visibleIds[0] === modelData.id
isLastInGroup: groupColumn.visibleIds[groupColumn.visibleIds.length - 1] === modelData.id
iconName: modelData.icon || ""
title: modelData.text || ""
hint: SettingsTabs.hubHint(modelData)
accent: SettingsTabs.accentFor(modelData.id)
active: root.activeCategoryId === modelData.id && !SettingsTabs.isPluginPage(root.currentPage)
highlighted: isHighlighted
onClicked: keyboard => {
root.keyboardHighlightId = "";
root.pageRequested(modelData.id);
root.focusAfterNavigation(keyboard);
}
}
Rectangle {
anchors.fill: groupColumn
radius: Theme.groupedListOuterRadius
color: "transparent"
border.width: SettingsMetrics.groupFrameWidth
border.color: Theme.outlineMedium
visible: border.width > 0
}
}
}
@@ -33,6 +33,12 @@ Singleton {
function sheetWidthFor(columns) {
return sheetPadding * 2 + columns * columnWidth + (columns - 1) * gridGap;
}
function pillRadius(width, height) {
if (Theme.pillControls)
return Theme.fullRadius(width, height);
return Math.min(Theme.cornerRadiusL, width / 2, height / 2);
}
readonly property real maxHeightInset: 100
// Short-grid floors scale with the row unit, or compact would pad small layouts back out.
readonly property real gridScale: tileHeight / baseTileHeight
@@ -67,7 +67,7 @@ DEditableGrid {
gravity: true
rowStep: CcMetrics.gridStep
packer: items => GridUtils.packCells(clamped(items), placementOrder, columns, null, CcMetrics.gridStep, true).cells
placeholderRadius: draggingSlot?.small ? Theme.fullRadius(draggingSlot.width, draggingSlot.height) : (draggingSlot?.tileItem?.bodyRadius ?? Theme.fullRadius(width, CcMetrics.tileHeight))
placeholderRadius: draggingSlot?.small ? CcMetrics.pillRadius(draggingSlot.width, draggingSlot.height) : (draggingSlot?.tileItem?.bodyRadius ?? CcMetrics.pillRadius(width, CcMetrics.tileHeight))
commitHandler: items => model.setLayout(withHidden(items))
onEditModeChanged: {
@@ -131,7 +131,7 @@ DEditableGridSlot {
passthrough: root.passthrough
dragging: root.dragging
resizing: root.resizing
cornerRadius: root.small ? Theme.fullRadius(root.width, root.height - root.bodyInset * 2) : (root.tileItem?.bodyRadius ?? Theme.fullRadius(root.width, root.height))
cornerRadius: root.small ? CcMetrics.pillRadius(root.width, root.height - root.bodyInset * 2) : (root.tileItem?.bodyRadius ?? CcMetrics.pillRadius(root.width, root.height))
sizeText: root.cols + "×" + (root.small ? CcMetrics.smallRowFraction : root.rows)
onResizeStarted: (px, py) => {
root.biasH = root.small ? CcMetrics.smallRowFraction * root.grid.cellWidth - CcMetrics.gridGap - root.height : 0;
@@ -27,7 +27,7 @@ Item {
readonly property bool receiving: groupId !== "" && host?.hoverGroup === groupId
// Members keep their own input in edit mode so they can be dragged out instead of moving the group.
readonly property Item passthrough: memberRow
readonly property real bodyRadius: Theme.fullRadius(width, Math.min(height, CcMetrics.tileHeight))
readonly property real bodyRadius: CcMetrics.pillRadius(width, Math.min(height, CcMetrics.tileHeight))
readonly property bool memberDragging: liftedIndex >= 0
property int liftedIndex: -1
@@ -13,11 +13,11 @@ Rectangle {
signal toggled
readonly property color contentColor: enabled ? (pinned ? Theme.onSecondaryContainer : Theme.onSurfaceVariant) : Theme.onSurface_38
readonly property real restRadius: Math.min(pinned ? Theme.cornerRadiusXL : Theme.cornerRadiusM, height / 2)
readonly property real restRadius: Math.min(pinned && Theme.shapeMorph ? Theme.cornerRadiusXL : Theme.cornerRadiusM, height / 2)
height: Theme.buttonHeightXS
width: iconOnly ? height : content.implicitWidth + Theme.spacingM * 2
radius: layer.pressed ? Math.min(Theme.cornerRadiusS, height / 2) : restRadius
radius: layer.pressed && Theme.shapeMorph ? Math.min(Theme.cornerRadiusS, height / 2) : restRadius
color: pinned ? Theme.secondaryContainer : "transparent"
border.width: pinned ? 0 : Theme.outlineWidth
border.color: Theme.outlineVariant
@@ -80,9 +80,9 @@ Item {
readonly property bool hasIconBox: (showExpand || opensPage || expanded) && !compact
readonly property bool showsActive: active && interactive
readonly property real restRadius: {
if (showsActive)
if (showsActive && Theme.shapeMorph)
return Math.min(CcMetrics.tileActiveRadius, width / 2, bodyHeight / 2);
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, bodyHeight);
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : CcMetrics.pillRadius(width, bodyHeight);
}
property bool acceptsInput: interactive && enabled
readonly property bool bodyActive: showsActive && !hasIconBox && !bare
@@ -121,13 +121,13 @@ Item {
return showsActive ? CcMetrics.tileActiveColor : CcMetrics.iconBoxInactiveColor;
}
property real bodyRadius: bodyLayer.pressed ? Math.min(Theme.cornerRadiusM, width / 2, bodyHeight / 2) : restRadius
property real bodyRadius: bodyLayer.pressed && Theme.shapeMorph ? Math.min(Theme.cornerRadiusM, width / 2, bodyHeight / 2) : restRadius
property real iconBoxRadius: {
if (boxLayer.pressed)
if (boxLayer.pressed && Theme.shapeMorph)
return Math.min(Theme.cornerRadiusS, CcMetrics.iconBoxSize / 2);
if (showsActive)
if (showsActive && Theme.shapeMorph)
return Math.min(CcMetrics.iconBoxActiveRadius, CcMetrics.iconBoxSize / 2);
return Theme.fullRadius(CcMetrics.iconBoxSize, CcMetrics.iconBoxSize);
return CcMetrics.pillRadius(CcMetrics.iconBoxSize, CcMetrics.iconBoxSize);
}
width: parent?.width ?? 0
@@ -34,7 +34,7 @@ Item {
readonly property real bodyRadius: {
if (!background)
return Theme.fullRadius(avatarSide, avatarSide);
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, height);
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : CcMetrics.pillRadius(width, height);
}
readonly property real fallbackGlyphRatio: 0.5
readonly property string avatarSource: PortalService.profileImageUrl
+3 -1
View File
@@ -30,11 +30,13 @@ Singleton {
return Math.min(Theme.cornerRadiusS, thickness / 2);
if (thickness < compactPillThickness)
return Math.min(Theme.cornerRadiusM, thickness / 2);
return Theme.fullRadius(thickness, thickness);
return Theme.pillControls ? Theme.fullRadius(thickness, thickness) : Math.min(Theme.cornerRadiusL, thickness / 2);
}
function cornerRadius(thickness, style, joined, pressProgress) {
const resting = joined ? Math.min(segmentInnerRadius, thickness / 2) : pillRadius(thickness, style);
if (!Theme.shapeMorph)
return resting;
const pressed = Math.min(resting, joined || style === "flat" ? segmentPressedInnerRadius : pressedRadius);
return Math.max(0, resting + (pressed - resting) * Math.max(0, Math.min(1, pressProgress)));
}
+7 -1
View File
@@ -220,7 +220,13 @@ BasePill {
height: parent.height
buttonSize: parent.width
iconSize: 14 * root.contentScale
radius: pressed ? Theme.cornerRadiusXS : (checked ? Theme.cornerRadiusFull : Theme.cornerRadiusS)
radius: {
if (!Theme.shapeMorph)
return Theme.cornerRadiusS;
if (pressed)
return Theme.cornerRadiusXS;
return checked ? Theme.cornerRadiusFull : Theme.cornerRadiusS;
}
variant: "filled"
round: false
checkable: true
@@ -40,7 +40,7 @@ Item {
iconName: "power_settings_new"
iconColor: Theme.onSecondaryContainer
backgroundColor: root.round ? Theme.secondaryContainer : "transparent"
radius: pressed ? Theme.cornerRadiusS : Theme.fullRadius(width, height)
radius: pressed && Theme.shapeMorph ? Theme.cornerRadiusS : (Theme.pillControls ? Theme.fullRadius(width, height) : Math.min(Theme.cornerRadiusL, width / 2, height / 2))
buttonSize: Theme.buttonHeightM
onClicked: root.lockHost?.showPowerMenu()
}
@@ -521,7 +521,6 @@ Item {
minButtonWidth: 56
buttonPadding: Theme.spacingS
textSize: Theme.fontSizeSmall
spacing: Theme.groupedListGap
onSelectionChanged: (index, selected) => root.setVisibleEntryAction(index, selected)
}
}
@@ -346,7 +346,7 @@ Item {
readonly property bool isExpanded: root.expandedWifiSsid === modelData.ssid
width: parent?.width ?? 0
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
SettingsRow {
id: wifiNetworkRow
title: wifiNetworkDelegate.modelData.ssid || I18n.tr("Unknown")
@@ -525,7 +525,7 @@ Item {
readonly property var detailFields: networkWifiTab.wifiDetailFields(modelData)
width: parent?.width ?? 0
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
SettingsRow {
id: savedWifiRow
+1 -1
View File
@@ -482,7 +482,7 @@ Item {
readonly property bool isStopped: printerData?.state === "stopped"
width: parent?.width ?? 0
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
SettingsRow {
iconName: printerDelegate.isStopped ? "print_disabled" : "print"
@@ -472,7 +472,7 @@ Item {
Column {
width: parent?.width ?? 0
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
Repeater {
model: root.packagesExpanded && !SystemUpdateService.isUpgrading ? SystemUpdateService.systemUpdates.slice(0, root.packageListCap) : []
@@ -251,7 +251,20 @@ Column {
SettingsCard {
tab: "theme"
title: I18n.tr("Density", "appearance settings section label")
title: I18n.tr("Appearance")
settingKey: "surfaceAppearance"
SettingsButtonGroupRow {
tab: "theme"
tags: ["style", "classic", "expressive", "pill", "round", "shape", "morph", "corners", "buttons"]
settingKey: "interfaceStyle"
text: I18n.tr("Style")
model: [I18n.tr("Expressive"), I18n.tr("Classic", "interface style option with one corner radius on controls and no shape change on press")]
values: ["expressive", "classic"]
value: SettingsData.interfaceStyle
fallbackValue: "expressive"
onValueSelected: value => SettingsData.set("interfaceStyle", value)
}
SettingsButtonGroupRow {
tab: "theme"
@@ -264,6 +277,20 @@ Column {
fallbackValue: "comfortable"
onValueSelected: value => SettingsData.set("density", value)
}
SettingsSliderRow {
tab: "theme"
tags: ["classic", "divider", "dividers", "lines", "rows", "sections", "sidebar", "outline"]
settingKey: "sectionDividerOpacity"
visible: SettingsData.interfaceStyle === "classic"
text: I18n.tr("Section dividers", "classic interface style: strength of the lines between rows of a settings section")
minimumLabel: I18n.tr("Off")
minimum: 0
maximum: 40
step: 1
value: Math.round((SettingsData.sectionDividerOpacity ?? 0) * 100)
onSliderValueChanged: newValue => SettingsData.set("sectionDividerOpacity", newValue / 100)
}
}
SettingsCard {
@@ -277,7 +304,7 @@ Column {
tags: ["corner", "radius", "rounded", "square", "fixed", "material", "shape"]
settingKey: "radiusMode"
text: I18n.tr("Corner style")
model: [I18n.tr("Material scale", "corner style: Material shape scale with a strength slider"), I18n.tr("Fixed", "corner style: one radius for every corner")]
model: [I18n.tr("Material", "corner style: Material shape scale with a strength slider"), I18n.tr("Fixed", "corner style: one radius for every corner")]
currentIndex: SettingsData.radiusMode === "fixed" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (selected)
@@ -77,13 +77,16 @@ Column {
readonly property bool hasHidden: disabledGroups.length > 0
property bool showHidden: false
readonly property bool isFirstInGroup: parent?.isSettingsGroupHost ? parent.isEdge(root, true) : true
readonly property bool isLastInGroup: parent?.isSettingsGroupHost ? parent.isEdge(root, false) : true
readonly property real topRadius: isFirstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
readonly property real bottomRadius: isLastInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
// A Loader may hold the editor, so the slot the group sees is the Loader, not the editor
readonly property Item groupSlot: parent?.isSettingsGroupHost ? root : parent
readonly property bool inGroup: groupSlot?.parent?.isSettingsGroupHost ?? false
readonly property bool isFirstInGroup: inGroup ? groupSlot.parent.isEdge(groupSlot, true) : true
readonly property bool isLastInGroup: inGroup ? groupSlot.parent.isEdge(groupSlot, false) : true
readonly property real topRadius: isFirstInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
readonly property real bottomRadius: isLastInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
width: parent?.width ?? 0
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
SettingsSearchRegistration {
target: root
@@ -91,8 +94,10 @@ Column {
}
SettingsRow {
inGroup: root.inGroup
dividerBefore: root.inGroup && !root.isFirstInGroup
topRadius: root.topRadius
bottomRadius: Theme.groupedListInnerRadius
bottomRadius: SettingsMetrics.groupInnerRadius
body: StyledText {
width: parent.width
text: I18n.tr("Drag groups above the clock to sit left of it, below to sit right. Click the eye to hide a group.", "island settings: home layout editor hint")
@@ -114,8 +119,10 @@ Column {
required property var modelData
reorderList: enabledList
topRadius: dragging ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomRadius: dragging ? Theme.groupedListOuterRadius : (position === enabledList.count - 1 && !root.hasHidden ? root.bottomRadius : Theme.groupedListInnerRadius)
inGroup: root.inGroup
dividerBefore: root.inGroup && !dragging
topRadius: dragging ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
bottomRadius: dragging ? Theme.groupedListOuterRadius : (position === enabledList.count - 1 && !root.hasHidden ? root.bottomRadius : SettingsMetrics.groupInnerRadius)
title: root.presentation[modelData.id].text
subtitle: root.presentation[modelData.id].description
iconName: root.presentation[modelData.id].icon
@@ -139,8 +146,10 @@ Column {
}
SettingsRow {
topRadius: Theme.groupedListInnerRadius
bottomRadius: root.showHidden ? Theme.groupedListInnerRadius : root.bottomRadius
inGroup: root.inGroup
dividerBefore: root.inGroup
topRadius: SettingsMetrics.groupInnerRadius
bottomRadius: root.showHidden ? SettingsMetrics.groupInnerRadius : root.bottomRadius
title: I18n.tr("Hidden (%1)", "island settings: hidden groups divider, %1 is count").arg(root.disabledGroups.length)
iconName: "visibility_off"
visible: root.hasHidden
@@ -157,7 +166,7 @@ Column {
Column {
width: parent.width
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
visible: root.showHidden && root.hasHidden
Repeater {
@@ -169,8 +178,10 @@ Column {
required property int index
required property var modelData
topRadius: Theme.groupedListInnerRadius
bottomRadius: index === root.disabledGroups.length - 1 ? root.bottomRadius : Theme.groupedListInnerRadius
inGroup: root.inGroup
dividerBefore: root.inGroup
topRadius: SettingsMetrics.groupInnerRadius
bottomRadius: index === root.disabledGroups.length - 1 ? root.bottomRadius : SettingsMetrics.groupInnerRadius
title: root.presentation[modelData.id].text
subtitle: root.presentation[modelData.id].description
iconName: root.presentation[modelData.id].icon
@@ -36,7 +36,6 @@ SettingsRow {
title: text
subtitle: description
spacing: Theme.groupedListGap
Item {
id: trailingHost
@@ -60,7 +59,6 @@ SettingsRow {
x: root.compact ? (parent.width - width) / 2 : 0
selectionMode: "single"
currentIndex: root.values.length > 0 ? root.indexOfValue(root.value) : 0
spacing: root.spacing
enabled: root.enabled
maximumWidth: root.compact ? root.width - SettingsMetrics.rowPaddingH * 2 : -1
onSelectionChanged: (index, selected) => {
@@ -22,7 +22,7 @@ Column {
signal hideOnLastDisplayToggled(bool checked)
width: parent?.width ?? 0
spacing: Theme.groupedListGap
spacing: SettingsMetrics.groupGap
function screenPref(screen) {
const pref = {
@@ -8,7 +8,7 @@ Item {
readonly property bool isSettingsGroup: true
default property alias content: host.data
property real spacing: Theme.groupedListGap
property real spacing: SettingsMetrics.groupGap
property real customPaddingH: SettingsMetrics.rowPaddingH
property real customPaddingV: SettingsMetrics.rowPaddingV
property bool highlighted: false
@@ -120,14 +120,22 @@ Item {
width: root.width
height: slot.height
visible: !slot.transparent
topLeftRadius: first ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
topRightRadius: first ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomLeftRadius: last ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomRightRadius: last ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
topLeftRadius: first ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
topRightRadius: first ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
bottomLeftRadius: last ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
bottomRightRadius: last ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
color: root.highlighted ? Theme.blend(root.slotColor, Theme.primary, SettingsMetrics.highlightBlend) : root.slotColor
border.width: Theme.layerOutlineWidth
border.width: SettingsMetrics.groupEdgeWidth
border.color: Theme.outlineMedium
Rectangle {
x: SettingsMetrics.rowPaddingH
width: parent.width - SettingsMetrics.rowPaddingH * 2
height: Theme.dividerWidth
visible: SettingsMetrics.continuousGroups && !parent.first
color: SettingsMetrics.groupDividerColor
}
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
ColorAnimation {
@@ -139,6 +147,16 @@ Item {
}
}
Rectangle {
anchors.fill: parent
z: 1
radius: Theme.groupedListOuterRadius
color: "transparent"
border.width: SettingsMetrics.groupFrameWidth
border.color: Theme.outlineMedium
visible: border.width > 0
}
Item {
id: host
@@ -36,6 +36,14 @@ Singleton {
readonly property real sectionLabelTopGap: Math.max(Theme.spacingXS, Theme.spacingS + Theme.densityStepS)
readonly property real sectionLabelBottomGap: Theme.spacingM
readonly property real navIconSize: Theme.avatarSize
readonly property bool navIconBadges: !Theme.classicStyle
readonly property bool continuousGroups: Theme.classicStyle
readonly property real groupGap: continuousGroups ? 0 : Theme.groupedListGap
readonly property real groupInnerRadius: continuousGroups ? 0 : Theme.groupedListInnerRadius
// Rows share one frame in a continuous group so Layer outline never doubles up between them
readonly property real groupEdgeWidth: continuousGroups ? 0 : Theme.layerOutlineWidth
readonly property real groupFrameWidth: continuousGroups ? Theme.layerOutlineWidth : 0
readonly property color groupDividerColor: Theme.withAlpha(Theme.outline, SettingsData.sectionDividerOpacity)
readonly property real navItemMinHeight: rowMinHeight
readonly property real sidebarGroupGap: Theme.spacingS
readonly property real searchBarGap: Theme.spacingM
@@ -1,4 +1,5 @@
import QtQuick
import qs.Common
import qs.DCommon.Widgets
import "../../../Common/QmlUtils.js" as QmlUtils
@@ -9,5 +10,6 @@ DReorderList {
readonly property bool transparentSlot: true
readonly property bool firstInGroup: parent?.isSettingsGroupHost ? parent.isEdge(root, true) : true
readonly property bool lastInGroup: parent?.isSettingsGroupHost ? parent.isEdge(root, false) : true
spacing: SettingsMetrics.groupGap
Component.onCompleted: flickable = QmlUtils.findParentFlickable(root.parent)
}
@@ -14,8 +14,8 @@ SettingsRow {
paintBackground: true
rowColor: dragging ? Theme.blend(SettingsMetrics.rowColor, Theme.onSurface, Theme.stateLayerDrag) : SettingsMetrics.rowColor
topRadius: dragging || (position === 0 && reorderList.firstInGroup) ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomRadius: dragging || (position === reorderList.count - 1 && reorderList.lastInGroup) ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
topRadius: dragging || (position === 0 && reorderList.firstInGroup) ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
bottomRadius: dragging || (position === reorderList.count - 1 && reorderList.lastInGroup) ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
z: dragging ? 100 : 0
function focusHandle(reason) {
@@ -53,10 +53,12 @@ T.Control {
readonly property bool hasText: title !== "" || subtitle !== ""
readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey
property bool highlighted: isHighlighted
property bool inGroup: (groupItem.parent?.isSettingsGroupHost ?? false) || (groupItem.parent?.parent?.isSettingsGroupHost ?? false)
readonly property bool isFirstInGroup: _edge(true)
readonly property bool isLastInGroup: _edge(false)
property real topRadius: isFirstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
property real bottomRadius: isLastInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
property bool dividerBefore: inGroup && !isFirstInGroup
property real topRadius: isFirstInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
property real bottomRadius: isLastInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
property real minHeight: subtitle !== "" ? SettingsMetrics.rowMinHeightTwoLine : SettingsMetrics.rowMinHeight
// Keyboard focus on the row or any control inside it tints the row, so navigation reads without a focus ring
readonly property Item activeItem: Window.activeFocusItem
@@ -141,8 +143,16 @@ T.Control {
anchors.fill: parent
visible: root.paintBackground
color: root.active ? Theme.selectedContainer : root.highlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor
border.width: Theme.layerOutlineWidth
border.width: root.inGroup ? SettingsMetrics.groupEdgeWidth : Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
x: root.paddingH
width: parent.width - root.paddingH * 2
height: Theme.dividerWidth
visible: SettingsMetrics.continuousGroups && root.dividerBefore
color: SettingsMetrics.groupDividerColor
}
topLeftRadius: root.topRadius
topRightRadius: root.topRadius
bottomLeftRadius: root.bottomRadius
@@ -33,12 +33,16 @@ Rectangle {
}
readonly property var accentPair: Theme.accent(accent)
readonly property color badgeColor: accentPair?.container ?? Theme.withAlpha(Theme.primary, Theme.tonalTintAlpha)
readonly property color glyphColor: accentPair?.onContainer ?? Theme.primary
readonly property color badgeColor: SettingsMetrics.navIconBadges ? accentPair?.container ?? Theme.withAlpha(Theme.primary, Theme.tonalTintAlpha) : "transparent"
readonly property color glyphColor: {
if (SettingsMetrics.navIconBadges)
return accentPair?.onContainer ?? Theme.primary;
return active ? Theme.onSelectedContainer : Theme.primary;
}
property bool isFirstInGroup: true
property bool isLastInGroup: true
property real topRadius: active || isFirstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
property real bottomRadius: active || isLastInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
property real topRadius: (active && Theme.shapeMorph) || isFirstInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
property real bottomRadius: (active && Theme.shapeMorph) || isLastInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
Behavior on topRadius {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
@@ -65,9 +69,17 @@ Rectangle {
bottomLeftRadius: bottomRadius
bottomRightRadius: bottomRadius
color: active ? SettingsMetrics.selectedRowColor : SettingsMetrics.sidebarRowColor
border.width: Theme.layerOutlineWidth
border.width: SettingsMetrics.groupEdgeWidth
border.color: Theme.outlineMedium
Rectangle {
x: Theme.spacingL
width: parent.width - Theme.spacingL * 2
height: Theme.dividerWidth
visible: SettingsMetrics.continuousGroups && !root.isFirstInGroup && !root.active
color: SettingsMetrics.groupDividerColor
}
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
ColorAnimation {
@@ -10,6 +10,8 @@ Item {
readonly property bool isSettingsRow: true
readonly property bool transparentSlot: true
readonly property bool inGroup: parent?.isSettingsGroupHost ?? false
readonly property bool isFirstInGroup: inGroup ? parent.isEdge(root, true) : true
property string startTitle: ""
property int startHour: 7
@@ -48,6 +50,15 @@ Item {
width: parent?.width ?? 0
height: halves.implicitHeight
Rectangle {
x: SettingsMetrics.rowPaddingH
z: 1
width: parent.width - SettingsMetrics.rowPaddingH * 2
height: Theme.dividerWidth
visible: SettingsMetrics.continuousGroups && root.inGroup && !root.isFirstInGroup
color: SettingsMetrics.groupDividerColor
}
Row {
id: halves
width: parent.width
@@ -62,6 +73,8 @@ Item {
width: root.showEnd ? (root.width - Theme.groupedListGap) / 2 : root.width
paintBackground: true
inGroup: root.inGroup
dividerBefore: false
topRadius: Theme.groupedListOuterRadius
bottomRadius: Theme.groupedListOuterRadius
title: isStart ? root.startTitle : root.endTitle
@@ -50,8 +50,8 @@ Item {
SettingsToggleRow {
id: header
width: parent.width
topRadius: root.firstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomRadius: root.lastInGroup && !root.expanded ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
topRadius: root.firstInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
bottomRadius: root.lastInGroup && !root.expanded ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
tab: root.tab
tags: root.tags
settingKey: root.settingKey
@@ -59,8 +59,8 @@ Item {
SettingsToggleRow {
id: header
width: parent.width
topRadius: root.firstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomRadius: root.lastInGroup && !root.checked ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
topRadius: root.firstInGroup ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
bottomRadius: root.lastInGroup && !root.checked ? Theme.groupedListOuterRadius : SettingsMetrics.groupInnerRadius
tab: root.tab
tags: root.tags
settingKey: root.settingKey
+2
View File
@@ -50,6 +50,8 @@ Theme.buttonRadius(width, height, buttonHeight, pressed, true)
Use `fullRadius()` for pills and round controls so lower strength values reduce their rounding. `cornerRadiusFull` remains available for compatibility. See the shared [shape reference](../../dux-qml-common/SHAPES.md) for component baselines.
`Theme.buttonRadius(...)` follows the Interface style setting: Classic turns off pills and the press morph. Plugins that hand-roll control shapes can read `Theme.pillControls` and `Theme.shapeMorph` (both false in Classic).
## Colors
### Surface Colors
+1 -1
View File
@@ -21,7 +21,7 @@ DCard {
clickable: true
pad: Theme.spacingL
restRadius: Theme.groupedListOuterRadius
bodyRadius: focusTarget.pressed ? Theme.cornerRadiusM : restRadius
bodyRadius: focusTarget.pressed && Theme.shapeMorph ? Theme.cornerRadiusM : restRadius
implicitHeight: body.implicitHeight + pad * 2
Accessible.name: heading
Accessible.description: description
+22 -1
View File
@@ -161,12 +161,14 @@ test("set and reset refuse managed keys and change the others", () => {
AdminPolicy: { lockSettings: { lockBeforeSuspend: true } },
Spec: { SPEC, set: (root, key, value) => changes.push([key, value]) },
DensityPresets: loadScript(new URL("../Common/settings/DensityPresets.js", import.meta.url)),
Store: SettingsStore,
density: "comfortable",
interfaceStyle: "expressive",
log: { warn: (...args) => warnings.push(args.at(-1)) },
saveSettings() {},
_hooks: {},
lockBeforeSuspend: true
}, ["isManaged", "set", "resetToDefault"]);
}, ["isManaged", "set", "resetToDefault", "presetDefault"]);
context.set("lockBeforeSuspend", false);
context.set("lockAtStartup", true);
context.resetToDefault(["lockBeforeSuspend", "acLockTimeout"]);
@@ -237,6 +239,25 @@ test("settings set refuses an unknown density, from IPC too, without saving or m
assert.deepEqual([moves, saves, SettingsData.density], [[["comfortable", "dense"]], 1, "dense"]);
});
test("settings set refuses an unknown interface style, from IPC too, without saving", () => {
let saves = 0;
const SettingsData = settingsDataContext({
AdminPolicy: { lockSettings: {} },
Spec: loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)),
log: { warn() {} },
saveSettings: () => saves++,
_hooks: {},
interfaceStyle: "expressive"
}, ["isManaged", "hasSetting", "set"]);
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet: false }, SettingsData, log: { warn() {} } });
for (const handler of settingsIpcHandlers())
vm.runInContext(handler, context);
for (const value of ["Classic", "flat", ""])
assert.equal(context.set("interfaceStyle", value), "SETTINGS_SET_FAILURE", value);
assert.equal(context.set("interfaceStyle", "classic"), "SETTINGS_SET_SUCCESS");
assert.deepEqual([saves, SettingsData.interfaceStyle], [1, "classic"]);
});
test("the legacy session migration leaves managed lock settings alone", () => {
const managed = new Set(["acLockTimeout", "lockBeforeSuspend"]);
const changes = [];
+3 -1
View File
@@ -137,8 +137,10 @@ test("an unknown previous level moves sizes from comfortable, the level Theme re
test("resets land on the current level's preset", () => {
const changes = [];
const settings = settingsFunctions(["barConfigDefault", "dockConfigDefaults", "specDefault", "isDefault", "resetToDefault", "isManaged"], vm.createContext({
const settings = settingsFunctions(["barConfigDefault", "dockConfigDefaults", "specDefault", "isDefault", "resetToDefault", "isManaged", "presetDefault"], vm.createContext({
DensityPresets: presets,
Store: loadScript(new URL("../Common/settings/SettingsStore.js", import.meta.url)),
interfaceStyle: "expressive",
DockConfig: loadScript(new URL("../Common/settings/DockConfig.js", import.meta.url)),
SpecUtil: loadScript(new URL("../DCommon/Common/settings/SpecUtil.js", import.meta.url)),
Spec: { SPEC: Spec.SPEC, set: (root, key, value) => changes.push([key, value]) },
+83
View File
@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import vm from "node:vm";
import { loadScript } from "./qml-script.mjs";
const Spec = loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url));
const store = loadScript(new URL("../Common/settings/SettingsStore.js", import.meta.url));
const presets = loadScript(new URL("../Common/settings/DensityPresets.js", import.meta.url));
function settingsFunctions(names, context) {
const source = readFileSync(new URL("../Common/SettingsData.qml", import.meta.url), "utf8");
context.root = context;
for (const name of names) {
const found = source.match(new RegExp(`^ function ${name}\\(([^)]*)\\) \\{\\n[\\s\\S]*?^ \\}`, "m"));
assert.ok(found, name);
vm.runInContext(found[0], context);
}
return context;
}
function settings(globals) {
return settingsFunctions(["applyInterfaceStyle", "presetDefault", "isManaged", "updateCompositorLayout"], vm.createContext(Object.assign({
Store: store,
Spec,
DensityPresets: presets,
AdminPolicy: { lockSettings: {} },
density: "comfortable",
compositorLayoutRefreshNeeded() {}
}, globals)));
}
test("a style change moves the corner style with it unless the user picked one", () => {
const cases = [
["expressive", "classic", "scale", "fixed"],
["classic", "expressive", "fixed", "scale"],
["expressive", "classic", "fixed", "fixed"],
["classic", "expressive", "scale", "scale"]
];
for (const [from, to, mode, expected] of cases) {
const root = settings({ interfaceStyle: from, radiusMode: mode });
Spec.set(root, "interfaceStyle", to, () => {}, { applyInterfaceStyle: root.applyInterfaceStyle });
assert.equal(root.radiusMode, expected, `${from} -> ${to} at ${mode}`);
}
const managed = settings({ interfaceStyle: "expressive", radiusMode: "scale", AdminPolicy: { lockSettings: { radiusMode: "scale" } } });
Spec.set(managed, "interfaceStyle", "classic", () => {}, { applyInterfaceStyle: managed.applyInterfaceStyle });
assert.equal(managed.radiusMode, "scale");
});
test("a load fills a missing corner style from the file's interface style, and a save omits only that", () => {
for (const [style, mode] of [["classic", "fixed"], ["expressive", "scale"]]) {
const root = { settingsConfigVersion: 999 };
store.parse(root, { configVersion: 999, interfaceStyle: style });
assert.equal(root.radiusMode, mode, style);
assert.equal("radiusMode" in store.toJson(root, new Set(["interfaceStyle"])), false, style);
root.radiusMode = mode === "fixed" ? "scale" : "fixed";
assert.equal(store.toJson(root, new Set(["interfaceStyle"])).radiusMode, root.radiusMode, style);
const kept = { settingsConfigVersion: 999 };
store.parse(kept, { configVersion: 999, interfaceStyle: style, radiusMode: root.radiusMode });
assert.equal(kept.radiusMode, root.radiusMode, style);
}
});
test("under classic the corner style's default, modified state and reset follow the style", () => {
const changes = [];
const root = settingsFunctions(["specDefault", "isDefault", "resetToDefault", "isManaged", "presetDefault"], vm.createContext({
Store: store,
DensityPresets: presets,
SpecUtil: loadScript(new URL("../DCommon/Common/settings/SpecUtil.js", import.meta.url)),
Spec: { SPEC: Spec.SPEC, set: (target, key, value) => { changes.push([key, value]); target[key] = value; } },
AdminPolicy: { lockSettings: {} },
saveSettings() {},
_hooks: {},
density: "comfortable",
interfaceStyle: "classic",
radiusMode: "scale"
}));
assert.equal(root.specDefault("radiusMode"), "fixed");
assert.equal(root.isDefault(["radiusMode"]), false);
root.resetToDefault(["radiusMode"]);
assert.deepEqual(changes, [["radiusMode", "fixed"]]);
assert.equal(root.isDefault(["radiusMode"]), true);
});
@@ -101,6 +101,26 @@
"page": "notifications",
"icon": "palette"
},
{
"section": "surfaceAppearance",
"label": "Appearance",
"tabIndex": -1,
"category": "Interface style",
"keywords": [
"appearance",
"buttons",
"classic",
"corners",
"expressive",
"interface",
"morph",
"pill",
"round",
"shape",
"style"
],
"page": "theme_surfaces"
},
{
"section": "wallpaper",
"label": "Appearance",
@@ -835,32 +855,6 @@
"icon": "schedule",
"description": "Clock, weather, locale"
},
{
"section": "density",
"label": "Density",
"tabIndex": -1,
"category": "Interface style",
"keywords": [
"bar",
"comfortable",
"compact",
"control center",
"dense",
"density",
"dock",
"frame",
"interface",
"island",
"menus",
"padding",
"rows",
"size",
"spacing",
"spacious",
"style"
],
"page": "theme_surfaces"
},
{
"section": "selectedMonitor",
"label": "Display",
@@ -1428,6 +1422,32 @@
"icon": "keyboard",
"description": "Shortcuts, keyboard, mouse"
},
{
"section": "density",
"label": "Interface density",
"tabIndex": -1,
"category": "Interface style",
"keywords": [
"bar",
"comfortable",
"compact",
"control center",
"dense",
"density",
"dock",
"frame",
"interface",
"island",
"menus",
"padding",
"rows",
"size",
"spacing",
"spacious",
"style"
],
"page": "theme_surfaces"
},
{
"section": "blurLayerOutline",
"label": "Layer outline",
@@ -2093,6 +2113,26 @@
"page": "theme_surfaces",
"icon": "unfold_more"
},
{
"section": "sectionDividerOpacity",
"label": "Section dividers",
"tabIndex": -1,
"category": "Interface style",
"keywords": [
"classic",
"divider",
"dividers",
"interface",
"lines",
"outline",
"rows",
"section",
"sections",
"sidebar",
"style"
],
"page": "theme_surfaces"
},
{
"section": "_hub_security_accounts",
"label": "Security",
@@ -2291,6 +2331,25 @@
],
"icon": "volume_up"
},
{
"section": "interfaceStyle",
"label": "Style",
"tabIndex": -1,
"category": "Interface style",
"keywords": [
"buttons",
"classic",
"corners",
"expressive",
"interface",
"morph",
"pill",
"round",
"shape",
"style"
],
"page": "theme_surfaces"
},
{
"section": "notificationSummaryFontSize",
"label": "Summary font size",