diff --git a/dux-qml-common b/dux-qml-common index 3d56a375f..b602c9e1b 160000 --- a/dux-qml-common +++ b/dux-qml-common @@ -1 +1 @@ -Subproject commit 3d56a375f29b691074cb61411356966da8ad060e +Subproject commit b602c9e1b7521bab36e3b5fb0f9485dd5ef6f531 diff --git a/flake.lock b/flake.lock index 5cc2b3094..fb458c63e 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 20c987d20..d54fc90f2 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -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); } } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 37332641c..14c1c4455 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -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 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 0886125bc..cf115b8ac 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -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 }, diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index a60ee3db9..7615e0d40 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -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; } diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index 1358edc43..d542e8c1b 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -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 } } } diff --git a/quickshell/Modules/ControlCenter/CcMetrics.qml b/quickshell/Modules/ControlCenter/CcMetrics.qml index 87602add9..ba6621a0f 100644 --- a/quickshell/Modules/ControlCenter/CcMetrics.qml +++ b/quickshell/Modules/ControlCenter/CcMetrics.qml @@ -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 diff --git a/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml b/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml index 06a7cb38e..b3369a793 100644 --- a/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml @@ -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: { diff --git a/quickshell/Modules/ControlCenter/Components/CcTileSlot.qml b/quickshell/Modules/ControlCenter/Components/CcTileSlot.qml index 788ee85da..f3652c5a1 100644 --- a/quickshell/Modules/ControlCenter/Components/CcTileSlot.qml +++ b/quickshell/Modules/ControlCenter/Components/CcTileSlot.qml @@ -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; diff --git a/quickshell/Modules/ControlCenter/Components/QuickTilesGroup.qml b/quickshell/Modules/ControlCenter/Components/QuickTilesGroup.qml index 368118930..ddf75a6bd 100644 --- a/quickshell/Modules/ControlCenter/Components/QuickTilesGroup.qml +++ b/quickshell/Modules/ControlCenter/Components/QuickTilesGroup.qml @@ -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 diff --git a/quickshell/Modules/ControlCenter/Widgets/CcPinChip.qml b/quickshell/Modules/ControlCenter/Widgets/CcPinChip.qml index 682bfa9b5..4f87d4b1a 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcPinChip.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcPinChip.qml @@ -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 diff --git a/quickshell/Modules/ControlCenter/Widgets/CcTile.qml b/quickshell/Modules/ControlCenter/Widgets/CcTile.qml index 1308157ec..766b32fb9 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcTile.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcTile.qml @@ -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 diff --git a/quickshell/Modules/ControlCenter/Widgets/UserTile.qml b/quickshell/Modules/ControlCenter/Widgets/UserTile.qml index c69992422..0487c5544 100644 --- a/quickshell/Modules/ControlCenter/Widgets/UserTile.qml +++ b/quickshell/Modules/ControlCenter/Widgets/UserTile.qml @@ -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 diff --git a/quickshell/Modules/DBar/BarMetrics.qml b/quickshell/Modules/DBar/BarMetrics.qml index 2917996f6..a8892c34e 100644 --- a/quickshell/Modules/DBar/BarMetrics.qml +++ b/quickshell/Modules/DBar/BarMetrics.qml @@ -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))); } diff --git a/quickshell/Modules/DBar/Widgets/Media.qml b/quickshell/Modules/DBar/Widgets/Media.qml index f912ec25b..dd3e7f795 100644 --- a/quickshell/Modules/DBar/Widgets/Media.qml +++ b/quickshell/Modules/DBar/Widgets/Media.qml @@ -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 diff --git a/quickshell/Modules/Lock/Widgets/LockPowerWidget.qml b/quickshell/Modules/Lock/Widgets/LockPowerWidget.qml index c4d240952..b06746ace 100644 --- a/quickshell/Modules/Lock/Widgets/LockPowerWidget.qml +++ b/quickshell/Modules/Lock/Widgets/LockPowerWidget.qml @@ -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() } diff --git a/quickshell/Modules/Settings/ClipboardTab.qml b/quickshell/Modules/Settings/ClipboardTab.qml index 38c963b8b..3e267d4e2 100644 --- a/quickshell/Modules/Settings/ClipboardTab.qml +++ b/quickshell/Modules/Settings/ClipboardTab.qml @@ -521,7 +521,6 @@ Item { minButtonWidth: 56 buttonPadding: Theme.spacingS textSize: Theme.fontSizeSmall - spacing: Theme.groupedListGap onSelectionChanged: (index, selected) => root.setVisibleEntryAction(index, selected) } } diff --git a/quickshell/Modules/Settings/NetworkWifiTab.qml b/quickshell/Modules/Settings/NetworkWifiTab.qml index 5656573e0..f33260939 100644 --- a/quickshell/Modules/Settings/NetworkWifiTab.qml +++ b/quickshell/Modules/Settings/NetworkWifiTab.qml @@ -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 diff --git a/quickshell/Modules/Settings/PrinterTab.qml b/quickshell/Modules/Settings/PrinterTab.qml index d181947da..22277ae7b 100644 --- a/quickshell/Modules/Settings/PrinterTab.qml +++ b/quickshell/Modules/Settings/PrinterTab.qml @@ -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" diff --git a/quickshell/Modules/Settings/SoftwareUpdatesTab.qml b/quickshell/Modules/Settings/SoftwareUpdatesTab.qml index a449ca7e7..0a255ea81 100644 --- a/quickshell/Modules/Settings/SoftwareUpdatesTab.qml +++ b/quickshell/Modules/Settings/SoftwareUpdatesTab.qml @@ -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) : [] diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index beadcac53..1b11d8e75 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -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) diff --git a/quickshell/Modules/Settings/Widgets/IslandHomeLayoutEditor.qml b/quickshell/Modules/Settings/Widgets/IslandHomeLayoutEditor.qml index 1e3612fcd..69bac85e4 100644 --- a/quickshell/Modules/Settings/Widgets/IslandHomeLayoutEditor.qml +++ b/quickshell/Modules/Settings/Widgets/IslandHomeLayoutEditor.qml @@ -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 diff --git a/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml b/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml index 1a901b239..70e50a219 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml @@ -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) => { diff --git a/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml b/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml index a044383ca..d8b2d6397 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml @@ -22,7 +22,7 @@ Column { signal hideOnLastDisplayToggled(bool checked) width: parent?.width ?? 0 - spacing: Theme.groupedListGap + spacing: SettingsMetrics.groupGap function screenPref(screen) { const pref = { diff --git a/quickshell/Modules/Settings/Widgets/SettingsGroup.qml b/quickshell/Modules/Settings/Widgets/SettingsGroup.qml index b6e7ceca2..ebe4ce76e 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsGroup.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsGroup.qml @@ -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 diff --git a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml index 65402174e..190b4e767 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml @@ -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 diff --git a/quickshell/Modules/Settings/Widgets/SettingsReorderList.qml b/quickshell/Modules/Settings/Widgets/SettingsReorderList.qml index 636ca01f7..ae1699f48 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsReorderList.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsReorderList.qml @@ -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) } diff --git a/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml b/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml index f899ff653..5c3a1aabf 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml @@ -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) { diff --git a/quickshell/Modules/Settings/Widgets/SettingsRow.qml b/quickshell/Modules/Settings/Widgets/SettingsRow.qml index ab2ecf33a..c306eaad3 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsRow.qml @@ -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 diff --git a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml index 13d13ef7a..faea91c6b 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml @@ -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 { diff --git a/quickshell/Modules/Settings/Widgets/SettingsTimeRow.qml b/quickshell/Modules/Settings/Widgets/SettingsTimeRow.qml index e4db98c41..958d8cbd0 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsTimeRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsTimeRow.qml @@ -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 diff --git a/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml b/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml index 97bb0e66e..409cc7d00 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml @@ -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 diff --git a/quickshell/Modules/Settings/Widgets/SettingsToggleSliderRow.qml b/quickshell/Modules/Settings/Widgets/SettingsToggleSliderRow.qml index c65585e4c..b8241a4fb 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsToggleSliderRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsToggleSliderRow.qml @@ -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 diff --git a/quickshell/PLUGINS/THEME_REFERENCE.md b/quickshell/PLUGINS/THEME_REFERENCE.md index f4c1f86c1..1cbdb3476 100644 --- a/quickshell/PLUGINS/THEME_REFERENCE.md +++ b/quickshell/PLUGINS/THEME_REFERENCE.md @@ -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 diff --git a/quickshell/Widgets/FeatureCard.qml b/quickshell/Widgets/FeatureCard.qml index c9150dade..8a7c6ba03 100644 --- a/quickshell/Widgets/FeatureCard.qml +++ b/quickshell/Widgets/FeatureCard.qml @@ -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 diff --git a/quickshell/tests/admin-policy.test.mjs b/quickshell/tests/admin-policy.test.mjs index e10d23d75..174793eb8 100644 --- a/quickshell/tests/admin-policy.test.mjs +++ b/quickshell/tests/admin-policy.test.mjs @@ -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 = []; diff --git a/quickshell/tests/density-presets.test.mjs b/quickshell/tests/density-presets.test.mjs index fa5d00452..8470b4537 100644 --- a/quickshell/tests/density-presets.test.mjs +++ b/quickshell/tests/density-presets.test.mjs @@ -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]) }, diff --git a/quickshell/tests/interface-style.test.mjs b/quickshell/tests/interface-style.test.mjs new file mode 100644 index 000000000..b59ceac47 --- /dev/null +++ b/quickshell/tests/interface-style.test.mjs @@ -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); +}); diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index b8f01cad8..429994b87 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -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",