From 4eb163538c18587fbd62e045c2aaf05f282b104a Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 22 Sep 2026 09:00:34 -0400 Subject: [PATCH] media: change player style theme: restore layer outline option launcher/theme: tweaks and fix icon service --- core/go.mod | 2 +- core/go.sum | 4 +- dank-qml-common | 2 +- flake.lock | 6 +- flake.nix | 2 +- quickshell/Common/Theme.qml | 2 +- quickshell/Common/settings/SettingsSpec.js | 2 +- quickshell/DMSShell.qml | 1 - quickshell/Modals/AppPickerModal.qml | 2 +- quickshell/Modals/BluetoothPairingModal.qml | 2 + .../Modals/Changelog/ChangelogFeatureCard.qml | 2 + .../Modals/Changelog/ChangelogModal.qml | 2 + .../Modals/Clipboard/ClipboardEditor.qml | 2 +- .../Clipboard/ClipboardImagePreview.qml | 4 +- .../Modals/Common/ConfirmDialogOverlay.qml | 4 +- .../Modals/DankLauncherV2/ActionPanel.qml | 2 + .../ClipboardLauncherPreview.qml | 4 +- .../DankLauncherV2/Components/LauncherRow.qml | 7 - .../Components/LauncherTile.qml | 12 +- .../Modals/DankLauncherV2/Controller.qml | 12 +- .../Modals/DankLauncherV2/ControllerUtils.js | 165 +++++----- .../Modals/DankLauncherV2/LauncherContent.qml | 9 +- .../Modals/DankLauncherV2/ResultsList.qml | 4 + quickshell/Modals/DankLauncherV2/Scorer.js | 215 +++++++------ quickshell/Modals/DankLauncherV2/TileItem.qml | 2 + .../Modals/DisplayConfirmationModal.qml | 2 + .../Modals/Greeter/GreeterCompletePage.qml | 4 + .../Modals/Greeter/GreeterDoctorPage.qml | 2 + .../Modals/Greeter/GreeterFeatureCard.qml | 2 + quickshell/Modals/Greeter/GreeterModal.qml | 4 + .../Modals/Greeter/GreeterQuickLink.qml | 2 + .../Modals/Greeter/GreeterSettingsCard.qml | 2 + .../Modals/Greeter/GreeterStatusCard.qml | 4 +- quickshell/Modals/MuxModal.qml | 4 + quickshell/Modals/NetworkInfoModal.qml | 4 +- quickshell/Modals/PowerProfileModal.qml | 4 +- quickshell/Modals/Settings/SettingsModal.qml | 2 + quickshell/Modals/SwitchUserModal.qml | 2 + .../Components/WidgetConfigOverlay.qml | 2 + .../Modules/ControlCenter/Widgets/CcMenu.qml | 2 + .../ControlCenter/Widgets/CcSheetDialog.qml | 2 + .../ControlCenter/Widgets/CcSliderRow.qml | 4 + .../Modules/ControlCenter/Widgets/CcTile.qml | 4 + .../ControlCenter/Widgets/CcWidgetLibrary.qml | 2 + quickshell/Modules/DankBar/Widgets/Media.qml | 1 + quickshell/Modules/DankDash/DashMetrics.qml | 4 + quickshell/Modules/DankDash/DashRegistry.qml | 8 + .../Modules/DankDash/Media/LyricsOverlay.qml | 4 +- .../DankDash/Media/MediaArtSurface.qml | 73 +++++ .../Modules/DankDash/Media/MediaArtView.qml | 9 +- .../DankDash/Media/MediaChromeBase.qml | 2 + .../DankDash/Media/MediaSourceGroup.qml | 7 +- .../Modules/DankDash/Media/MediaTransport.qml | 27 +- .../DankDash/Media/MediaTransportButton.qml | 8 +- .../Modules/DankDash/Media/ZurvanChrome.qml | 96 +++--- .../DankDash/Overview/MediaOverviewCard.qml | 17 +- quickshell/Modules/DankDash/WallpaperTab.qml | 4 + .../DankDash/Weather/WeatherForecastChart.qml | 2 + .../DankDash/Weather/WeatherMetricCard.qml | 2 + .../Modules/DankDash/Weather/WeatherSky.qml | 2 + quickshell/Modules/Greetd/GreeterContent.qml | 4 +- quickshell/Modules/Lock/LockScreenContent.qml | 4 +- .../Modules/Notepad/NotepadSettings.qml | 2 +- .../Modules/Notepad/NotepadTextEditor.qml | 8 +- .../Notifications/NotificationCard.qml | 2 + quickshell/Modules/OSD/MediaPlaybackOSD.qml | 2 + quickshell/Modules/ProcessList/DisksView.qml | 4 + .../Modules/ProcessList/PerformanceCard.qml | 2 + .../ProcessList/ProcessContextMenu.qml | 2 + quickshell/Modules/ProcessList/SystemView.qml | 4 + quickshell/Modules/Settings/DankBarTab.qml | 36 +-- .../Modules/Settings/DesktopWidgetBrowser.qml | 4 +- .../DisplayConfig/ICCProfileInfoModal.qml | 4 +- quickshell/Modules/Settings/DockHubHeader.qml | 2 +- .../Modules/Settings/NetworkCellularTab.qml | 8 +- .../Modules/Settings/NetworkEthernetTab.qml | 8 +- quickshell/Modules/Settings/NetworkVpnTab.qml | 4 +- quickshell/Modules/Settings/PluginBrowser.qml | 4 +- quickshell/Modules/Settings/PluginCard.qml | 4 +- .../Modules/Settings/PluginUpdatesDialog.qml | 4 +- .../Modules/Settings/ThemeColorsTab.qml | 6 +- .../Modules/Settings/ThemeSurfacesTab.qml | 3 +- .../Settings/Widgets/SettingsGroup.qml | 2 + .../Settings/Widgets/SettingsLayoutPicker.qml | 39 ++- .../Modules/Settings/Widgets/SettingsRow.qml | 2 + .../Settings/Widgets/SettingsSidebarItem.qml | 2 + .../Widgets/SettingsSurfacePreview.qml | 2 + .../Settings/Widgets/SettingsSwatchGrid.qml | 2 +- .../Settings/Widgets/SettingsToggleCard.qml | 2 + quickshell/Services/IconThemeIndex.js | 65 ++++ quickshell/Services/IconThemeService.qml | 299 ++++++++---------- quickshell/shell.qml | 4 +- quickshell/tests/icon-theme-index.test.mjs | 60 ++++ scripts/format-staged.py | 14 + 94 files changed, 866 insertions(+), 539 deletions(-) create mode 100644 quickshell/Modules/DankDash/Media/MediaArtSurface.qml create mode 100644 quickshell/Services/IconThemeIndex.js create mode 100644 quickshell/tests/icon-theme-index.test.mjs diff --git a/core/go.mod b/core/go.mod index eb849640d..a58568d4c 100644 --- a/core/go.mod +++ b/core/go.mod @@ -83,7 +83,7 @@ require ( ) require ( - github.com/AvengeMedia/dankgo v1.6.3-0.20260918210430-d03946da44cd + github.com/AvengeMedia/dankgo v1.6.3-0.20260922103155-c6cc9c3c32ba github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect diff --git a/core/go.sum b/core/go.sum index 6d16d7869..71228e441 100644 --- a/core/go.sum +++ b/core/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/AvengeMedia/dankgo v1.6.3-0.20260918210430-d03946da44cd h1:8Pa1/uo973yP2TkjBeBEdVY20hwlCfwQMq5D3bFOf7U= -github.com/AvengeMedia/dankgo v1.6.3-0.20260918210430-d03946da44cd/go.mod h1:H6yEdNZ+JJHpBSpvAEJVfXWKhIrttydVU3s2JLyTvQI= +github.com/AvengeMedia/dankgo v1.6.3-0.20260922103155-c6cc9c3c32ba h1:Basmkwiyh8ZAOXpweOw/lu6mEV4mmZl0vK+WPhZPSqA= +github.com/AvengeMedia/dankgo v1.6.3-0.20260922103155-c6cc9c3c32ba/go.mod h1:H6yEdNZ+JJHpBSpvAEJVfXWKhIrttydVU3s2JLyTvQI= github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9 h1:ssoiAng7ng500BgtZa1riJH6kKudQVM8Uk9a9kF7mDA= github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9/go.mod h1:8TFIP72NLxCbogF2hHU6sXj3RoTbHQW8beeCrisHs/Y= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/dank-qml-common b/dank-qml-common index 33558beb0..c9c29deeb 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 33558beb0630a4bea6718cfcf52bf875c63abc55 +Subproject commit c9c29deeb3b6436583d20d23d3b210c5522e5292 diff --git a/flake.lock b/flake.lock index 5cc99d84d..5dcdafc38 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1790026440, - "narHash": "sha256-oNfBLukk6ppC6IvFksPdh17OeNOUEp8WBLduOkIZAas=", + "lastModified": 1790081738, + "narHash": "sha256-wPe6BGzBoQaH9YtKG+TXgNGxBjgDj+2Qrozg75qLDpc=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "33558beb0630a4bea6718cfcf52bf875c63abc55", + "rev": "c9c29deeb3b6436583d20d23d3b210c5522e5292", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index a77b01de6..5d3791b58 100644 --- a/flake.nix +++ b/flake.nix @@ -111,7 +111,7 @@ inherit version; pname = "dms-shell"; src = ./core; - vendorHash = "sha256-eZBP7+9NLdJ88qizCZ+mTmcQO+FHthqQSmNtUErfKv4="; + vendorHash = "sha256-sWvntuwG8BkVykveWdda5aTiXggB7uhf57ufReb4PWk="; subPackages = [ "cmd/dms" ]; diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index cf39d62c8..b473ee9b8 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -587,7 +587,7 @@ Singleton { readonly property color notificationFloatingSurface: notificationForegroundLayers ? readableSurface : withAlpha(readableSurface, 0) readonly property color notificationFloatingSurfaceHigh: notificationForegroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0) readonly property color notificationNestedSurface: notificationFloatingSurfaceHigh - readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12))) + readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0 : (SettingsData.blurLayerOutlineOpacity ?? 0))) readonly property real layerOutlineOpacity: blurLayerOutlineOpacity readonly property int layerOutlineWidth: layerOutlineOpacity > 0 ? 1 : 0 readonly property real floatingWindowFieldAlpha: floatingWindowForegroundAlpha diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 103cb98c4..c460aeb7a 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -392,7 +392,7 @@ var LOCAL_SPEC = { coerce: Util.percentToUnit }, blurLayerOutlineOpacity: { - def: 0.12, + def: 0, coerce: Util.percentToUnit }, focusRingEnabled: { diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 0db35fd65..6a13a1f5e 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -253,7 +253,6 @@ Item { PolkitService.polkitAvailable; DisplayConfigState.hasOutputBackend; PortalService.systemColorScheme; - IconThemeService.revision; DesktopService.isSystemd; TrashService.count; WallpaperCyclingService.cyclingActive; diff --git a/quickshell/Modals/AppPickerModal.qml b/quickshell/Modals/AppPickerModal.qml index cd89cd56b..e206a6902 100644 --- a/quickshell/Modals/AppPickerModal.qml +++ b/quickshell/Modals/AppPickerModal.qml @@ -532,7 +532,7 @@ DankModal { radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface border.color: Theme.outlineMedium - border.width: 1 + border.width: Theme.layerOutlineWidth visible: root.showTargetData && root.targetData.length > 0 StyledText { diff --git a/quickshell/Modals/BluetoothPairingModal.qml b/quickshell/Modals/BluetoothPairingModal.qml index 9d72ec616..0e8dd3e1f 100644 --- a/quickshell/Modals/BluetoothPairingModal.qml +++ b/quickshell/Modals/BluetoothPairingModal.qml @@ -220,6 +220,8 @@ DankModal { height: 56 radius: Theme.cornerRadius color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium visible: requestType === "confirm" || requestType === "display-passkey" Column { diff --git a/quickshell/Modals/Changelog/ChangelogFeatureCard.qml b/quickshell/Modals/Changelog/ChangelogFeatureCard.qml index 5891ae78b..ffa3db5d6 100644 --- a/quickshell/Modals/Changelog/ChangelogFeatureCard.qml +++ b/quickshell/Modals/Changelog/ChangelogFeatureCard.qml @@ -16,6 +16,8 @@ Rectangle { height: Math.round(Theme.fontSizeMedium * 4.2) radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.fill: parent diff --git a/quickshell/Modals/Changelog/ChangelogModal.qml b/quickshell/Modals/Changelog/ChangelogModal.qml index e3bb1af2f..376c840ea 100644 --- a/quickshell/Modals/Changelog/ChangelogModal.qml +++ b/quickshell/Modals/Changelog/ChangelogModal.qml @@ -76,6 +76,8 @@ DankFloatingWindow { anchors.bottom: parent.bottom height: Math.round(Theme.fontSizeMedium * 4.5) color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.top: parent.top diff --git a/quickshell/Modals/Clipboard/ClipboardEditor.qml b/quickshell/Modals/Clipboard/ClipboardEditor.qml index 3c06494e5..555164062 100644 --- a/quickshell/Modals/Clipboard/ClipboardEditor.qml +++ b/quickshell/Modals/Clipboard/ClipboardEditor.qml @@ -377,7 +377,7 @@ Item { radius: Theme.windowRadius color: Theme.floatingWindowNestedSurface border.color: Theme.outlineMedium - border.width: 1 + border.width: Theme.layerOutlineWidth } contentItem: Column { diff --git a/quickshell/Modals/Clipboard/ClipboardImagePreview.qml b/quickshell/Modals/Clipboard/ClipboardImagePreview.qml index 8c370ddc8..06f07ae1d 100644 --- a/quickshell/Modals/Clipboard/ClipboardImagePreview.qml +++ b/quickshell/Modals/Clipboard/ClipboardImagePreview.qml @@ -83,8 +83,8 @@ ClippingRectangle { anchors.margins: Theme.spacingL radius: Theme.cornerRadiusL color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineVariant - border.width: Theme.outlineWidth + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth MouseArea { anchors.fill: parent diff --git a/quickshell/Modals/Common/ConfirmDialogOverlay.qml b/quickshell/Modals/Common/ConfirmDialogOverlay.qml index dca558505..86af86373 100644 --- a/quickshell/Modals/Common/ConfirmDialogOverlay.qml +++ b/quickshell/Modals/Common/ConfirmDialogOverlay.qml @@ -108,8 +108,8 @@ Item { anchors.centerIn: parent radius: Theme.windowRadius color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) - border.color: Theme.outlineVariant - border.width: Theme.outlineWidth + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth MouseArea { anchors.fill: parent diff --git a/quickshell/Modals/DankLauncherV2/ActionPanel.qml b/quickshell/Modals/DankLauncherV2/ActionPanel.qml index 8eb54a1dd..cfe1fdf82 100644 --- a/quickshell/Modals/DankLauncherV2/ActionPanel.qml +++ b/quickshell/Modals/DankLauncherV2/ActionPanel.qml @@ -121,6 +121,8 @@ Rectangle { width: parent?.width ?? Theme.fieldDefaultWidth height: expanded && hasActions ? Theme.listItemHeight : 0 color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.cornerRadius clip: true diff --git a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml index 9f2ee2d98..f84d989d7 100644 --- a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml +++ b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml @@ -19,8 +19,8 @@ Rectangle { radius: Theme.cornerRadiusM clip: true color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) - border.color: Theme.outlineVariant - border.width: Theme.outlineWidth + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth onEntryChanged: reloadPreview() Component.onCompleted: reloadPreview() diff --git a/quickshell/Modals/DankLauncherV2/Components/LauncherRow.qml b/quickshell/Modals/DankLauncherV2/Components/LauncherRow.qml index 0088327bb..79d8587d8 100644 --- a/quickshell/Modals/DankLauncherV2/Components/LauncherRow.qml +++ b/quickshell/Modals/DankLauncherV2/Components/LauncherRow.qml @@ -162,13 +162,6 @@ DankListItem { anchors.verticalCenter: parent.verticalCenter } - DankKeycap { - text: "↵" - textColor: root.contentColor - visible: root.isSelected - anchors.verticalCenter: parent.verticalCenter - } - Image { width: LauncherMetrics.previewWidth height: LauncherMetrics.previewHeight diff --git a/quickshell/Modals/DankLauncherV2/Components/LauncherTile.qml b/quickshell/Modals/DankLauncherV2/Components/LauncherTile.qml index 228a24ee6..e2ed935d0 100644 --- a/quickshell/Modals/DankLauncherV2/Components/LauncherTile.qml +++ b/quickshell/Modals/DankLauncherV2/Components/LauncherTile.qml @@ -54,6 +54,8 @@ Rectangle { radius: Theme.cornerRadiusL color: externalHighlight ? "transparent" : isSelected ? Theme.selectedContainer : Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: externalHighlight ? 0 : Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.fill: parent @@ -89,16 +91,6 @@ Rectangle { } } - DankKeycap { - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: Theme.spacingXS - text: "↵" - textColor: root.contentColor - visible: root.isSelected - z: 1 - } - Loader { anchors.fill: parent active: !root.imageTile diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index f98ddc5e0..81d769cf6 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -1329,7 +1329,9 @@ Item { newSections = existingNonFile.concat(fileSections); } newSections.sort(function (a, b) { - return a.priority - b.priority; + if (a.priority !== b.priority) + return a.priority - b.priority; + return a.id.localeCompare(b.id); }); _applyHighlights(newSections, searchQuery); flatModel = Scorer.flattenSections(newSections); @@ -1713,7 +1715,9 @@ Item { } baseDefs.sort(function (a, b) { - return a.priority - b.priority; + if (a.priority !== b.priority) + return a.priority - b.priority; + return a.id.localeCompare(b.id); }); return baseDefs; } @@ -1930,7 +1934,9 @@ Item { return null; sectionsData.sort(function (a, b) { - return a.priority - b.priority; + if (a.priority !== b.priority) + return a.priority - b.priority; + return a.id.localeCompare(b.id); }); return sectionsData; } diff --git a/quickshell/Modals/DankLauncherV2/ControllerUtils.js b/quickshell/Modals/DankLauncherV2/ControllerUtils.js index b1ee37bd9..33790650e 100644 --- a/quickshell/Modals/DankLauncherV2/ControllerUtils.js +++ b/quickshell/Modals/DankLauncherV2/ControllerUtils.js @@ -4,72 +4,72 @@ function getFileIcon(filename) { var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : ""; switch (ext) { - case "pdf": - return "picture_as_pdf"; - case "doc": - case "docx": - case "odt": - return "description"; - case "xls": - case "xlsx": - case "ods": - return "table_chart"; - case "ppt": - case "pptx": - case "odp": - return "slideshow"; - case "txt": - case "md": - case "rst": - return "article"; - case "jpg": - case "jpeg": - case "png": - case "gif": - case "svg": - case "webp": - return "image"; - case "mp3": - case "wav": - case "flac": - case "ogg": - return "audio_file"; - case "mp4": - case "mkv": - case "avi": - case "webm": - return "video_file"; - case "zip": - case "tar": - case "gz": - case "7z": - case "rar": - return "folder_zip"; - case "js": - case "ts": - case "py": - case "rs": - case "go": - case "java": - case "c": - case "cpp": - case "h": - return "code"; - case "html": - case "css": - case "htm": - return "web"; - case "json": - case "xml": - case "yaml": - case "yml": - return "data_object"; - case "sh": - case "bash": - case "zsh": - return "terminal"; - default: - return "insert_drive_file"; + case "pdf": + return "picture_as_pdf"; + case "doc": + case "docx": + case "odt": + return "description"; + case "xls": + case "xlsx": + case "ods": + return "table_chart"; + case "ppt": + case "pptx": + case "odp": + return "slideshow"; + case "txt": + case "md": + case "rst": + return "article"; + case "jpg": + case "jpeg": + case "png": + case "gif": + case "svg": + case "webp": + return "image"; + case "mp3": + case "wav": + case "flac": + case "ogg": + return "audio_file"; + case "mp4": + case "mkv": + case "avi": + case "webm": + return "video_file"; + case "zip": + case "tar": + case "gz": + case "7z": + case "rar": + return "folder_zip"; + case "js": + case "ts": + case "py": + case "rs": + case "go": + case "java": + case "c": + case "cpp": + case "h": + return "code"; + case "html": + case "css": + case "htm": + return "web"; + case "json": + case "xml": + case "yaml": + case "yml": + return "data_object"; + case "sh": + case "bash": + case "zsh": + return "terminal"; + default: + return "insert_drive_file"; } } @@ -116,19 +116,13 @@ function classifyAppSource(app) { if (cmd0 === "flatpak" || exec.indexOf("flatpak run ") !== -1) return "flatpak"; - if (cmd0 === "snap" - || exec.indexOf("bamf_desktop_file_hint=") !== -1 - || exec.indexOf("/snap/bin/") !== -1 - || exec.indexOf("/snap/core") !== -1 - || exec.indexOf("snap run ") === 0) + if (cmd0 === "snap" || exec.indexOf("bamf_desktop_file_hint=") !== -1 || exec.indexOf("/snap/bin/") !== -1 || exec.indexOf("/snap/core") !== -1 || exec.indexOf("snap run ") === 0) return "snap"; if (/\.appimage(\s|$|")/i.test(execRaw) || id.indexOf("appimagekit_") === 0) return "appimage"; - if (exec.indexOf("/nix/store/") !== -1 - || exec.indexOf("/run/current-system/sw/") !== -1 - || exec.indexOf("/etc/profiles/per-user/") !== -1) + if (exec.indexOf("/nix/store/") !== -1 || exec.indexOf("/run/current-system/sw/") !== -1 || exec.indexOf("/etc/profiles/per-user/") !== -1) return "nix"; return "system"; @@ -174,7 +168,9 @@ function sortPluginIdsByOrder(pluginIds, order) { return pluginIds.slice().sort(function (a, b) { var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999; var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999; - return aOrder - bOrder; + if (aOrder !== bOrder) + return aOrder - bOrder; + return a.localeCompare(b); }); } @@ -187,7 +183,9 @@ function sortPluginsOrdered(plugins, order) { return plugins.sort(function (a, b) { var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999; var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999; - return aOrder - bOrder; + if (aOrder !== bOrder) + return aOrder - bOrder; + return a.id.localeCompare(b.id); }); } @@ -196,8 +194,17 @@ function parseFileSearchPrefix(query) { return null; var rest = query.substring(1); if (rest === "d" || rest.startsWith("d ") || rest.startsWith("d\t")) - return { type: "dir", query: rest.substring(1).trim() }; + return { + type: "dir", + query: rest.substring(1).trim() + }; if (rest === "f" || rest.startsWith("f ") || rest.startsWith("f\t")) - return { type: "file", query: rest.substring(1).trim() }; - return { type: null, query: rest.trim() }; + return { + type: "file", + query: rest.substring(1).trim() + }; + return { + type: null, + query: rest.trim() + }; } diff --git a/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/quickshell/Modals/DankLauncherV2/LauncherContent.qml index 4e48ba6ca..abb084bd5 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -330,6 +330,8 @@ FocusScope { visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) && !Theme.blurLayersActive color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.windowRadius } DankFilterChips { @@ -370,13 +372,6 @@ FocusScope { color: Theme.onSurfaceVariant } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: "↵ " + I18n.tr("Open") - font.pixelSize: Theme.fontSizeSmall - color: Theme.onSurfaceVariant - } - StyledText { anchors.verticalCenter: parent.verticalCenter text: I18n.tr("Tab", "keyboard tab key name", true) + " " + I18n.tr("Actions", "noun, launcher footer hint after the tab key, also section label") diff --git a/quickshell/Modals/DankLauncherV2/ResultsList.qml b/quickshell/Modals/DankLauncherV2/ResultsList.qml index b887815ea..16c1791ff 100644 --- a/quickshell/Modals/DankLauncherV2/ResultsList.qml +++ b/quickshell/Modals/DankLauncherV2/ResultsList.qml @@ -329,6 +329,8 @@ Item { visible: delegateRoot.visible opacity: delegateRoot.opacity color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.groupedListInnerRadius topLeftRadius: delegateRoot.modelData?.firstInGroup ? Theme.groupedListOuterRadius : radius topRightRadius: topLeftRadius @@ -417,6 +419,8 @@ Item { visible: delegateRoot.visible opacity: delegateRoot.opacity color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.cornerRadiusL } diff --git a/quickshell/Modals/DankLauncherV2/Scorer.js b/quickshell/Modals/DankLauncherV2/Scorer.js index 64f4c09c9..02e32c1e0 100644 --- a/quickshell/Modals/DankLauncherV2/Scorer.js +++ b/quickshell/Modals/DankLauncherV2/Scorer.js @@ -18,180 +18,195 @@ const Weights = { file: 800, action: 600 } -} +}; function tokenize(text) { - return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 }) + return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { + return w.length > 0; + }); } function hasWordBoundaryMatch(text, query) { - var textWords = tokenize(text) - var queryWords = tokenize(query) + var textWords = tokenize(text); + var queryWords = tokenize(query); - if (queryWords.length === 0) return false - if (queryWords.length > textWords.length) return false + if (queryWords.length === 0) + return false; + if (queryWords.length > textWords.length) + return false; for (var i = 0; i <= textWords.length - queryWords.length; i++) { - var allMatch = true + var allMatch = true; for (var j = 0; j < queryWords.length; j++) { if (!textWords[i + j].startsWith(queryWords[j])) { - allMatch = false - break + allMatch = false; + break; } } - if (allMatch) return true + if (allMatch) + return true; } - return false + return false; } function levenshteinDistance(s1, s2) { - var len1 = s1.length - var len2 = s2.length - var prev = new Array(len2 + 1) - var curr = new Array(len2 + 1) + var len1 = s1.length; + var len2 = s2.length; + var prev = new Array(len2 + 1); + var curr = new Array(len2 + 1); for (var j = 0; j <= len2; j++) - prev[j] = j + prev[j] = j; for (var i = 1; i <= len1; i++) { - curr[0] = i + curr[0] = i; for (var j = 1; j <= len2; j++) { - var cost = s1[i - 1] === s2[j - 1] ? 0 : 1 - curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost) + var cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); } - var tmp = prev - prev = curr - curr = tmp + var tmp = prev; + prev = curr; + curr = tmp; } - return prev[len2] + return prev[len2]; } function fuzzyScore(text, query) { - var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3 - var bestScore = 0 + var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3; + var bestScore = 0; if (Math.abs(text.length - query.length) <= maxDistance) { - var distance = levenshteinDistance(text, query) + var distance = levenshteinDistance(text, query); if (distance <= maxDistance) { - var maxLen = Math.max(text.length, query.length) - bestScore = 1 - (distance / maxLen) + var maxLen = Math.max(text.length, query.length); + bestScore = 1 - (distance / maxLen); } } - var words = tokenize(text) + var words = tokenize(text); for (var i = 0; i < words.length && bestScore < 0.8; i++) { - if (Math.abs(words[i].length - query.length) > maxDistance) continue - var wordDistance = levenshteinDistance(words[i], query) + if (Math.abs(words[i].length - query.length) > maxDistance) + continue; + var wordDistance = levenshteinDistance(words[i], query); if (wordDistance <= maxDistance) { - var wordMaxLen = Math.max(words[i].length, query.length) - var score = 1 - (wordDistance / wordMaxLen) - bestScore = Math.max(bestScore, score) + var wordMaxLen = Math.max(words[i].length, query.length); + var score = 1 - (wordDistance / wordMaxLen); + bestScore = Math.max(bestScore, score); } } - return bestScore + return bestScore; } function calculateTextScore(name, query) { - if (name === query) return Weights.exactMatch - if (name.startsWith(query)) return Weights.prefixMatch - if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary - if (name.includes(query)) return Weights.substring + if (name === query) + return Weights.exactMatch; + if (name.startsWith(query)) + return Weights.prefixMatch; + if (hasWordBoundaryMatch(name, query)) + return Weights.wordBoundary; + if (name.includes(query)) + return Weights.substring; if (query.length >= 3) { - var fs = fuzzyScore(name, query) - if (fs > 0) return fs * Weights.fuzzy + var fs = fuzzyScore(name, query); + if (fs > 0) + return fs * Weights.fuzzy; } - return 0 + return 0; } function score(item, query, frecencyData) { - var typeBonus = Weights.typeBonus[item.type] || 0 + var typeBonus = Weights.typeBonus[item.type] || 0; if (!query || query.length === 0) { - var usageCount = frecencyData ? frecencyData.usageCount : 0 - return typeBonus + (usageCount * 100) + var usageCount = frecencyData ? frecencyData.usageCount : 0; + return typeBonus + (usageCount * 100); } - var name = (item.name || "").toLowerCase() - var q = query.toLowerCase() + var name = (item.name || "").toLowerCase(); + var q = query.toLowerCase(); - var textScore = calculateTextScore(name, q) + var textScore = calculateTextScore(name, q); if (textScore === 0 && item.subtitle) { - var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q) - textScore = subtitleScore * 0.5 + var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q); + textScore = subtitleScore * 0.5; } if (textScore === 0 && item.keywords) { for (var i = 0; i < item.keywords.length; i++) { - var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q) + var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q); if (keywordScore > 0) { - textScore = keywordScore * 0.3 - break + textScore = keywordScore * 0.3; + break; } } } if (textScore === 0 && item.data && item.data.genericName) { - var genericLower = item.data.genericName.toLowerCase() + var genericLower = item.data.genericName.toLowerCase(); if (genericLower.startsWith(q)) - textScore = Weights.genericPrefix + textScore = Weights.genericPrefix; else if (genericLower.includes(q)) - textScore = Weights.generic + textScore = Weights.generic; } if (textScore === 0 && item.id && item.type === "app") { - var id = item.id.toLowerCase().replace(/\.desktop$/, "") + var id = item.id.toLowerCase().replace(/\.desktop$/, ""); if (id.includes(q)) { - textScore = Weights.id + textScore = Weights.id; } } - if (textScore === 0) return 0 + if (textScore === 0) + return 0; - var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0 + var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0; - return textScore + usageBonus + typeBonus + return textScore + usageBonus + typeBonus; } function scoreItems(items, query, getFrecencyFn) { - var scored = [] + var scored = []; for (var i = 0; i < items.length; i++) { - var item = items[i] - var itemScore + var item = items[i]; + var itemScore; if (item._preScored !== undefined && (query || item._preScored > 900)) { - itemScore = item._preScored + itemScore = item._preScored; } else { - var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null - itemScore = score(item, query, frecencyData) + var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null; + itemScore = score(item, query, frecencyData); } if (itemScore > 0 || !query || query.length === 0) { scored.push({ item: item, - score: itemScore - }) + score: itemScore, + order: scored.length + }); } } scored.sort(function (a, b) { - return b.score - a.score - }) + if (a.score !== b.score) + return b.score - a.score; + return a.order - b.order; + }); - return scored + return scored; } function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) { - var sections = {} - var result = [] - var limit = maxPerSection || 50 + var sections = {}; + var result = []; + var limit = maxPerSection || 50; for (var i = 0; i < sectionOrder.length; i++) { - var sectionId = sectionOrder[i].id + var sectionId = sectionOrder[i].id; sections[sectionId] = { id: sectionId, title: sectionOrder[i].title, @@ -200,53 +215,53 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec items: [], collapsed: false, flatStartIndex: 0 - } + }; } for (var i = 0; i < scoredItems.length; i++) { - var scoredItem = scoredItems[i] - var item = scoredItem.item - var sectionId = item.section || "apps" + var scoredItem = scoredItems[i]; + var item = scoredItem.item; + var sectionId = item.section || "apps"; if (sections[sectionId] && sections[sectionId].items.length < limit) { - sections[sectionId].items.push(item) + sections[sectionId].items.push(item); } else if (sections["apps"] && sections["apps"].items.length < limit) { - sections["apps"].items.push(item) + sections["apps"].items.push(item); } } for (var i = 0; i < sectionOrder.length; i++) { - var section = sections[sectionOrder[i].id] + var section = sections[sectionOrder[i].id]; if (section && section.items.length > 0) { if (sortAlphabetically && section.id === "apps") { section.items.sort(function (a, b) { - return (a.name || "").localeCompare(b.name || "") - }) + return (a.name || "").localeCompare(b.name || ""); + }); } - result.push(section) + result.push(section); } } - return result + return result; } function flattenSections(sections) { - var flat = [] - flat._sectionBounds = null - var bounds = {} + var flat = []; + flat._sectionBounds = null; + var bounds = {}; for (var i = 0; i < sections.length; i++) { - var section = sections[i] + var section = sections[i]; flat.push({ isHeader: true, section: section, sectionId: section.id, sectionIndex: i - }) + }); - var itemStart = flat.length - section.flatStartIndex = itemStart + var itemStart = flat.length; + section.flatStartIndex = itemStart; if (!section.collapsed) { for (var j = 0; j < section.items.length; j++) { @@ -256,21 +271,21 @@ function flattenSections(sections) { sectionId: section.id, sectionIndex: i, indexInSection: j - }) + }); } } - var itemEnd = flat.length - 1 - var itemCount = flat.length - itemStart + var itemEnd = flat.length - 1; + var itemCount = flat.length - itemStart; if (itemCount > 0) { bounds[section.id] = { start: itemStart, end: itemEnd, count: itemCount - } + }; } } - flat._sectionBounds = bounds - return flat + flat._sectionBounds = bounds; + return flat; } diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml index fce72d8e6..af5f86221 100644 --- a/quickshell/Modals/DankLauncherV2/TileItem.qml +++ b/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -65,6 +65,8 @@ LauncherTile { anchors.fill: parent radius: Theme.cornerRadiusM color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium ScreencopyView { id: screencopyView diff --git a/quickshell/Modals/DisplayConfirmationModal.qml b/quickshell/Modals/DisplayConfirmationModal.qml index 8f40b197e..24cd5eacf 100644 --- a/quickshell/Modals/DisplayConfirmationModal.qml +++ b/quickshell/Modals/DisplayConfirmationModal.qml @@ -89,6 +89,8 @@ DankModal { height: 70 radius: Theme.cornerRadius color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium StyledText { anchors.centerIn: parent diff --git a/quickshell/Modals/Greeter/GreeterCompletePage.qml b/quickshell/Modals/Greeter/GreeterCompletePage.qml index 8da31ada6..7b58462d3 100644 --- a/quickshell/Modals/Greeter/GreeterCompletePage.qml +++ b/quickshell/Modals/Greeter/GreeterCompletePage.qml @@ -172,6 +172,8 @@ Item { height: keybindsGrid.height + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium readonly property bool useTwoColumns: width > 500 readonly property int columnCount: useTwoColumns ? 2 : 1 @@ -251,6 +253,8 @@ Item { height: noKeybindsColumn.height + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium visible: !root.hasKeybinds Column { diff --git a/quickshell/Modals/Greeter/GreeterDoctorPage.qml b/quickshell/Modals/Greeter/GreeterDoctorPage.qml index e9fff1fb1..bf7195dcb 100644 --- a/quickshell/Modals/Greeter/GreeterDoctorPage.qml +++ b/quickshell/Modals/Greeter/GreeterDoctorPage.qml @@ -290,6 +290,8 @@ Item { anchors.rightMargin: Theme.spacingXL radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium clip: true Column { diff --git a/quickshell/Modals/Greeter/GreeterFeatureCard.qml b/quickshell/Modals/Greeter/GreeterFeatureCard.qml index a4f8536cd..efac2b27a 100644 --- a/quickshell/Modals/Greeter/GreeterFeatureCard.qml +++ b/quickshell/Modals/Greeter/GreeterFeatureCard.qml @@ -16,6 +16,8 @@ Rectangle { height: Math.round(Theme.fontSizeMedium * 6.4) radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.fill: parent diff --git a/quickshell/Modals/Greeter/GreeterModal.qml b/quickshell/Modals/Greeter/GreeterModal.qml index 967d834cf..705f7d64b 100644 --- a/quickshell/Modals/Greeter/GreeterModal.qml +++ b/quickshell/Modals/Greeter/GreeterModal.qml @@ -163,6 +163,8 @@ DankFloatingWindow { height: indicatorHeight radius: Theme.fullRadius(width, height) color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Row { id: pageIndicatorRow @@ -224,6 +226,8 @@ DankFloatingWindow { anchors.bottom: parent.bottom height: Math.round(Theme.fontSizeMedium * 4.5) color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.top: parent.top diff --git a/quickshell/Modals/Greeter/GreeterQuickLink.qml b/quickshell/Modals/Greeter/GreeterQuickLink.qml index 738965275..f16634ae0 100644 --- a/quickshell/Modals/Greeter/GreeterQuickLink.qml +++ b/quickshell/Modals/Greeter/GreeterQuickLink.qml @@ -14,6 +14,8 @@ Rectangle { height: Math.round(Theme.fontSizeMedium * 3.1) radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.fill: parent diff --git a/quickshell/Modals/Greeter/GreeterSettingsCard.qml b/quickshell/Modals/Greeter/GreeterSettingsCard.qml index 3b7bf5d2f..e212910d9 100644 --- a/quickshell/Modals/Greeter/GreeterSettingsCard.qml +++ b/quickshell/Modals/Greeter/GreeterSettingsCard.qml @@ -16,6 +16,8 @@ Rectangle { height: Math.round(Theme.fontSizeMedium * 4.5) radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Rectangle { anchors.fill: parent diff --git a/quickshell/Modals/Greeter/GreeterStatusCard.qml b/quickshell/Modals/Greeter/GreeterStatusCard.qml index 8ff1a5a91..bc6ce418c 100644 --- a/quickshell/Modals/Greeter/GreeterStatusCard.qml +++ b/quickshell/Modals/Greeter/GreeterStatusCard.qml @@ -17,8 +17,8 @@ Rectangle { height: Math.round(Theme.fontSizeMedium * 5) radius: Theme.cornerRadius color: bgColor - border.width: selected ? 2 : 0 - border.color: selected ? iconColor : Theme.withAlpha(iconColor, 0) + border.width: selected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth + border.color: selected ? iconColor : Theme.outlineMedium scale: mouseArea.pressed ? 0.97 : 1 Behavior on scale { diff --git a/quickshell/Modals/MuxModal.qml b/quickshell/Modals/MuxModal.qml index 93a9faf42..8241eb5c9 100644 --- a/quickshell/Modals/MuxModal.qml +++ b/quickshell/Modals/MuxModal.qml @@ -327,6 +327,8 @@ DankModal { height: 56 radius: Theme.cornerRadius color: muxModal.selectedIndex === -1 ? Theme.selectedContainer : (newMouse.containsMouse ? Theme.foregroundColor(Theme.chipSurface, true) : Theme.floatingWindowNestedSurface) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium RowLayout { anchors.fill: parent @@ -404,6 +406,8 @@ DankModal { height: 64 radius: Theme.cornerRadius color: muxModal.selectedIndex === index ? Theme.selectedContainer : (sessionMouse.containsMouse ? Theme.cardSurface : Theme.withAlpha(Theme.cardSurface, 0)) + border.width: muxModal.selectedIndex === index || sessionMouse.containsMouse ? Theme.layerOutlineWidth : 0 + border.color: Theme.outlineMedium MouseArea { id: sessionMouse diff --git a/quickshell/Modals/NetworkInfoModal.qml b/quickshell/Modals/NetworkInfoModal.qml index d14022c82..e1797c5a7 100644 --- a/quickshell/Modals/NetworkInfoModal.qml +++ b/quickshell/Modals/NetworkInfoModal.qml @@ -95,8 +95,8 @@ DankModal { height: parent.height - 140 radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineStrong - border.width: 1 + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth clip: true DankFlickable { diff --git a/quickshell/Modals/PowerProfileModal.qml b/quickshell/Modals/PowerProfileModal.qml index b7a215e22..4e13278e9 100644 --- a/quickshell/Modals/PowerProfileModal.qml +++ b/quickshell/Modals/PowerProfileModal.qml @@ -196,8 +196,8 @@ DankModal { return Theme.floatingWindowNestedSurface; } - border.color: isActive ? Theme.primary : (isSelected ? Theme.withAlpha(Theme.primary, 0.5) : Theme.withAlpha(Theme.primary, 0)) - border.width: (isActive || isSelected) ? 2 : 0 + border.color: isActive ? Theme.primary : (isSelected ? Theme.withAlpha(Theme.primary, 0.5) : Theme.outlineMedium) + border.width: (isActive || isSelected) ? Theme.outlineWidthFocused : Theme.layerOutlineWidth // Shortcut Key Badge on Top-Right Corner Rectangle { diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 4b9a95226..b24a4610d 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -325,6 +325,8 @@ DankFloatingWindow { width: parent.width height: showBanner ? bannerContent.implicitHeight + Theme.spacingM * 2 : 0 color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium visible: showBanner clip: true diff --git a/quickshell/Modals/SwitchUserModal.qml b/quickshell/Modals/SwitchUserModal.qml index 487888d37..3cee65b50 100644 --- a/quickshell/Modals/SwitchUserModal.qml +++ b/quickshell/Modals/SwitchUserModal.qml @@ -160,6 +160,8 @@ DankModal { height: bodyCol.implicitHeight + Theme.spacingM * 2 radius: Theme.cornerRadiusL color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Row { anchors.fill: parent diff --git a/quickshell/Modules/ControlCenter/Components/WidgetConfigOverlay.qml b/quickshell/Modules/ControlCenter/Components/WidgetConfigOverlay.qml index b9dd8dfda..682baea5c 100644 --- a/quickshell/Modules/ControlCenter/Components/WidgetConfigOverlay.qml +++ b/quickshell/Modules/ControlCenter/Components/WidgetConfigOverlay.qml @@ -79,6 +79,8 @@ Item { height: menu.implicitHeight + Theme.spacingS * 2 radius: Theme.windowRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium x: Math.max(Theme.spacingS, Math.min(root.anchorX + root.anchorWidth - width, root.width - width - Theme.spacingS)) y: root.anchorY - height - Theme.spacingS < Theme.spacingS ? root.anchorY + root.anchorHeight + Theme.spacingS : root.anchorY - height - Theme.spacingS opacity: root.visible ? 1 : 0 diff --git a/quickshell/Modules/ControlCenter/Widgets/CcMenu.qml b/quickshell/Modules/ControlCenter/Widgets/CcMenu.qml index d328801b6..b0fd14aa6 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcMenu.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcMenu.qml @@ -134,6 +134,8 @@ Item { height: column.implicitHeight + Theme.spacingS * 2 radius: Theme.windowRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium opacity: root._open ? 1 : 0 scale: root._open ? 1 : CcMetrics.popupEnterScale transformOrigin: I18n.isRtl ? Item.TopLeft : Item.TopRight diff --git a/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml b/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml index 4d880b0ef..b41c471cd 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml @@ -82,6 +82,8 @@ Item { height: column.implicitHeight + Theme.spacingL * 2 radius: Theme.windowRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium opacity: root.shown ? 1 : 0 scale: root.shown ? 1 : CcMetrics.popupEnterScale diff --git a/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml b/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml index 8e85b500a..4e1c59aa9 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml @@ -48,6 +48,8 @@ Item { anchors.fill: parent radius: root.bodyRadius color: CcMetrics.tileInactiveColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium visible: root.tall } @@ -63,6 +65,8 @@ Item { y: root.vertical ? parent.height - height : root.tall ? 0 : (parent.height - height) / 2 buttonSize: root.actionSize backgroundColor: CcMetrics.tileInactiveColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium iconName: root.iconName iconSize: CcMetrics.iconBoxIconSize iconColor: CcMetrics.tileInactiveContent diff --git a/quickshell/Modules/ControlCenter/Widgets/CcTile.qml b/quickshell/Modules/ControlCenter/Widgets/CcTile.qml index 215735b23..30ea28bb8 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcTile.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcTile.qml @@ -151,6 +151,8 @@ Item { anchors.fill: parent radius: root.bodyRadius color: root.bodyColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Behavior on color { enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion @@ -221,6 +223,8 @@ Item { height: width radius: root.hasIconBox ? root.iconBoxRadius : 0 color: root.hasIconBox ? root.iconBoxColor : "transparent" + border.width: root.hasIconBox ? Theme.layerOutlineWidth : 0 + border.color: Theme.outlineMedium activeFocusOnTab: root.showExpand && root.toggle && root.acceptsInput Accessible.role: root.toggle ? Accessible.CheckBox : Accessible.Button Accessible.name: root.title diff --git a/quickshell/Modules/ControlCenter/Widgets/CcWidgetLibrary.qml b/quickshell/Modules/ControlCenter/Widgets/CcWidgetLibrary.qml index 80a96486f..d30e024e2 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcWidgetLibrary.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcWidgetLibrary.qml @@ -27,6 +27,8 @@ Rectangle { implicitHeight: CcMetrics.libraryPanelHeight radius: Theme.windowRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium MouseArea { anchors.fill: parent diff --git a/quickshell/Modules/DankBar/Widgets/Media.qml b/quickshell/Modules/DankBar/Widgets/Media.qml index cf43f85b5..65f7640cb 100644 --- a/quickshell/Modules/DankBar/Widgets/Media.qml +++ b/quickshell/Modules/DankBar/Widgets/Media.qml @@ -137,6 +137,7 @@ BasePill { round: false checkable: true checked: root._isPlaying + iconFilled: false iconName: root._isPlaying ? "pause" : "play_arrow" Accessible.name: root._isPlaying ? I18n.tr("Pause") : I18n.tr("Play") enabled: root.activePlayer?.canTogglePlaying ?? false diff --git a/quickshell/Modules/DankDash/DashMetrics.qml b/quickshell/Modules/DankDash/DashMetrics.qml index dad192d6e..d9624a8ee 100644 --- a/quickshell/Modules/DankDash/DashMetrics.qml +++ b/quickshell/Modules/DankDash/DashMetrics.qml @@ -198,8 +198,12 @@ Singleton { readonly property real mediaArtLowResScale: 0.85 readonly property real mediaArtSizeMaterial: 200 readonly property real mediaArtRadius: Theme.cornerRadiusXL + readonly property real mediaArtSoftRadiusRatio: 0.3 readonly property real mediaArtPlaceholderIcon: Theme.buttonHeightM readonly property real mediaSeekbarHeight: 22 + readonly property real mediaPlayWidthRatio: 1.5 + readonly property real mediaPlayWidthStep: 0.75 + readonly property real mediaTransportPressScale: 1.2 readonly property real accentSelectedAlpha: 0.2 readonly property real groupIdleAlpha: 0.1 readonly property int mediaPositionPollInterval: 1000 diff --git a/quickshell/Modules/DankDash/DashRegistry.qml b/quickshell/Modules/DankDash/DashRegistry.qml index 46e1b16ef..b7c905c9c 100644 --- a/quickshell/Modules/DankDash/DashRegistry.qml +++ b/quickshell/Modules/DankDash/DashRegistry.qml @@ -113,6 +113,14 @@ Singleton { { "value": "circle", "text": I18n.tr("Circle") + }, + { + "value": "square", + "text": I18n.tr("Soft square", "Album artwork shape") + }, + { + "value": "slanted", + "text": I18n.tr("Slanted", "Album artwork shape") } ]), choice("titleFont", I18n.tr("Title font"), MediaOptions.defaultTitleFont, Theme.fontChoices), toggle("seekbar", I18n.tr("Show seekbar"), MediaOptions.defaults.seekbar), toggle("waveProgress", I18n.tr("Wave progress bars"), MediaOptions.defaults.waveProgress), toggle("albumArtBackdrop", I18n.tr("Album art backdrop"), MediaOptions.defaults.albumArtBackdrop), toggle("albumArtAccent", I18n.tr("Use album art accent"), MediaOptions.defaults.albumArtAccent), toggle("animatedArt", I18n.tr("Apple Music animated covers"), MediaOptions.defaults.animatedArt, I18n.tr("Sends the playing artist and album name to Apple")), toggle("lyrics", I18n.tr("Lyrics", "Media player lyrics button"), MediaOptions.defaults.lyrics, I18n.tr("Sends the playing track, artist and album name to enabled lyrics providers"))] }, diff --git a/quickshell/Modules/DankDash/Media/LyricsOverlay.qml b/quickshell/Modules/DankDash/Media/LyricsOverlay.qml index 50ea70258..601ac13ec 100644 --- a/quickshell/Modules/DankDash/Media/LyricsOverlay.qml +++ b/quickshell/Modules/DankDash/Media/LyricsOverlay.qml @@ -219,11 +219,11 @@ FocusScope { anchors.topMargin: Theme.spacingM anchors.horizontalCenter: parent.horizontalCenter width: parent.width - Theme.spacingM * 2 - text: root.unsynced ? I18n.tr("Unsynced", "Lyrics have no timestamps") : I18n.tr("Lyrics", "Media player lyrics button") + text: I18n.tr("Unsynced", "Lyrics have no timestamps") color: Theme.onSurfaceVariant font.pixelSize: Theme.fontSizeSmall horizontalAlignment: Text.AlignHCenter - visible: !root.compact + visible: root.unsynced && !root.compact } DankListView { diff --git a/quickshell/Modules/DankDash/Media/MediaArtSurface.qml b/quickshell/Modules/DankDash/Media/MediaArtSurface.qml new file mode 100644 index 000000000..3fa466003 --- /dev/null +++ b/quickshell/Modules/DankDash/Media/MediaArtSurface.qml @@ -0,0 +1,73 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import Quickshell.Widgets +import qs.Common +import qs.Modules.DankDash +import "../../../DankCommon/Widgets/MaterialShapes.js" as Shapes + +ClippingRectangle { + id: root + + property string artStyle: "rounded" + readonly property real contentRadius: artStyle === "slanted" ? width * DashMetrics.mediaArtSoftRadiusRatio * Theme.shapeScale : radius + + radius: { + switch (artStyle) { + case "circle": + return width / 2; + case "square": + return width * DashMetrics.mediaArtSoftRadiusRatio * Theme.shapeScale; + case "slanted": + return 0; + } + return DashMetrics.mediaArtRadius; + } + color: "transparent" + antialiasing: true + layer.enabled: artStyle === "slanted" + layer.effect: MultiEffect { + maskEnabled: true + maskSource: shapeMask.item + } + + Loader { + id: shapeMask + anchors.fill: parent + active: root.layer.enabled + visible: false + + sourceComponent: Item { + layer.enabled: true + + Shape { + id: shape + + readonly property real scaleX: boundingRect.width > 0 ? root.width / boundingRect.width : 1 + readonly property real scaleY: boundingRect.height > 0 ? root.height / boundingRect.height : 1 + + x: -boundingRect.x * scaleX + y: -boundingRect.y * scaleY + width: root.width + height: root.height + preferredRendererType: Shape.CurveRenderer + transform: Scale { + xScale: shape.scaleX + yScale: shape.scaleY + } + + ShapePath { + strokeWidth: 0 + strokeColor: "transparent" + fillColor: Theme.withAlpha(Theme.surface, 1) + + PathSvg { + path: Shapes.buildPath("slanted", root.width, root.height, Theme.shapeScale === 0) + } + } + } + } + } +} diff --git a/quickshell/Modules/DankDash/Media/MediaArtView.qml b/quickshell/Modules/DankDash/Media/MediaArtView.qml index 44b89ba7e..ed0b79274 100644 --- a/quickshell/Modules/DankDash/Media/MediaArtView.qml +++ b/quickshell/Modules/DankDash/Media/MediaArtView.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import QtQuick -import Quickshell.Widgets import qs.Widgets import qs.Modules.DankDash @@ -12,14 +11,12 @@ Item { property bool showLyrics: true property bool holdArt: false - readonly property real artRadius: root.player.options?.artStyle === "circle" ? width / 2 : DashMetrics.mediaArtRadius + readonly property real artRadius: surface.contentRadius - ClippingRectangle { + MediaArtSurface { id: surface anchors.fill: parent - radius: root.artRadius - color: "transparent" - antialiasing: true + artStyle: root.player.options?.artStyle ?? "rounded" MediaArtwork { id: art diff --git a/quickshell/Modules/DankDash/Media/MediaChromeBase.qml b/quickshell/Modules/DankDash/Media/MediaChromeBase.qml index adf71b0ba..8efa07953 100644 --- a/quickshell/Modules/DankDash/Media/MediaChromeBase.qml +++ b/quickshell/Modules/DankDash/Media/MediaChromeBase.qml @@ -59,6 +59,8 @@ Item { anchors.fill: parent radius: root.backdropRadius color: root.surfaceColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Loader { anchors.fill: parent diff --git a/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml b/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml index ea9fe7892..e9a332062 100644 --- a/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml +++ b/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml @@ -94,9 +94,10 @@ Item { iconName: AudioService.sinkIcon(AudioService.sink) tooltipText: root.fittedSinkName === root.sinkName ? root.devicesLabel : root.sinkName Accessible.name: root.devicesLabel + ": " + root.sinkName - backgroundColor: "transparent" - textColor: Theme.onSurfaceVariant - horizontalPadding: Theme.spacingS + buttonHeight: Theme.buttonHeightXS + backgroundColor: MediaAccentService.accentSecondaryContainer + textColor: MediaAccentService.onAccentSecondaryContainer + horizontalPadding: Theme.spacingM onClicked: root.player.togglePanel("devices") MediaSinkWheel {} diff --git a/quickshell/Modules/DankDash/Media/MediaTransport.qml b/quickshell/Modules/DankDash/Media/MediaTransport.qml index 7b842f75c..ef26750a4 100644 --- a/quickshell/Modules/DankDash/Media/MediaTransport.qml +++ b/quickshell/Modules/DankDash/Media/MediaTransport.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import qs.Common import qs.Services import qs.Widgets +import qs.Modules.DankDash import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation RowLayout { @@ -14,7 +15,8 @@ RowLayout { required property var presentation readonly property var focusTargets: [shuffleButton, previousButton, playButton, nextButton, repeatButton] - readonly property bool hasControls: !!presentation && (presentation.play || presentation.previous || presentation.next || presentation.shuffle || presentation.repeat) + readonly property bool hasControls: !!presentation + readonly property var pressSpring: Theme.springPreset("fast", Theme.expressiveDurations.expressiveFastSpatial) spacing: Theme.spacingS implicitHeight: Theme.buttonHeightM @@ -38,7 +40,7 @@ RowLayout { id: playButton mediaAction: "play" size: "m" - Layout.preferredWidth: Theme.buttonHeightM + Theme.spacingS + weight: DashMetrics.mediaPlayWidthRatio + (Number(shuffleButton.visible) + Number(repeatButton.visible)) * DashMetrics.mediaPlayWidthStep variant: "filled" iconSize: Theme.iconSizeLarge containerColor: MediaAccentService.accentContainer @@ -56,15 +58,32 @@ RowLayout { } component TransportButton: MediaTransportButton { + id: button + + property real weight: 1 + readonly property real targetWeight: weight * (pressed ? DashMetrics.mediaTransportPressScale : 1) + player: root.player + keepNavigation: true Layout.fillWidth: true - Layout.preferredWidth: Theme.minimumTouchTargetSize + Layout.preferredWidth: Theme.minimumTouchTargetSize * widthSpring.value Layout.minimumWidth: Theme.iconSizeMedium + Theme.spacingXS * 2 Layout.alignment: Qt.AlignVCenter round: false - buttonSize: size === "m" ? Theme.buttonHeightM : Theme.minimumTouchTargetSize + buttonSize: Theme.buttonHeightM + radius: Theme.buttonRadius(width, height, buttonSize, pressed, false) variant: "tonal" containerColor: checked ? MediaAccentService.accentContainer : MediaAccentService.accentSecondaryContainer contentColor: checked ? MediaAccentService.onAccentContainer : MediaAccentService.onAccentSecondaryContainer + + onTargetWeightChanged: widthSpring.retarget(targetWeight) + + SpringMotion { + id: widthSpring + enabled: root.visible && root.player.live && !Theme.springMotionDisabled + stiffness: root.pressSpring.stiffness + damping: root.pressSpring.damping + Component.onCompleted: snapTo(button.targetWeight) + } } } diff --git a/quickshell/Modules/DankDash/Media/MediaTransportButton.qml b/quickshell/Modules/DankDash/Media/MediaTransportButton.qml index 1633bfdde..ac1b9ff22 100644 --- a/quickshell/Modules/DankDash/Media/MediaTransportButton.qml +++ b/quickshell/Modules/DankDash/Media/MediaTransportButton.qml @@ -11,6 +11,7 @@ DankIconButton { required property var player required property string mediaAction + property bool keepNavigation: false readonly property var presentation: root.player.presentation readonly property var activePlayer: root.player.activePlayer @@ -18,6 +19,7 @@ DankIconButton { readonly property bool repeatingTrack: root.presentation?.loopState === MprisLoopState.Track checkable: root.mediaAction === "shuffle" || root.mediaAction === "play" || root.mediaAction === "repeat" + iconFilled: false iconName: { switch (root.mediaAction) { @@ -56,11 +58,11 @@ DankIconButton { case "shuffle": return !!root.presentation?.shuffle; case "previous": - return !!root.presentation?.previous; + return root.keepNavigation ? !!root.presentation : !!root.presentation?.previous; case "play": return !!root.presentation?.play; case "next": - return !!root.presentation?.next; + return root.keepNavigation ? !!root.presentation : !!root.presentation?.next; case "repeat": return !!root.presentation?.repeat; } @@ -72,7 +74,7 @@ DankIconButton { case "shuffle": return !!root.activePlayer?.shuffleSupported && !!root.activePlayer?.canControl; case "previous": - return !!root.activePlayer?.canGoPrevious || !!root.activePlayer?.canSeek; + return !!root.activePlayer?.canGoPrevious || (!root.keepNavigation && !!root.activePlayer?.canSeek); case "play": return !!root.activePlayer?.canTogglePlaying; case "next": diff --git a/quickshell/Modules/DankDash/Media/ZurvanChrome.qml b/quickshell/Modules/DankDash/Media/ZurvanChrome.qml index a664e472d..06d048e56 100644 --- a/quickshell/Modules/DankDash/Media/ZurvanChrome.qml +++ b/quickshell/Modules/DankDash/Media/ZurvanChrome.qml @@ -10,11 +10,9 @@ import "../../../Common/Format.js" as Format MediaChromeBase { id: root - readonly property bool compact: width < Theme.smallBreakpoint + readonly property bool compact: width < Theme.smallBreakpoint || halfWidth < headerHeight + controls.implicitHeight readonly property real halfWidth: Math.max(0, (width - padding * 2 - Theme.spacingXL) / 2) readonly property real bodyHeight: Math.max(0, height - padding * 2) - readonly property real paneWidth: artworkSize - readonly property real paneOffset: Math.max(0, (width - padding * 2 - paneWidth * 2 - Theme.spacingXL) / 2) // !TODO: expose this variant? playerPaneOpen false gives art beside lyrics, or art alone with hover transport readonly property bool playerPane: root.player.playerPaneOpen readonly property bool controlsColumn: playerPane @@ -26,24 +24,24 @@ MediaChromeBase { readonly property real columnHeight: compact ? artworkSize : Math.max(artworkSize, paneHeight) readonly property real paneHeight: Math.max(artworkSize, headerHeight + controls.implicitHeight) readonly property real paneMinHeight: Math.max(naturalArtSize, headerHeight + controls.implicitHeight) - readonly property real headerHeight: sourceRow.height + Theme.spacingL + readonly property real headerHeight: sourceRow.height + Theme.spacingS readonly property real renderScale: Window.window?.devicePixelRatio ?? Screen.devicePixelRatio readonly property int artSide: TrackArtService.resolvedArtSide readonly property bool lowResArt: artSide > 0 && artSide < artworkSize * renderScale * DashMetrics.mediaArtLowResRatio artSize: DashMetrics.mediaArtSizeDash - surfaceColor: DashMetrics.cardColor + surfaceColor: Theme.foregroundColor(Theme.hostSurface) baseHeight: Math.max(DashMetrics.tabMinHeight, padding * 2 + (compact ? naturalArtSize + Theme.spacingXL + paneMinHeight : Math.max(naturalArtSize, paneMinHeight))) - focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], seekbar.canSeek && seekBlock.visible ? [seekbar] : [], transport.focusTargets, sourceGroup.focusTargets) + focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], sourceGroup.focusTargets, transport.focusTargets, seekbar.canSeek && seekBlock.visible ? [seekbar] : []) panelButtons: [playerButton].concat(sourceGroup.panelButtons) inlineVolume: true Item { id: artPane - x: root.compact || !root.splitPanes ? (parent.width - width) / 2 : root.paneOffset + (I18n.isRtl ? root.paneWidth + Theme.spacingXL : 0) + x: root.compact || !root.splitPanes ? (parent.width - width) / 2 : I18n.isRtl ? parent.width - width : 0 y: root.compact && root.splitPanes ? 0 : (parent.height - height) / 2 - width: root.compact ? parent.width : root.paneWidth + width: root.compact ? parent.width : art.width height: root.columnHeight Behavior on x { @@ -57,8 +55,11 @@ MediaChromeBase { Item { id: art + + readonly property real scaledSize: root.artworkSize * (root.lowResArt ? DashMetrics.mediaArtLowResScale : 1) + anchors.centerIn: parent - width: root.artworkSize * (root.lowResArt ? DashMetrics.mediaArtLowResScale : 1) + width: root.compact ? scaledSize : Math.min(root.artworkSize, Math.max(scaledSize, root.headerHeight + controls.implicitHeight)) height: width HoverHandler { @@ -191,6 +192,7 @@ MediaChromeBase { component ArtTransportButton: MediaTransportButton { player: root.player + keepNavigation: true round: true checkable: false buttonSize: Theme.minimumTouchTargetSize @@ -202,9 +204,9 @@ MediaChromeBase { Item { id: contentPane - x: root.compact ? 0 : root.paneOffset + (I18n.isRtl ? 0 : root.paneWidth + Theme.spacingXL) + x: root.compact || I18n.isRtl ? 0 : artPane.width + Theme.spacingXL y: root.compact ? root.artworkSize + Theme.spacingXL : (parent.height - height) / 2 - width: root.compact ? parent.width : root.paneWidth + width: root.compact ? parent.width : parent.width - artPane.width - Theme.spacingXL height: root.compact ? root.paneHeight : root.columnHeight visible: root.splitPanes @@ -225,20 +227,22 @@ MediaChromeBase { Column { id: controls visible: root.controlsColumn - y: root.headerHeight + (parent.height - root.headerHeight - height) / 2 + y: root.compact ? parent.height - height : art.y + art.height - height width: parent.width - spacing: Theme.spacingL + spacing: Theme.spacingS Rectangle { width: parent.width - height: metadata.implicitHeight + Theme.spacingL * 2 + height: metadata.implicitHeight + Theme.spacingS * 2 radius: Theme.cornerRadiusL color: DashMetrics.cardColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Column { id: metadata x: Theme.spacingL - y: Theme.spacingL + y: Theme.spacingS width: parent.width - Theme.spacingL * 2 spacing: Theme.spacingXS @@ -248,7 +252,7 @@ MediaChromeBase { font: Qt.font({ family: titleText.font.family, weight: titleText.font.weight, - pixelSize: Theme.fontSizeXXLarge + pixelSize: Theme.fontSizeDisplay }) } @@ -262,9 +266,9 @@ MediaChromeBase { readonly property real fittedPixelSize: { if (fullSizeTitle.advanceWidth <= width) - return Theme.fontSizeXXLarge; - const fitted = Math.floor(Theme.fontSizeXXLarge * width / fullSizeTitle.advanceWidth); - return Math.max(Theme.fontSizeXLarge, fitted); + return Theme.fontSizeDisplay; + const fitted = Math.floor(Theme.fontSizeDisplay * width / fullSizeTitle.advanceWidth); + return Math.max(Theme.fontSizeXXLarge, fitted); } width: parent.width @@ -299,7 +303,8 @@ MediaChromeBase { height: playback.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadiusL color: DashMetrics.cardColor - visible: root.hasSeekbar || transport.hasControls + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Column { id: playback @@ -308,6 +313,20 @@ MediaChromeBase { width: parent.width - Theme.spacingL * 2 spacing: Theme.spacingS + MediaSourceGroup { + id: sourceGroup + width: parent.width + player: root.player + } + + MediaTransport { + id: transport + width: parent.width + player: root.player + presentation: root.presentation + visible: hasControls + } + Item { id: seekBlock width: parent.width @@ -316,16 +335,12 @@ MediaChromeBase { LayoutMirroring.enabled: false LayoutMirroring.childrenInherit: true - NumericText { + TimeLabel { id: elapsed anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: Math.ceil(reservedWidth) text: Format.formatDuration(seekbar.value * (root.presentation?.length ?? 0)) reserveText: duration.text.replace(/\d/g, "8") - isMonospace: false - color: Theme.onSurfaceVariant - font.pixelSize: Theme.fontSizeSmall } MediaSeekbar { @@ -337,41 +352,22 @@ MediaChromeBase { player: root.player } - NumericText { + TimeLabel { id: duration anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - width: Math.ceil(reservedWidth) text: Format.formatDuration(root.presentation?.length ?? 0) reserveText: text.replace(/\d/g, "8") - isMonospace: false - color: Theme.onSurfaceVariant - font.pixelSize: Theme.fontSizeSmall } } - - MediaTransport { - id: transport - anchors.horizontalCenter: parent.horizontalCenter - width: Math.min(parent.width, implicitWidth) - player: root.player - presentation: root.presentation - visible: hasControls - } } } - - MediaSourceGroup { - id: sourceGroup - width: parent.width - player: root.player - } } Item { id: sourceRow - y: Math.round((controls.y - height) / 2) + y: root.compact ? Math.round((controls.y - height) / 2) : art.y width: parent.width height: Theme.buttonHeightXS @@ -409,4 +405,12 @@ MediaChromeBase { } } } + + component TimeLabel: NumericText { + width: Math.ceil(reservedWidth) + isMonospace: false + color: Theme.onSurfaceVariant + font.pixelSize: Theme.fontSizeSmall + font.weight: Theme.fontWeightMedium + } } diff --git a/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml b/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml index 7b01c98c8..b5485e16d 100644 --- a/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml +++ b/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml @@ -6,6 +6,7 @@ import qs.Common import qs.Services import qs.Widgets import qs.Modules.DankDash +import qs.Modules.DankDash.Media import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation Card { @@ -25,7 +26,6 @@ Card { readonly property string artUrl: TrackArtService.resolvedArtUrl readonly property color accent: MediaAccentService.accent readonly property bool showSeekbar: options.seekbar !== false && !tiny - readonly property bool circleArt: options.artStyle === "circle" readonly property bool zurvan: options.playerStyle === "zurvan" readonly property bool inlineMetadata: compact && titleLabel.implicitHeight + artistLabel.implicitHeight + trackText.spacing > transport.y - Theme.spacingXS readonly property var playbackFocusTargets: [playButton, previousButton, nextButton].concat(seekbar.canSeek ? [seekbar] : []) @@ -92,15 +92,20 @@ Card { visible: !!root.activePlayer enabled: root.interactive - MediaArtwork { + MediaArtSurface { id: artwork anchors.right: parent.right anchors.top: parent.top width: root.showArtwork ? Math.max(0, Math.min(DashMetrics.overviewArtHero, parent.width / 3, seekBlock.y - Theme.spacingM)) : 0 height: width - cornerRadius: root.circleArt ? width / 2 : DashMetrics.mediaArtRadius - artUrl: root.artUrl + artStyle: root.options.artStyle ?? "rounded" visible: root.showArtwork + + MediaArtwork { + anchors.fill: parent + cornerRadius: 0 + artUrl: root.artUrl + } } Column { @@ -188,6 +193,8 @@ Card { round: false checkable: true checked: root.playing + iconFilled: false + radius: Theme.buttonRadius(width, height, buttonSize, pressed, !root.zurvan && checked) iconName: root.playing ? "pause" : "play_arrow" Accessible.name: root.playing ? I18n.tr("Pause") : I18n.tr("Play") enabled: !!root.activePlayer?.canTogglePlaying @@ -204,6 +211,7 @@ Card { width: transport.stacked ? (parent.width - transport.spacing) / 2 : (parent.width - playButton.width - transport.spacing * 2) / 2 variant: root.zurvan ? "tonal" : "standard" round: !root.zurvan + radius: Theme.buttonRadius(width, height, buttonSize, pressed, round) containerColor: MediaAccentService.accentSecondaryContainer contentColor: root.zurvan ? MediaAccentService.onAccentSecondaryContainer : Theme.onSecondaryContainer iconName: "skip_previous" @@ -221,6 +229,7 @@ Card { width: previousButton.width variant: previousButton.variant round: previousButton.round + radius: Theme.buttonRadius(width, height, buttonSize, pressed, round) containerColor: previousButton.containerColor contentColor: previousButton.contentColor iconName: "skip_next" diff --git a/quickshell/Modules/DankDash/WallpaperTab.qml b/quickshell/Modules/DankDash/WallpaperTab.qml index 90f528430..42a148779 100644 --- a/quickshell/Modules/DankDash/WallpaperTab.qml +++ b/quickshell/Modules/DankDash/WallpaperTab.qml @@ -662,6 +662,8 @@ Item { anchors.fill: parent anchors.margins: Theme.spacingXS color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: DashMetrics.wallpaperThumbRadius ClippingRectangle { @@ -953,6 +955,8 @@ Item { width: DashMetrics.pageJumpWidth height: jumpColumn.height + Theme.spacingM * 2 color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.cornerRadiusM visible: false z: DashMetrics.overlayZ + 1 diff --git a/quickshell/Modules/DankDash/Weather/WeatherForecastChart.qml b/quickshell/Modules/DankDash/Weather/WeatherForecastChart.qml index 515f34619..eea290e44 100644 --- a/quickshell/Modules/DankDash/Weather/WeatherForecastChart.qml +++ b/quickshell/Modules/DankDash/Weather/WeatherForecastChart.qml @@ -11,6 +11,8 @@ Rectangle { radius: Theme.cornerRadiusXL color: DashMetrics.cardColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium LayoutMirroring.enabled: false LayoutMirroring.childrenInherit: true diff --git a/quickshell/Modules/DankDash/Weather/WeatherMetricCard.qml b/quickshell/Modules/DankDash/Weather/WeatherMetricCard.qml index 0b15b3763..d52e00849 100644 --- a/quickshell/Modules/DankDash/Weather/WeatherMetricCard.qml +++ b/quickshell/Modules/DankDash/Weather/WeatherMetricCard.qml @@ -117,6 +117,8 @@ Rectangle { radius: graphics && reading.shape ? Theme.fullRadius(width, height) : Theme.cornerRadiusXL color: graphics && widgetId === "uv" ? "transparent" : DashMetrics.cardColor + border.width: graphics && widgetId === "uv" ? 0 : Theme.layerOutlineWidth + border.color: Theme.outlineMedium Accessible.role: Accessible.StaticText Accessible.name: reading.label + " " + (hasValue ? reading.value : I18n.tr("Not available")) + " " + (reading.detail ?? "") Accessible.description: graphicDescription diff --git a/quickshell/Modules/DankDash/Weather/WeatherSky.qml b/quickshell/Modules/DankDash/Weather/WeatherSky.qml index 22b3b1ce5..26838b9ee 100644 --- a/quickshell/Modules/DankDash/Weather/WeatherSky.qml +++ b/quickshell/Modules/DankDash/Weather/WeatherSky.qml @@ -40,6 +40,8 @@ Rectangle { implicitHeight: DashMetrics.gridRowUnit * 2 + DashMetrics.gridGap radius: Theme.cornerRadiusXL color: DashMetrics.cardColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Row { anchors.left: parent.left diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index 96c42f7f4..0d0eb7337 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -914,8 +914,8 @@ Item { clip: true radius: Theme.cornerRadius color: Theme.withAlpha(Theme.cardSurface, 0.9) - border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) - border.width: inputField.activeFocus ? 2 : 1 + border.color: inputField.activeFocus ? Theme.primary : Theme.outlineMedium + border.width: inputField.activeFocus ? Theme.outlineWidthFocused : Theme.layerOutlineWidth GreeterUserPicker { id: userPicker diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index c6a60a4e3..6f714e071 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -623,8 +623,8 @@ Item { Layout.preferredHeight: LockMetrics.fieldHeight radius: Theme.fullRadius(width, height) color: Theme.cardSurface - border.width: passwordField.activeFocus ? Math.max(Theme.outlineWidth, Theme.focusRingWidth) : Theme.outlineWidth - border.color: passwordField.activeFocus ? Theme.focusRingColor : Theme.outlineVariant + border.width: passwordField.activeFocus ? Math.max(Theme.outlineWidth, Theme.focusRingWidth) : Theme.layerOutlineWidth + border.color: passwordField.activeFocus ? Theme.focusRingColor : Theme.outlineMedium Accessible.name: I18n.tr("Password") visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0 diff --git a/quickshell/Modules/Notepad/NotepadSettings.qml b/quickshell/Modules/Notepad/NotepadSettings.qml index 62b891da5..9bd51d219 100644 --- a/quickshell/Modules/Notepad/NotepadSettings.qml +++ b/quickshell/Modules/Notepad/NotepadSettings.qml @@ -92,7 +92,7 @@ Item { radius: Theme.cornerRadius color: Theme.withAlpha(Theme.cardSurface, Theme.notepadTransparency) border.color: Theme.outlineMedium - border.width: 1 + border.width: Theme.layerOutlineWidth z: 100 Rectangle { diff --git a/quickshell/Modules/Notepad/NotepadTextEditor.qml b/quickshell/Modules/Notepad/NotepadTextEditor.qml index 35edb0695..f78e0e842 100644 --- a/quickshell/Modules/Notepad/NotepadTextEditor.qml +++ b/quickshell/Modules/Notepad/NotepadTextEditor.qml @@ -365,6 +365,8 @@ Column { visible: searchVisible opacity: searchVisible ? 1 : 0 color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.cornerRadius Behavior on opacity { @@ -486,7 +488,7 @@ Column { height: parent.height - bottomControls.height - Theme.spacingM - (searchVisible ? searchBar.height + Theme.spacingM : 0) color: Theme.withAlpha(Theme.cardSurface, Theme.notepadTransparency) border.color: Theme.outlineMedium - border.width: 1 + border.width: Theme.layerOutlineWidth radius: Theme.cornerRadius RowLayout { @@ -720,6 +722,8 @@ Column { anchors.right: parent.right height: 36 color: Theme.withAlpha(Theme.cardSurface, Theme.notepadTransparency) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium z: 2 Row { @@ -930,7 +934,7 @@ Column { radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface border.color: Theme.outlineMedium - border.width: 1 + border.width: Theme.layerOutlineWidth z: 10 Row { diff --git a/quickshell/Modules/Notifications/NotificationCard.qml b/quickshell/Modules/Notifications/NotificationCard.qml index 5cc9a6d52..28ad381be 100644 --- a/quickshell/Modules/Notifications/NotificationCard.qml +++ b/quickshell/Modules/Notifications/NotificationCard.qml @@ -74,6 +74,8 @@ Rectangle { bottomLeftRadius: lastInGroup ? outerRadius : radius bottomRightRadius: bottomLeftRadius color: surfaceColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium clip: true LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true diff --git a/quickshell/Modules/OSD/MediaPlaybackOSD.qml b/quickshell/Modules/OSD/MediaPlaybackOSD.qml index deaceeb01..b0801fde6 100644 --- a/quickshell/Modules/OSD/MediaPlaybackOSD.qml +++ b/quickshell/Modules/OSD/MediaPlaybackOSD.qml @@ -294,6 +294,7 @@ DankOSD { round: false checkable: true checked: root.playing + iconFilled: false iconName: root._displayIcon containerColor: MediaAccentService.accentContainer contentColor: MediaAccentService.onAccentContainer @@ -360,6 +361,7 @@ DankOSD { round: false checkable: true checked: root.playing + iconFilled: false backgroundColor: "transparent" iconColor: MediaAccentService.onAccent iconName: root._displayIcon diff --git a/quickshell/Modules/ProcessList/DisksView.qml b/quickshell/Modules/ProcessList/DisksView.qml index e38c3f77f..434fd9f60 100644 --- a/quickshell/Modules/ProcessList/DisksView.qml +++ b/quickshell/Modules/ProcessList/DisksView.qml @@ -23,6 +23,8 @@ Item { Layout.preferredHeight: 80 radius: Theme.cornerRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium RowLayout { anchors.fill: parent @@ -100,6 +102,8 @@ Item { Layout.fillHeight: true radius: Theme.cornerRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium ColumnLayout { anchors.fill: parent diff --git a/quickshell/Modules/ProcessList/PerformanceCard.qml b/quickshell/Modules/ProcessList/PerformanceCard.qml index 45a42d88f..1dd51972a 100644 --- a/quickshell/Modules/ProcessList/PerformanceCard.qml +++ b/quickshell/Modules/ProcessList/PerformanceCard.qml @@ -23,6 +23,8 @@ Rectangle { implicitHeight: compact ? cardContent.implicitHeight + Theme.spacingM * 2 : 0 radius: Theme.cornerRadiusL color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(card)) + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Connections { target: DgopService diff --git a/quickshell/Modules/ProcessList/ProcessContextMenu.qml b/quickshell/Modules/ProcessList/ProcessContextMenu.qml index d7c18c9f7..5a9de9d8f 100644 --- a/quickshell/Modules/ProcessList/ProcessContextMenu.qml +++ b/quickshell/Modules/ProcessList/ProcessContextMenu.qml @@ -201,6 +201,8 @@ Popup { background: Rectangle { color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.windowRadius } diff --git a/quickshell/Modules/ProcessList/SystemView.qml b/quickshell/Modules/ProcessList/SystemView.qml index f7e4cead7..cbe9d200b 100644 --- a/quickshell/Modules/ProcessList/SystemView.qml +++ b/quickshell/Modules/ProcessList/SystemView.qml @@ -22,6 +22,8 @@ Item { Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium ColumnLayout { id: systemInfoColumn @@ -95,6 +97,8 @@ Item { Layout.fillHeight: true radius: Theme.cornerRadius color: Theme.nestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium ColumnLayout { anchors.fill: parent diff --git a/quickshell/Modules/Settings/DankBarTab.qml b/quickshell/Modules/Settings/DankBarTab.qml index d87976b24..82d9d274c 100644 --- a/quickshell/Modules/Settings/DankBarTab.qml +++ b/quickshell/Modules/Settings/DankBarTab.qml @@ -56,29 +56,19 @@ Item { subtitle: I18n.tr("The Island holds this edge on a display this bar covers, so the bar stays hidden there") } - SettingsRow { - body: Item { - width: parent.width - height: positionButtonGroup.height - - DankButtonGroup { - id: positionButtonGroup - anchors.horizontalCenter: parent.horizontalCenter - model: bar.positionChoices.map(pos => bar.positionLabel(pos)) - currentIndex: { - bar.selectedBarId; - const config = SettingsData.getBarConfig(bar.selectedBarId); - return bar.positionChoices.indexOf(config?.position ?? SettingsData.Position.Top); - } - onSelectionChanged: (index, selected) => { - if (!selected || index < 0 || index >= bar.positionChoices.length) - return; - SettingsData.updateBarConfig(bar.selectedBarId, { - position: bar.positionChoices[index] - }); - bar.notifyHorizontalBarChange(); - } - } + SettingsLayoutPicker { + edgePlacement: true + choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({ + key: String(position), + label: bar.positionLabel(position), + enabled: bar.positionChoices.includes(position) + })) + selectedKey: String(bar.selectedBarConfig?.position ?? SettingsData.Position.Top) + onSelected: key => { + SettingsData.updateBarConfig(bar.selectedBarId, { + position: Number(key) + }); + bar.notifyHorizontalBarChange(); } } diff --git a/quickshell/Modules/Settings/DesktopWidgetBrowser.qml b/quickshell/Modules/Settings/DesktopWidgetBrowser.qml index 60659b186..ceb4bcd03 100644 --- a/quickshell/Modules/Settings/DesktopWidgetBrowser.qml +++ b/quickshell/Modules/Settings/DesktopWidgetBrowser.qml @@ -42,8 +42,8 @@ WidgetPickerWindow { radius: Theme.cornerRadius property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex color: isSelected ? Theme.primarySelected : widgetArea.containsMouse ? Theme.primaryHover : Theme.floatingWindowNestedSurface - border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.outline, 0.2) - border.width: isSelected ? Theme.outlineWidthFocused : Theme.outlineWidth + border.color: isSelected ? Theme.primary : Theme.outlineMedium + border.width: isSelected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth Row { anchors.fill: parent diff --git a/quickshell/Modules/Settings/DisplayConfig/ICCProfileInfoModal.qml b/quickshell/Modules/Settings/DisplayConfig/ICCProfileInfoModal.qml index 92b2b0184..f43f2d7aa 100644 --- a/quickshell/Modules/Settings/DisplayConfig/ICCProfileInfoModal.qml +++ b/quickshell/Modules/Settings/DisplayConfig/ICCProfileInfoModal.qml @@ -164,8 +164,8 @@ DankModal { height: parent.height - 120 radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineStrong - border.width: 1 + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth clip: true DankFlickable { diff --git a/quickshell/Modules/Settings/DockHubHeader.qml b/quickshell/Modules/Settings/DockHubHeader.qml index f196bf426..0c6cd97ab 100644 --- a/quickshell/Modules/Settings/DockHubHeader.qml +++ b/quickshell/Modules/Settings/DockHubHeader.qml @@ -142,7 +142,7 @@ Column { tags: ["dock", "layout", "placement", "position", "edge"] SettingsLayoutPicker { - dockPlacement: true + edgePlacement: true choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({ key: String(position), label: dock.positionLabel(position), diff --git a/quickshell/Modules/Settings/NetworkCellularTab.qml b/quickshell/Modules/Settings/NetworkCellularTab.qml index e311ca18d..8d9399886 100644 --- a/quickshell/Modules/Settings/NetworkCellularTab.qml +++ b/quickshell/Modules/Settings/NetworkCellularTab.qml @@ -97,8 +97,8 @@ Item { height: 56 radius: Theme.cornerRadius color: modemMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface - border.width: isConnected ? Theme.outlineWidthFocused : 0 - border.color: Theme.primary + border.width: isConnected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth + border.color: isConnected ? Theme.primary : Theme.outlineMedium Row { anchors.left: parent.left @@ -232,8 +232,8 @@ Item { height: 56 radius: Theme.cornerRadius color: profileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface - border.color: isActive ? Theme.primary : Theme.outlineLight - border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth + border.color: isActive ? Theme.primary : Theme.outlineMedium + border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth Row { anchors.left: parent.left diff --git a/quickshell/Modules/Settings/NetworkEthernetTab.qml b/quickshell/Modules/Settings/NetworkEthernetTab.qml index 9e86efdce..95576486f 100644 --- a/quickshell/Modules/Settings/NetworkEthernetTab.qml +++ b/quickshell/Modules/Settings/NetworkEthernetTab.qml @@ -85,8 +85,8 @@ Item { height: isExpanded ? 56 + ethExpandedContent.height : 56 radius: Theme.cornerRadius color: ethDeviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface - border.width: isConnected ? Theme.outlineWidthFocused : 0 - border.color: Theme.primary + border.width: isConnected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth + border.color: isConnected ? Theme.primary : Theme.outlineMedium clip: true Behavior on height { @@ -381,8 +381,8 @@ Item { height: 48 radius: Theme.cornerRadius color: wiredMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface - border.width: modelData.isActive ? Theme.outlineWidthFocused : 0 - border.color: Theme.primary + border.width: modelData.isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth + border.color: modelData.isActive ? Theme.primary : Theme.outlineMedium Row { anchors.left: parent.left diff --git a/quickshell/Modules/Settings/NetworkVpnTab.qml b/quickshell/Modules/Settings/NetworkVpnTab.qml index d3128d9d4..527acd307 100644 --- a/quickshell/Modules/Settings/NetworkVpnTab.qml +++ b/quickshell/Modules/Settings/NetworkVpnTab.qml @@ -231,8 +231,8 @@ Item { height: isExpanded ? 56 + vpnExpandedContent.height : 56 radius: Theme.cornerRadius color: vpnRowArea.containsMouse ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.floatingWindowNestedSurface) - border.width: isActive ? Theme.outlineWidthFocused : 0 - border.color: Theme.primary + border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth + border.color: isActive ? Theme.primary : Theme.outlineMedium opacity: DMSNetworkService.isBusy ? 0.6 : 1.0 clip: true diff --git a/quickshell/Modules/Settings/PluginBrowser.qml b/quickshell/Modules/Settings/PluginBrowser.qml index 58dc79f98..ef5e7934f 100644 --- a/quickshell/Modules/Settings/PluginBrowser.qml +++ b/quickshell/Modules/Settings/PluginBrowser.qml @@ -966,8 +966,8 @@ RegistryBrowserWindow { height: Math.round(width * SettingsMetrics.choiceCardPreviewRatio) radius: Theme.cornerRadiusM color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineVariant - border.width: Theme.outlineWidth + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth ClippingRectangle { anchors.fill: parent diff --git a/quickshell/Modules/Settings/PluginCard.qml b/quickshell/Modules/Settings/PluginCard.qml index 22aaca003..92b994e33 100644 --- a/quickshell/Modules/Settings/PluginCard.qml +++ b/quickshell/Modules/Settings/PluginCard.qml @@ -27,8 +27,8 @@ DankCard { implicitHeight: previewHeight + infoHeight + Theme.spacingS * 2 + Theme.spacingM radius: Theme.cornerRadiusM color: Theme.floatingWindowNestedSurface - border.color: Theme.focusRingColor - border.width: selected ? Theme.focusRingWidth : 0 + border.color: selected ? Theme.focusRingColor : Theme.outlineMedium + border.width: selected ? Theme.focusRingWidth : Theme.layerOutlineWidth pad: 0 clickable: true Accessible.name: plugin.name || "" diff --git a/quickshell/Modules/Settings/PluginUpdatesDialog.qml b/quickshell/Modules/Settings/PluginUpdatesDialog.qml index d46bddf16..88735419b 100644 --- a/quickshell/Modules/Settings/PluginUpdatesDialog.qml +++ b/quickshell/Modules/Settings/PluginUpdatesDialog.qml @@ -21,8 +21,8 @@ StyledRect { height: visible ? innerColumn.implicitHeight + Theme.spacingL * 2 : 0 radius: Theme.cornerRadiusM color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineVariant - border.width: Theme.outlineWidth + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth clip: true visible: false diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index c5959ca93..3300c86b2 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -195,6 +195,8 @@ Item { height: 90 radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium ClippingRectangle { anchors.fill: parent @@ -523,8 +525,8 @@ Item { height: themeGrid.cardHeight radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface - border.color: isActive ? Theme.primary : Theme.outline - border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth + border.color: isActive ? Theme.primary : Theme.outlineMedium + border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth scale: isActive ? 1.03 : 1 Behavior on scale { diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index 663ab4c80..23fc2878b 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -178,8 +178,9 @@ Column { tags: ["foreground", "layers", "outline", "border", "cards", "pills", "widgets", "notifications", "control center"] settingKey: "blurLayerOutlineOpacity" text: I18n.tr("Layer outline opacity") - value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100) + value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0) * 100) minimum: 0 + minimumLabel: I18n.tr("Off") maximum: 40 onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100) } diff --git a/quickshell/Modules/Settings/Widgets/SettingsGroup.qml b/quickshell/Modules/Settings/Widgets/SettingsGroup.qml index dc6fd9e69..3ae960cb2 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsGroup.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsGroup.qml @@ -114,6 +114,8 @@ Item { bottomLeftRadius: last ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius bottomRightRadius: last ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius color: root.highlighted ? Theme.blend(root.slotColor, Theme.primary, SettingsMetrics.highlightBlend) : root.slotColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Behavior on color { enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None diff --git a/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml b/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml index 971d38210..e0c46ca94 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml @@ -1,20 +1,21 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Layouts import qs.Common import qs.Modules.DankBar import qs.Widgets -Row { +GridLayout { id: root - property bool dockPlacement: false + property bool edgePlacement: false property bool widgetStyle: false property var choices: barModes property string selectedKey: activeBarMode signal selected(string key) onSelected: key => { - if (!dockPlacement && !widgetStyle) + if (!edgePlacement && !widgetStyle) applyBarMode(key); } @@ -37,6 +38,8 @@ Row { readonly property real previewAspect: 0.62 readonly property real previewStripRatio: 0.55 readonly property real previewIslandRatio: 0.42 + readonly property real previewWidth: Math.round(Theme.iconSize * previewWidthRatio) + readonly property real minimumCardWidth: previewWidth + Theme.spacingL * 2 readonly property var targetConfig: { SettingsData.barConfigs; SettingsUiState.selectedBarId; @@ -70,7 +73,13 @@ Row { } width: parent?.width ?? 0 - spacing: Theme.spacingS + columns: { + const count = Math.max(1, root.choices.length); + const availableColumns = Math.max(1, Math.floor((width + columnSpacing) / (minimumCardWidth + columnSpacing))); + return Math.ceil(count / Math.ceil(count / availableColumns)); + } + columnSpacing: Theme.spacingS + rowSpacing: Theme.spacingS Repeater { model: root.choices @@ -83,11 +92,14 @@ Row { readonly property bool isActive: root.selectedKey === modelData.key - width: (root.width - root.spacing * (root.choices.length - 1)) / Math.max(1, root.choices.length) - height: Math.round(Theme.fontSizeMedium * root.cardHeightRatio) + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumWidth: 0 + implicitWidth: root.minimumCardWidth + implicitHeight: Math.max(Math.round(Theme.fontSizeMedium * root.cardHeightRatio), cardContent.implicitHeight + Theme.spacingM * 2) radius: Theme.cornerRadius color: Theme.floatingWindowNestedSurface - border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth + border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth border.color: isActive ? Theme.primary : Theme.outlineMedium activeFocusOnTab: true @@ -108,7 +120,10 @@ Row { } Column { + id: cardContent + anchors.centerIn: parent + width: Math.max(0, parent.width - Theme.spacingM * 2) spacing: Theme.spacingS Rectangle { @@ -116,7 +131,7 @@ Row { readonly property real edgePad: Math.max(2, Math.round(width * 0.045)) readonly property real stripSize: Math.round(width * 0.11) - width: Math.round(Theme.iconSize * root.previewWidthRatio) + width: Math.min(root.previewWidth, cardContent.width) height: Math.round(width * root.previewAspect) radius: Theme.spacingXS color: Theme.chipSurface @@ -148,7 +163,7 @@ Row { Rectangle { readonly property int edge: Number(modeCard.modelData.key) readonly property bool vertical: edge === SettingsData.Position.Left || edge === SettingsData.Position.Right - visible: root.dockPlacement + visible: root.edgePlacement x: edge === SettingsData.Position.Right ? parent.width - width - screenPreview.edgePad : screenPreview.edgePad y: edge === SettingsData.Position.Bottom ? parent.height - height - screenPreview.edgePad : screenPreview.edgePad width: vertical ? screenPreview.stripSize : parent.width - screenPreview.edgePad * 2 @@ -158,7 +173,7 @@ Row { } Rectangle { - visible: !root.widgetStyle && !root.dockPlacement && modeCard.modelData.key === "standard" + visible: !root.widgetStyle && !root.edgePlacement && modeCard.modelData.key === "standard" anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -202,11 +217,13 @@ Row { } StyledText { + width: parent.width text: modeCard.modelData.label font.pixelSize: Theme.fontSizeSmall font.weight: Theme.fontWeightMedium color: modeCard.isActive ? Theme.primary : Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsRow.qml b/quickshell/Modules/Settings/Widgets/SettingsRow.qml index b793a2f6e..2a4551743 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsRow.qml @@ -116,6 +116,8 @@ T.Control { anchors.fill: parent visible: root.paintBackground color: root.isHighlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium 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 77db9dfec..3bcb2295a 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml @@ -46,6 +46,8 @@ Rectangle { bottomLeftRadius: bottomRadius bottomRightRadius: bottomRadius color: active ? SettingsMetrics.selectedRowColor : SettingsMetrics.rowColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium Behavior on color { enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None diff --git a/quickshell/Modules/Settings/Widgets/SettingsSurfacePreview.qml b/quickshell/Modules/Settings/Widgets/SettingsSurfacePreview.qml index ddf71d235..c53cc584e 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSurfacePreview.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSurfacePreview.qml @@ -46,6 +46,8 @@ Item { Layer { label: I18n.tr("Cards") color: Theme.cardSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium radius: Theme.cornerRadiusM Layer { diff --git a/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml b/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml index 7cb8f08a6..35d2891b1 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml @@ -37,7 +37,7 @@ Flow { height: root.compact ? root.compactTileHeight : swatch.height + label.implicitHeight + Theme.spacingS * 3 radius: Theme.cornerRadiusM color: Theme.floatingWindowNestedSurface - border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth + border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth border.color: isActive ? Theme.primary : Theme.outlineMedium activeFocusOnTab: true diff --git a/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml b/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml index fe75878ea..222597a93 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml @@ -37,6 +37,8 @@ Item { anchors.fill: parent radius: Theme.groupedListOuterRadius color: SettingsMetrics.rowColor + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium visible: root.standalone } diff --git a/quickshell/Services/IconThemeIndex.js b/quickshell/Services/IconThemeIndex.js new file mode 100644 index 000000000..7571bff47 --- /dev/null +++ b/quickshell/Services/IconThemeIndex.js @@ -0,0 +1,65 @@ +.pragma library + +function create() { + return { + paths: Object.create(null), + scores: Object.create(null), + directories: Object.create(null) + }; +} + +function add(index, path, searchDirs, looseDirs) { + const separator = path.lastIndexOf("/") + 1; + const fileName = path.substring(separator); + const name = fileName.replace(/\.(svg|png|xpm)$/, ""); + if (!/^[\w.+-]+$/.test(name)) + return; + const directory = path.substring(0, separator); + let directoryScore = index.directories[directory]; + if (directoryScore === undefined) { + directoryScore = score(directory, searchDirs, looseDirs); + index.directories[directory] = directoryScore; + } + const candidateScore = directoryScore + (fileName.endsWith(".svg") ? 100000 : 0); + const currentScore = index.scores[name]; + if (currentScore !== undefined && candidateScore <= currentScore) + return; + index.paths[name] = path; + index.scores[name] = candidateScore; +} + +function chainIndex(path, searchDirs, looseDirs) { + for (let i = 0; i < searchDirs.length; i++) { + if (path.startsWith(searchDirs[i] + "/")) + return i; + } + for (let i = 0; i < looseDirs.length; i++) { + if (path.startsWith(looseDirs[i] + "/")) + return searchDirs.length + i; + } + return searchDirs.length + looseDirs.length; +} + +function score(path, searchDirs, looseDirs) { + let s = 0; + if (path.includes("/apps/")) + s += 3000000000; + else if (path.includes("/categories/")) + s += 1000000000; + else if (path.includes("/places/") || path.includes("/devices/") || path.includes("/mimetypes/") || path.includes("/status/") || path.includes("/actions/")) + s += 100000000; + + s += Math.min(99, Math.max(0, searchDirs.length + looseDirs.length - chainIndex(path, searchDirs, looseDirs))) * 1000000; + + if (path.endsWith(".svg")) + s += 100000; + + if (path.includes("/scalable/")) { + s += 1000; + } else { + const m = path.match(/\/(\d+)(?:x\d+)?(?:@\d+x)?\//); + if (m) + s += Math.min(parseInt(m[1]), 999); + } + return s; +} diff --git a/quickshell/Services/IconThemeService.qml b/quickshell/Services/IconThemeService.qml index 4c73c6fcc..5069aa110 100644 --- a/quickshell/Services/IconThemeService.qml +++ b/quickshell/Services/IconThemeService.qml @@ -4,9 +4,11 @@ pragma ComponentBehavior: Bound import QtCore import QtQuick import Quickshell +import Quickshell.Io import qs.Common import qs.Services -import "../Common/GSettings.js" as GSettings // ! This import is actually used even though qmlls claims it isnt, do not delete +import "../Common/GSettings.js" as GSettings // qmllint disable unused-imports +import "IconThemeIndex.js" as IconIndex Singleton { id: root @@ -15,23 +17,18 @@ Singleton { readonly property string settingsTheme: { if (typeof SettingsData === "undefined") return ""; - const t = SettingsData.resolveIconTheme(); - return (!t || t === "System Default") ? "" : t; + const theme = SettingsData.resolveIconTheme(); + return (!theme || theme === "System Default") ? "" : theme; } property string systemProbedTheme: "" readonly property string managedTheme: settingsTheme || systemProbedTheme + property bool ready: false + property var _iconPaths: Object.create(null) + property bool _initialized: false + property bool _probing: false + property bool _building: false + property bool _rebuildPending: false - property var _searchDirs: [] - property string _dirsForTheme: "" - property var _cache: ({}) - property var _looseIconPaths: ({}) - property int _cacheGeneration: 0 - property int _rebuildGeneration: 0 - property int _looseIndexGeneration: 0 - property int revision: 0 - property bool _bumpPending: false - - // XDG icon spec order: user dirs win over system ones, so overrides in ~/.local/share/icons beat installed themes. readonly property var _userIconRoots: { const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)); const home = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation)); @@ -59,101 +56,63 @@ Singleton { return bases; } - // Loose icon files (AppImage managers, legacy /usr/share/pixmaps installs) live outside any theme. readonly property var _looseDirs: { const dirs = _userIconRoots.concat(_dataDirs.map(d => d + "/pixmaps")); return dirs.filter((dir, index) => dirs.indexOf(dir) === index); } - onManagedThemeChanged: _rebuild() + onSettingsThemeChanged: { + if (_initialized && !settingsTheme) + _probeSystemTheme(); + } + onManagedThemeChanged: Qt.callLater(_rebuild) + Component.onCompleted: { + _initialized = true; Paths.iconResolver = name => resolve(name); _probeSystemTheme(); - _rebuild(); + Qt.callLater(_rebuild); } Connections { target: DesktopEntries function onApplicationsChanged() { - root._indexLooseIcons(() => root._invalidateMisses()); + if (!root.ready) + return; + Qt.callLater(root._rebuild); } } - // "System Default" leaves Qt's lookup, which sees only hicolor unless a Qt platform theme or QS_ICON_THEME is configured. function _probeSystemTheme() { - if (Quickshell.env("QS_ICON_THEME")) + if (Quickshell.env("QS_ICON_THEME") || _probing) return; + _probing = true; const script = `v=$(sed -n 's/^gtk-icon-theme-name *= *//p' "\${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/settings.ini" 2>/dev/null | head -1) [ -z "$v" ] && v=$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")}) printf '%s' "$v" | tr -d "'\\""`; - Proc.runCommand("iconThemeProbe", ["sh", "-c", script], (out, code) => { - const theme = (out || "").trim(); - if (!theme) - return; - root.systemProbedTheme = theme; - }); - } - - function _bumpRevision() { - if (_bumpPending) - return; - _bumpPending = true; - Qt.callLater(() => { - _bumpPending = false; - revision++; - }); - } - - function _invalidateMisses() { - const c = {}; - let changed = false; - for (const name in _cache) { - if (_cache[name]) - c[name] = _cache[name]; - else - changed = true; - } - if (!changed) - return; - _cacheGeneration++; - _cache = c; - _bumpRevision(); - } - - function _indexLooseIcons(callback) { - const generation = ++_looseIndexGeneration; - const args = ["find", "-L"].concat(_looseDirs, ["-maxdepth", "1", "(", "-name", "*.svg", "-o", "-name", "*.png", "-o", "-name", "*.xpm", ")"]); - Proc.runCommand("iconLooseIndex", args, (out, code) => { - if (root._looseIndexGeneration !== generation) - return; - const index = {}; - const paths = (out || "").trim().split("\n").filter(s => s); - for (const path of paths) { - const fileName = path.substring(path.lastIndexOf("/") + 1); - const name = fileName.replace(/\.(svg|png|xpm)$/, ""); - const current = index[name]; - index[name] = current ? root._pickBest([current, path]) : path; - } - root._looseIconPaths = index; - if (callback) - callback(); + Proc.runCommand(null, ["sh", "-c", script], (out, code) => { + root.systemProbedTheme = (out || "").trim(); + root._probing = false; + Qt.callLater(root._rebuild); }, 0); } function _rebuild() { - const rebuildGeneration = ++_rebuildGeneration; - _cacheGeneration++; - _cache = ({}); - _dirsForTheme = ""; - _indexLooseIcons(() => root._invalidateMisses()); - if (!managedTheme) { - _searchDirs = []; - _bumpRevision(); + if (!_initialized || _probing) + return; + if (_building) { + _rebuildPending = true; return; } + _building = true; + _rebuildPending = false; const theme = managedTheme; + if (!theme) { + _publishIndex(theme, Object.create(null)); + return; + } const script = `theme=$1 shift find_index() { target=$1; shift; for b; do [ -f "$b/$target/index.theme" ] && { echo "$b/$target/index.theme"; return 0; }; done; return 1; } @@ -170,105 +129,101 @@ done case " $visited " in *" hicolor "*) ;; *) order="$order hicolor";; esac for t in $order; do for b; do d="$b/$t"; [ -d "$d" ] && echo "$d"; done; done`; - Proc.runCommand("iconChain:" + theme, ["sh", "-c", script, "icon-chain", theme].concat(_baseDirs), (out, code) => { - if (root.managedTheme !== theme || root._rebuildGeneration !== rebuildGeneration) - return; - root._searchDirs = (out || "").trim().split("\n").filter(s => s); - root._dirsForTheme = theme; - root._cacheGeneration++; - root._cache = ({}); - root._bumpRevision(); - }); - } - - function resolve(name) { - const _dep = revision; - if (!managedTheme || !name) - return ""; - if (name.startsWith("/") || name.startsWith("file://") || name.startsWith("image://") || name.startsWith("~")) - return ""; - if (!/^[\w.+-]+$/.test(name)) - return ""; - if (_dirsForTheme !== managedTheme) - return ""; - if (name in _cache) - return _cache[name] || ""; - _cache[name] = null; - _resolveAsync(name); - return ""; - } - - function _resolveAsync(name) { - const generation = _cacheGeneration; - - function finish(paths) { - if (root._cacheGeneration !== generation) - return; - const best = paths.length > 0 ? root._pickBest(paths) : root._looseIconPaths[name] || ""; - const c = root._cache; - c[name] = best ? Paths.toFileUrl(best) : ""; - root._cache = c; - root._bumpRevision(); - } - - if (_searchDirs.length === 0) { - finish([]); - return; - } - - const args = ["find", "-L"].concat(_searchDirs, ["(", "-name", name + ".svg", "-o", "-name", name + ".png", ")"]); - Proc.runCommand("iconResolveTheme:" + name, args, (out, code) => { - const paths = (out || "").trim().split("\n").filter(s => s); - finish(paths); + Proc.runCommand(null, ["sh", "-c", script, "icon-chain", theme].concat(_baseDirs), (out, code) => { + const dirs = (out || "").trim().split("\n").filter(dir => dir); + root._indexTheme(theme, dirs); }, 0); } - function _pickBest(paths) { - let best = ""; - let bestScore = -1; - for (let i = 0; i < paths.length; i++) { - const s = _score(paths[i]); - if (s > bestScore) { - bestScore = s; - best = paths[i]; + function _indexTheme(theme, dirs) { + indexJob.createObject(root, { + theme: theme, + searchDirs: dirs, + looseDirs: _looseDirs + }); + } + + function _publishIndex(theme, paths) { + _building = false; + if (managedTheme === theme && !_probing) { + _iconPaths = paths; + ready = true; + } + if (_rebuildPending) + Qt.callLater(_rebuild); + } + + Component { + id: indexJob + + Scope { + id: job + + required property string theme + required property var searchDirs + required property var looseDirs + property var themeIndex: IconIndex.create() + property var looseIndex: IconIndex.create() + property int remaining: searchDirs.length ? 2 : 1 + property bool finished: false + + function complete() { + if (--remaining > 0) + return; + publish(); + } + + function publish() { + if (finished) + return; + finished = true; + for (const name in looseIndex.paths) { + if (!themeIndex.paths[name]) + themeIndex.paths[name] = looseIndex.paths[name]; + } + root._publishIndex(theme, themeIndex.paths); + destroy(); + } + + Process { + command: ["find", "-L"].concat(job.searchDirs, ["(", "-name", "*.svg", "-o", "-name", "*.png", ")"]) + running: job.searchDirs.length > 0 + stdout: SplitParser { + onRead: data => { + if (!job.finished) + IconIndex.add(job.themeIndex, data, job.searchDirs, job.looseDirs); + } + } + onExited: job.complete() + } + + Process { + command: ["find", "-L"].concat(job.looseDirs, ["-maxdepth", "1", "(", "-name", "*.svg", "-o", "-name", "*.png", "-o", "-name", "*.xpm", ")"]) + running: true + stdout: SplitParser { + onRead: data => { + if (!job.finished) + IconIndex.add(job.looseIndex, data, job.searchDirs, job.looseDirs); + } + } + onExited: job.complete() + } + + Timer { + interval: 10000 + running: true + onTriggered: { + root.log.warn("Icon theme index timed out:", job.theme); + job.publish(); + } } } - return best; } - function _chainIndex(path) { - for (let i = 0; i < _searchDirs.length; i++) { - if (path.startsWith(_searchDirs[i] + "/")) - return i; - } - for (let i = 0; i < _looseDirs.length; i++) { - if (path.startsWith(_looseDirs[i] + "/")) - return _searchDirs.length + i; - } - return _searchDirs.length + _looseDirs.length; - } - - function _score(path) { - let s = 0; - if (path.includes("/apps/")) - s += 3000000000; - else if (path.includes("/categories/")) - s += 1000000000; - else if (path.includes("/places/") || path.includes("/devices/") || path.includes("/mimetypes/") || path.includes("/status/") || path.includes("/actions/")) - s += 100000000; - - s += Math.min(99, Math.max(0, _searchDirs.length + _looseDirs.length - _chainIndex(path))) * 1000000; - - if (path.endsWith(".svg")) - s += 100000; - - if (path.includes("/scalable/")) { - s += 1000; - } else { - const m = path.match(/\/(\d+)(?:x\d+)?(?:@\d+x)?\//); - if (m) - s += Math.min(parseInt(m[1]), 999); - } - return s; + function resolve(name) { + if (!name || !/^[\w.+-]+$/.test(name)) + return ""; + const path = _iconPaths[name]; + return path ? Paths.toFileUrl(path) : ""; } } diff --git a/quickshell/shell.qml b/quickshell/shell.qml index 0f73c2433..77cf8d4fe 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -58,7 +58,7 @@ ShellRoot { Loader { id: shellCoreLoader - active: !entrypoint.runGreeter + active: !entrypoint.runGreeter && IconThemeService.ready asynchronous: true source: "ShellCore.qml" onLoaded: dmsShellLoader.setSource("DMSShell.qml", { @@ -73,7 +73,7 @@ ShellRoot { Loader { id: dmsGreeterLoader - active: entrypoint.runGreeter + active: entrypoint.runGreeter && IconThemeService.ready asynchronous: false source: "DMSGreeter.qml" } diff --git a/quickshell/tests/icon-theme-index.test.mjs b/quickshell/tests/icon-theme-index.test.mjs new file mode 100644 index 000000000..bcc9555d5 --- /dev/null +++ b/quickshell/tests/icon-theme-index.test.mjs @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +import test from "node:test"; + +const index = vm.createContext({}); +vm.runInContext(readFileSync(new URL("../Services/IconThemeIndex.js", import.meta.url), "utf8").replace(/^\.pragma.*$/m, ""), index); +const searchDirs = ["/user/theme", "/system/theme", "/system/inherited", "/system/hicolor"]; +const looseDirs = ["/user/icons", "/system/pixmaps"]; + +function build(paths) { + const result = index.create(); + for (const path of paths) + index.add(result, path, searchDirs, looseDirs); + return result.paths; +} + +test("index keeps the existing category, root, format, size and tie priorities", () => { + const cases = [ + ["/user/theme/actions/icon.svg", "/system/inherited/apps/icon.png"], + ["/system/theme/apps/scalable/icon.svg", "/user/theme/apps/16/icon.png"], + ["/user/theme/apps/512/icon.png", "/user/theme/apps/16/icon.svg"], + ["/user/theme/apps/512/icon.svg", "/user/theme/apps/scalable/icon.svg"], + ["/user/theme/apps/16x16/icon.png", "/user/theme/apps/48x48@2x/icon.png"], + ["/system/pixmaps/icon.svg", "/user/icons/icon.png"] + ]; + for (const [loser, winner] of cases) { + assert.equal(build([loser, winner]).icon, winner); + assert.equal(build([winner, loser]).icon, winner); + } + const ties = ["/user/theme/apps/a/icon.svg", "/user/theme/apps/b/icon.svg"]; + assert.equal(build(ties).icon, ties[0]); +}); + +test("index bounds retained paths by resolvable names and handles object property names", () => { + const paths = ["/user/theme/apps/icon.svg", "/user/theme/apps/16/icon.png", "/system/pixmaps/loose.xpm", "/user/theme/apps/__proto__.svg", "/user/theme/apps/constructor.png", "/user/theme/apps/not a name.svg"]; + const result = build(paths); + assert.equal(Object.keys(result).length, 4); + assert.equal(result.__proto__, paths[3]); + assert.equal(result.constructor, paths[4]); + assert.equal(result.loose, paths[2]); +}); + +test("first lookup uses the completed index without spawning or retaining misses", () => { + const source = readFileSync(process.env.ICON_SERVICE_SOURCE || new URL("../Services/IconThemeService.qml", import.meta.url), "utf8"); + const resolve = source.match(/^ function resolve\(name\) \{[\s\S]*?^ \}/m)[0]; + let spawns = 0; + const context = vm.createContext({ + _iconPaths: build(["/user/theme/apps/icon.svg"]), + Paths: { toFileUrl: path => "file://" + path }, + revision: 0, managedTheme: "theme", _dirsForTheme: "theme", _cache: {}, + _resolveAsync: () => spawns++ + }); + vm.runInContext(resolve, context); + assert.equal(context.resolve("icon"), "file:///user/theme/apps/icon.svg"); + for (let i = 0; i < 100; i++) + assert.equal(context.resolve("missing-" + i), ""); + assert.equal(spawns, 0); + assert.equal(Object.keys(context._iconPaths).length, 1); +}); diff --git a/scripts/format-staged.py b/scripts/format-staged.py index f0619e273..6b1c165fe 100755 --- a/scripts/format-staged.py +++ b/scripts/format-staged.py @@ -81,6 +81,12 @@ def format_js(qmlformat, file): return result.stdout +def truncated(original, new_text): + """qmlls and qmlformat can succeed and still hand back an empty or gutted + document; writing that back destroys the file and stages the wreckage.""" + return not new_text.strip() or len(new_text) < len(original) // 2 + + def find_qmllint(): for candidate in QMLLINT_CANDIDATES: path = candidate if "/" in candidate and Path(candidate).is_file() else shutil.which(candidate) @@ -276,6 +282,10 @@ def format_js_files(qmlformat, root, files): skipped += 1 print("skipped (qmlformat rejected the file)") continue + if truncated(original, new_text): + skipped += 1 + print("skipped (qmlformat returned a truncated document)") + continue if new_text == original: print("unchanged") continue @@ -377,6 +387,10 @@ def main(): client.notify("textDocument/didClose", {"textDocument": {"uri": uri}}) new_text = apply_edits(original, edits or []) + if truncated(original, new_text): + skipped += 1 + print("skipped (qmlls returned a truncated document)") + continue if new_text == original: print("unchanged") continue