From c9032926d72c6d78199bd99f902e4df2cf8b5edd Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 21 Sep 2026 16:47:09 -0400 Subject: [PATCH] settings: load all pages asynchronously --- dank-qml-common | 2 +- flake.lock | 6 +- quickshell/Modals/PowerProfileModal.qml | 1 + .../Modals/Settings/SettingsContent.qml | 80 ++++++++++++++++--- .../Modules/Settings/ColorDropdownRow.qml | 7 +- quickshell/Modules/Settings/ShadowsTab.qml | 7 +- .../Modules/Settings/WallpaperColorsTab.qml | 14 +++- .../Settings/Widgets/SettingsColorPicker.qml | 7 +- .../Settings/Widgets/SettingsMetrics.qml | 4 + .../Modules/Settings/Widgets/SettingsRow.qml | 20 +++-- .../Settings/Widgets/SettingsSidebarItem.qml | 1 + .../Widgets/SettingsWallpaperThumb.qml | 1 + quickshell/tests/qml/settings-scroll.qml | 6 +- 13 files changed, 116 insertions(+), 40 deletions(-) diff --git a/dank-qml-common b/dank-qml-common index 1a531ca78..c30ef9f1a 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 1a531ca78180f1fdedcdcf1696e3f662f47d325f +Subproject commit c30ef9f1abd09bae6b2e70939104b59b659187db diff --git a/flake.lock b/flake.lock index 48882b7d3..5fea12231 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1790019586, - "narHash": "sha256-ChWD8MBgZLeNMQu0xjlPF3Lpz1Rw5rLGX0X7CfPHjns=", + "lastModified": 1790023369, + "narHash": "sha256-BwL+cY6QjWTjKZ13feTjRpkPg4J4B0YGcl3gMPBK6pI=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "1a531ca78180f1fdedcdcf1696e3f662f47d325f", + "rev": "c30ef9f1abd09bae6b2e70939104b59b659187db", "type": "github" }, "original": { diff --git a/quickshell/Modals/PowerProfileModal.qml b/quickshell/Modals/PowerProfileModal.qml index 7051f837a..b7a215e22 100644 --- a/quickshell/Modals/PowerProfileModal.qml +++ b/quickshell/Modals/PowerProfileModal.qml @@ -228,6 +228,7 @@ DankModal { name: Theme.getPowerProfileIcon(modelData) size: Theme.iconSize + 16 color: isActive ? Theme.primary : Theme.surfaceText + filled: isActive anchors.horizontalCenter: parent.horizontalCenter } diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index b624ecbd5..890d35f85 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -115,25 +115,59 @@ FocusScope { Component { id: pageComponent - Loader { + // StackView writes visible and opacity on the element it manages, so the presentation gate lives on the inner Loader + Item { id: host required property string page readonly property bool pageActive: root.sessionVisible && pageStack.currentItem === host + readonly property alias item: loader.item + readonly property int status: loader.status + + property bool presented: false enabled: pageActive - asynchronous: page === "dankbar_widgets" || page === "window_rules" - Component.onCompleted: { - const file = root._fileFor(page); - if (file) - setSource(Qt.resolvedUrl("../../Modules/Settings/" + file), root._propertiesFor(page)); + Loader { + id: loader + + anchors.fill: parent + asynchronous: true + opacity: host.presented ? 1 : 0 + + Component.onCompleted: { + const file = root._fileFor(host.page); + if (file) + setSource(Qt.resolvedUrl("../../Modules/Settings/" + file), root._propertiesFor(host.page)); + } + onLoaded: { + if (item.pageActive !== undefined) + item.pageActive = Qt.binding(() => host.pageActive); + root._focusPage(); + } } - onLoaded: { - if (item.pageActive !== undefined) - item.pageActive = Qt.binding(() => host.pageActive); - root._focusPage(); + + FrameAnimation { + id: settleWatch + + property real lastHeight: -1 + property int stableFrames: 0 + + running: loader.status === Loader.Ready && !host.presented + onTriggered: { + const h = loader.item?.contentHeight ?? loader.item?.height ?? 0; + if (h === lastHeight) + stableFrames++; + else { + lastHeight = h; + stableFrames = 0; + } + if (stableFrames < SettingsMetrics.pageSettleFrames && elapsedTime * 1000 < SettingsMetrics.pageSettleDeadline) + return; + host.presented = true; + } } + onPageActiveChanged: { if (pageActive) root._focusPage(); @@ -216,9 +250,13 @@ FocusScope { Item { id: backSlot + + readonly property real glyphInset: (Theme.iconButtonSize - Theme.iconSize) / 2 + anchors.left: parent.left + anchors.leftMargin: root.showBack ? -glyphInset : 0 anchors.verticalCenter: parent.verticalCenter - width: root.showBack ? Theme.iconButtonSize + Theme.spacingL : 0 + width: root.showBack ? Theme.iconButtonSize + Theme.spacingM - glyphInset : 0 height: Theme.iconButtonSize DankActionButton { @@ -273,8 +311,26 @@ FocusScope { } DankSpinner { + id: pageSpinner + + readonly property bool loading: pageStack.currentItem?.presented === false + anchors.centerIn: parent - visible: pageStack.currentItem?.status === Loader.Loading + visible: false + onLoadingChanged: { + if (!loading) { + spinnerDelay.stop(); + visible = false; + return; + } + spinnerDelay.restart(); + } + + Timer { + id: spinnerDelay + interval: SettingsMetrics.pageSettleDeadline + SettingsMetrics.fadeDuration + onTriggered: pageSpinner.visible = pageSpinner.loading + } } } } diff --git a/quickshell/Modules/Settings/ColorDropdownRow.qml b/quickshell/Modules/Settings/ColorDropdownRow.qml index 9fe89cae9..287689bb1 100644 --- a/quickshell/Modules/Settings/ColorDropdownRow.qml +++ b/quickshell/Modules/Settings/ColorDropdownRow.qml @@ -137,13 +137,10 @@ Column { anchors.rightMargin: SettingsMetrics.rowPaddingH spacing: Theme.spacingM - Rectangle { + DankColorSwatch { width: Theme.avatarSize height: Theme.avatarSize - radius: width / 2 - color: root.customColor - border.color: Theme.outline - border.width: Theme.outlineWidth + swatchColor: root.customColor anchors.verticalCenter: parent.verticalCenter DankIcon { diff --git a/quickshell/Modules/Settings/ShadowsTab.qml b/quickshell/Modules/Settings/ShadowsTab.qml index 04f9c72c8..7fc49db31 100644 --- a/quickshell/Modules/Settings/ShadowsTab.qml +++ b/quickshell/Modules/Settings/ShadowsTab.qml @@ -119,13 +119,10 @@ Item { clickable: true onClicked: root.openShadowColorPicker() - Rectangle { + DankColorSwatch { width: Theme.iconSizeMedium height: width - radius: width / 2 - color: SettingsData.m3ElevationCustomColor ?? "#000000" - border.color: Theme.outline - border.width: Theme.outlineWidth + swatchColor: SettingsData.m3ElevationCustomColor ?? "#000000" anchors.verticalCenter: parent.verticalCenter } } diff --git a/quickshell/Modules/Settings/WallpaperColorsTab.qml b/quickshell/Modules/Settings/WallpaperColorsTab.qml index db62531ee..cfa477ee2 100644 --- a/quickshell/Modules/Settings/WallpaperColorsTab.qml +++ b/quickshell/Modules/Settings/WallpaperColorsTab.qml @@ -182,6 +182,8 @@ Column { SettingsRow { settingKey: "colorMode" tags: ["light", "dark", "mode", "theme", "color", "palette"] + paddingH: SettingsMetrics.heroPadding + paddingV: SettingsMetrics.heroPadding body: Flow { id: hero @@ -192,8 +194,8 @@ Column { spacing: Theme.spacingL SettingsWallpaperThumb { + id: thumb width: hero.thumbWidth - height: hero.stacked ? width * SettingsMetrics.wallpaperThumbRatio : side.implicitHeight path: root.currentWallpaper onBrowse: root.openBrowser() onPickColor: root.pickColor() @@ -202,10 +204,14 @@ Column { Column { id: side + + readonly property real navRowHeight: (thumb.height - modeGroup.height - spacing - navGroup.spacing) / 2 + width: hero.stacked ? hero.width : hero.width - hero.thumbWidth - hero.spacing spacing: Theme.spacingM DankButtonGroup { + id: modeGroup width: parent.width fillWidth: true checkEnabled: false @@ -233,6 +239,7 @@ Column { } SettingsGroup { + id: navGroup width: parent.width slotColor: Theme.foregroundColor(Theme.chipSurface, true) @@ -243,6 +250,8 @@ Column { title: I18n.tr("Dark mode") hint: root.colorModeStatus iconName: "schedule" + iconBox: true + height: hero.stacked ? implicitHeight : Math.max(implicitHeight, side.navRowHeight) paddingH: Theme.spacingM paddingV: Theme.spacingM onClicked: root.parentModal?.navigateTo("theme_schedule") @@ -254,12 +263,13 @@ Column { settingKey: "themeNav" title: I18n.tr("Theme & colors") hint: Theme.currentThemeLabel + height: hero.stacked ? implicitHeight : Math.max(implicitHeight, side.navRowHeight) paddingH: Theme.spacingM paddingV: Theme.spacingM onClicked: root.parentModal?.navigateTo("theme") leading: DankPaletteSwatch { - width: Theme.iconSize + width: SettingsMetrics.heroLeadingSize height: width primaryColor: root.themePalette.primary secondaryColor: root.themePalette.secondary diff --git a/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml b/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml index f85874de9..887d04f8a 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml @@ -69,13 +69,10 @@ Item { anchors.centerIn: parent spacing: Theme.spacingXS - Rectangle { + DankColorSwatch { width: Theme.iconSize height: Theme.iconSize - radius: Theme.fullRadius(width, height) - color: modelData.color - border.color: Theme.outline - border.width: Theme.outlineWidth + swatchColor: modelData.color anchors.horizontalCenter: parent.horizontalCenter DankIcon { diff --git a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml index 680acc4f7..5efc1c992 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml @@ -19,6 +19,8 @@ Singleton { readonly property real rowPaddingH: 20 readonly property real rowPaddingV: 16 readonly property real rowContentSpacing: Theme.spacingL + readonly property real heroPadding: rowPaddingV + readonly property real heroLeadingSize: Theme.avatarSize readonly property real sectionLabelTopGap: Theme.spacingS readonly property real sectionLabelBottomGap: Theme.spacingM readonly property real navIconSize: Theme.avatarSize @@ -42,4 +44,6 @@ Singleton { readonly property color selectedRowColor: Theme.blend(rowColor, Theme.onSurface, Theme.stateLayerDrag) readonly property int transitionDuration: Theme.expressiveDurations.expressiveFastSpatial readonly property int fadeDuration: Theme.expressiveDurations.expressiveEffects + readonly property int pageSettleFrames: 2 + readonly property int pageSettleDeadline: 250 } diff --git a/quickshell/Modules/Settings/Widgets/SettingsRow.qml b/quickshell/Modules/Settings/Widgets/SettingsRow.qml index 3868c690b..b793a2f6e 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsRow.qml @@ -24,6 +24,7 @@ T.Control { property string subtitle: "" property color subtitleColor: Theme.surfaceVariantText property string iconName: "" + property bool iconBox: false property bool clickable: false property bool showChevron: false property string trailingBadge: "" @@ -198,8 +199,7 @@ T.Control { id: mainColumn anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: root.paddingV + anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: root.paddingH anchors.rightMargin: root.paddingH spacing: Theme.spacingM @@ -226,13 +226,21 @@ T.Control { visible: children.length > 0 } - DankIcon { + Rectangle { id: leadingIcon anchors.verticalCenter: parent.verticalCenter - name: root.iconName - size: Theme.iconSize - color: root.iconColor + width: root.iconBox ? SettingsMetrics.heroLeadingSize : Theme.iconSize + height: width + radius: Theme.fullRadius(width, height) + color: root.iconBox ? Theme.withAlpha(root.iconColor, Theme.tonalTintAlpha) : "transparent" visible: root.iconName !== "" + + DankIcon { + anchors.centerIn: parent + name: root.iconName + size: root.iconBox ? Theme.iconSizeMedium : Theme.iconSize + color: root.iconColor + } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml index a1c9be6d5..77db9dfec 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml @@ -108,6 +108,7 @@ Rectangle { name: root.iconName size: Theme.iconSizeMedium color: root.glyphColor + filled: root.active } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml b/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml index de9a4bc90..5c236c54f 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml @@ -36,6 +36,7 @@ Rectangle { sourceComponent: CachingImage { imagePath: root.path maxCacheSize: SettingsMetrics.wallpaperThumbCache + asynchronous: false animate: false } } diff --git a/quickshell/tests/qml/settings-scroll.qml b/quickshell/tests/qml/settings-scroll.qml index 71cf4324a..7362faeea 100644 --- a/quickshell/tests/qml/settings-scroll.qml +++ b/quickshell/tests/qml/settings-scroll.qml @@ -73,8 +73,12 @@ ShellRoot { function open(page) { root.content.currentPage = page; + try { + tryVerify(() => scrollable() !== null, 5000); + } catch (error) { + throw new Error(page + " has a scrollable page"); + } settle(); - check(scrollable() !== null, page + " has a scrollable page"); } function expectPosition(y, label) {