From 0511cd19df814162dfcf6573ff3c61e34ea7825d Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 7 Jul 2026 16:11:11 -0400 Subject: [PATCH] qs: numerous performance optimizations - ClippingRectangle usages - Asynchronous loader usages - Replace cava visualizers with shaders --- core/internal/config/embedded/hypr-binds.lua | 2 +- .../internal/config/embedded/mango-binds.conf | 2 +- core/internal/config/embedded/niri-binds.kdl | 2 +- quickshell/DMSShellIPC.qml | 3 +- .../Modals/Clipboard/ClipboardThumbnail.qml | 37 +-- .../ClipboardLauncherPreview.qml | 4 +- .../DankLauncherV2ModalSpotlight.qml | 19 ++ .../Modals/DankLauncherV2/ResultItem.qml | 2 +- .../DankLauncherV2/SpotlightResultRow.qml | 2 +- .../DankBar/Widgets/AudioVisualization.qml | 88 +++---- .../Modules/DankBar/Widgets/FocusedApp.qml | 38 ++- .../Modules/DankDash/DankDashPopout.qml | 4 +- .../Modules/DankDash/MediaDropdownOverlay.qml | 4 + .../Modules/DankDash/MediaPlayerTab.qml | 123 ++++++---- quickshell/Modules/DankDash/OverviewTab.qml | 23 +- quickshell/Modules/DankDash/WeatherTab.qml | 34 +-- quickshell/Services/ClipboardService.qml | 40 ++- quickshell/Services/TrackArtService.qml | 35 ++- quickshell/Shaders/frag/blob.frag | 87 +++++++ quickshell/Shaders/frag/viz_bars.frag | 50 ++++ quickshell/Shaders/qsb/blob.frag.qsb | Bin 0 -> 5825 bytes quickshell/Shaders/qsb/viz_bars.frag.qsb | Bin 0 -> 3625 bytes quickshell/Widgets/DankAlbumArt.qml | 229 ++++++++++-------- quickshell/Widgets/DankCircularImage.qml | 54 ++--- quickshell/Widgets/DankPopoutConnected.qml | 15 +- quickshell/Widgets/DankPopoutStandalone.qml | 27 ++- 26 files changed, 558 insertions(+), 366 deletions(-) create mode 100644 quickshell/Shaders/frag/blob.frag create mode 100644 quickshell/Shaders/frag/viz_bars.frag create mode 100644 quickshell/Shaders/qsb/blob.frag.qsb create mode 100644 quickshell/Shaders/qsb/viz_bars.frag.qsb diff --git a/core/internal/config/embedded/hypr-binds.lua b/core/internal/config/embedded/hypr-binds.lua index ad4b5f95a..274cdb8e0 100644 --- a/core/internal/config/embedded/hypr-binds.lua +++ b/core/internal/config/embedded/hypr-binds.lua @@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle")) hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle")) hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle")) hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) -hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper")) +hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper")) hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle")) diff --git a/core/internal/config/embedded/mango-binds.conf b/core/internal/config/embedded/mango-binds.conf index 9498f5f2a..38c00048d 100644 --- a/core/internal/config/embedded/mango-binds.conf +++ b/core/internal/config/embedded/mango-binds.conf @@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle # Notepad bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle # Browse Wallpapers -bind=SUPER,y,spawn,dms ipc call dankdash wallpaper +bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper # Power Menu bind=SUPER,x,spawn,dms ipc call powermenu toggle # Cycle Display Profile diff --git a/core/internal/config/embedded/niri-binds.kdl b/core/internal/config/embedded/niri-binds.kdl index 414ece73a..09a75a30b 100644 --- a/core/internal/config/embedded/niri-binds.kdl +++ b/core/internal/config/embedded/niri-binds.kdl @@ -24,7 +24,7 @@ binds { spawn "dms" "ipc" "call" "settings" "focusOrToggle"; } Mod+Y hotkey-overlay-title="Browse Wallpapers" { - spawn "dms" "ipc" "call" "dankdash" "wallpaper"; + spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper"; } Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; } Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; } diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index 75aa6a04f..3d809dc94 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -712,12 +712,13 @@ Item { target: "hypr" } + // ! TODO - remove for v1.6 IpcHandler { function wallpaper(): string { const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar(); if (bar) { bar.triggerWallpaperBrowser(); - return "SUCCESS: Toggled wallpaper browser"; + return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead"; } return "ERROR: Failed to toggle wallpaper browser"; } diff --git a/quickshell/Modals/Clipboard/ClipboardThumbnail.qml b/quickshell/Modals/Clipboard/ClipboardThumbnail.qml index 7e9197b33..f975d8166 100644 --- a/quickshell/Modals/Clipboard/ClipboardThumbnail.qml +++ b/quickshell/Modals/Clipboard/ClipboardThumbnail.qml @@ -1,10 +1,10 @@ import QtQuick -import QtQuick.Effects +import Quickshell.Widgets import qs.Common import qs.Services import qs.Widgets -Item { +ClippingRectangle { id: thumbnail readonly property var log: Log.scoped("ClipboardThumbnail") @@ -15,6 +15,10 @@ Item { required property int itemIndex property bool disposed: false + radius: Theme.cornerRadius / 2 + color: "transparent" + antialiasing: true + Image { id: thumbnailImage @@ -34,7 +38,7 @@ Item { fillMode: Image.PreserveAspectCrop smooth: true cache: false - visible: false + visible: entryType === "image" && status === Image.Ready && source != "" asynchronous: true sourceSize.width: 128 sourceSize.height: 128 @@ -236,33 +240,6 @@ Item { } } - MultiEffect { - anchors.fill: parent - anchors.margins: 2 - source: thumbnailImage - maskEnabled: true - maskSource: clipboardRoundedRectangularMask - visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "" - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - - Item { - id: clipboardRoundedRectangularMask - width: ClipboardConstants.thumbnailSize - height: ClipboardConstants.itemHeight - 4 - layer.enabled: true - layer.smooth: true - visible: false - - Rectangle { - anchors.fill: parent - radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px) - color: "black" - antialiasing: true - } - } - DankIcon { visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "") name: { diff --git a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml index 022e76074..66ed7ffcf 100644 --- a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml +++ b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml @@ -13,7 +13,7 @@ Rectangle { property string cachedMimeType: "" property var _requestedEntryId: null - readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/") + readonly property bool canLoadImage: typeof entry?.id === "number" && !!entry?.isImage && String(entry?.mimeType ?? "").startsWith("image/") readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? "")) radius: Math.max(6, Theme.cornerRadius - 2) @@ -41,7 +41,7 @@ Rectangle { } function reloadPreview() { - if (!canLoadImage || !entry?.id) { + if (!canLoadImage || typeof entry?.id !== "number") { _requestedEntryId = null; cachedImageData = ""; cachedMimeType = ""; diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml index 0dffd0c5b..e07e73768 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml @@ -120,6 +120,18 @@ Item { readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken + // Blur region isn't auto-committed on geometry changes; kick twice to catch resize settling. + function _kickBlurCommit() { + launcherBlur.kick(); + Qt.callLater(launcherBlur.kick); + } + + onAlignedXChanged: _kickBlurCommit() + onAlignedYChanged: _kickBlurCommit() + onAlignedWidthChanged: _kickBlurCommit() + on_ContentImplicitHChanged: _kickBlurCommit() + onContentVisibleChanged: _kickBlurCommit() + signal dialogClosed function _ensureContentLoadedAndInitialize(query, mode) { @@ -328,6 +340,7 @@ Item { exclusionMode: ExclusionMode.Ignore WindowBlur { + id: launcherBlur targetWindow: launcherWindow readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2)) blurX: modalContainer.x @@ -337,6 +350,9 @@ Item { blurRadius: root.cornerRadius } + onWidthChanged: root._kickBlurCommit() + onHeightChanged: root._kickBlurCommit() + WlrLayershell.namespace: "dms:spotlight" WlrLayershell.layer: root.effectiveLauncherLayer WlrLayershell.exclusiveZone: -1 @@ -421,6 +437,9 @@ Item { opacity: contentVisible ? 1 : 0 + onOpacityChanged: root._kickBlurCommit() + onSlideOffsetChanged: root._kickBlurCommit() + Behavior on opacity { NumberAnimation { duration: contentVisible ? root._openDuration : root._closeDuration diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml index 5056bbecb..8f3e89a2a 100644 --- a/quickshell/Modals/DankLauncherV2/ResultItem.qml +++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -33,7 +33,7 @@ Rectangle { return item.icon || ""; } } - readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/") + readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/") width: parent?.width ?? 200 height: 52 diff --git a/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml b/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml index 7bc57600f..bb961d591 100644 --- a/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml +++ b/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml @@ -48,7 +48,7 @@ Rectangle { return "file://" + raw; return raw; } - readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/") + readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/") readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview readonly property string typeLabel: { diff --git a/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml b/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml index 52ace330f..e4a6df59b 100644 --- a/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml +++ b/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml @@ -7,15 +7,24 @@ Item { id: root readonly property MprisPlayer activePlayer: MprisController.activePlayer - readonly property bool hasActiveMedia: activePlayer !== null - readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing + readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing + readonly property bool live: visible && isPlaying width: 20 height: Theme.iconSize - Loader { - active: isPlaying + readonly property real maxBarHeight: Theme.iconSize - 2 + readonly property real minBarHeight: 3 + onLiveChanged: { + if (!live) { + bars.bandsA = Qt.vector4d(0, 0, 0, 0); + bars.bandsB = Qt.vector2d(0, 0); + } + } + + Loader { + active: root.live sourceComponent: Component { Ref { service: CavaService @@ -23,15 +32,8 @@ Item { } } - readonly property real maxBarHeight: Theme.iconSize - 2 - readonly property real minBarHeight: 3 - readonly property real heightRange: maxBarHeight - minBarHeight - property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight] - Timer { - id: fallbackTimer - - running: !CavaService.cavaAvailable && isPlaying + running: !CavaService.cavaAvailable && root.live interval: 500 repeat: true onTriggered: { @@ -41,54 +43,32 @@ Item { Connections { target: CavaService + enabled: root.live function onValuesChanged() { - if (!root.isPlaying) { - root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight]; + const v = CavaService.values; + if (v.length < 6) return; - } - - const newHeights = []; - for (let i = 0; i < 6; i++) { - if (CavaService.values.length <= i) { - newHeights.push(root.minBarHeight); - continue; - } - - const rawLevel = CavaService.values[i]; - if (rawLevel <= 0) { - newHeights.push(root.minBarHeight); - } else if (rawLevel >= 100) { - newHeights.push(root.maxBarHeight); - } else { - newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange); - } - } - root.barHeights = newHeights; + const n = i => { + const x = v[i]; + return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01); + }; + bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3)); + bars.bandsB = Qt.vector2d(n(4), n(5)); } } - Row { - anchors.centerIn: parent - spacing: 1.5 + ShaderEffect { + id: bars + anchors.fill: parent - Repeater { - model: 6 + property real widthPx: width + property real heightPx: height + property real minH: root.minBarHeight + property real maxH: root.maxBarHeight + property vector4d bandsA: Qt.vector4d(0, 0, 0, 0) + property vector2d bandsB: Qt.vector2d(0, 0) + property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a) - Rectangle { - width: 2 - height: root.barHeights[index] - radius: 1.5 - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - - Behavior on height { - enabled: root.isPlaying && !CavaService.cavaAvailable - NumberAnimation { - duration: 100 - easing.type: Easing.Linear - } - } - } - } + fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb") } } diff --git a/quickshell/Modules/DankBar/Widgets/FocusedApp.qml b/quickshell/Modules/DankBar/Widgets/FocusedApp.qml index dc7bfa991..9a31d3f27 100644 --- a/quickshell/Modules/DankBar/Widgets/FocusedApp.qml +++ b/quickshell/Modules/DankBar/Widgets/FocusedApp.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Effects -import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Widgets @@ -234,16 +233,19 @@ BasePill { color: Theme.widgetTextColor } - RowLayout { + Row { id: contentRow anchors.centerIn: parent spacing: Theme.spacingS visible: !root.isVerticalOrientation + readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + IconImage { id: horizontalAppIcon - Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - Layout.preferredHeight: Layout.preferredWidth + width: contentRow.iconSize + height: contentRow.iconSize + anchors.verticalCenter: parent.verticalCenter visible: root.showIcon && activeWindow && status === Image.Ready source: { if (!activeWindow || !activeWindow.appId) @@ -264,8 +266,10 @@ BasePill { } DankIcon { - Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - size: Layout.preferredWidth + id: horizontalSteamIcon + width: contentRow.iconSize + size: contentRow.iconSize + anchors.verticalCenter: parent.verticalCenter name: "sports_esports" color: Theme.widgetTextColor visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId) @@ -280,9 +284,11 @@ BasePill { } font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + wrapMode: Text.NoWrap elide: Text.ElideRight maximumLineCount: 1 - Layout.maximumWidth: compactMode ? 80 : 180 + width: Math.min(implicitWidth, compactMode ? 80 : 180) visible: text.length > 0 } @@ -291,6 +297,7 @@ BasePill { text: compactMode ? "" : "•" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter visible: !compactMode && appText.text && titleText.text } @@ -318,9 +325,24 @@ BasePill { } font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + wrapMode: Text.NoWrap elide: Text.ElideRight maximumLineCount: 1 - Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth + width: { + const sp = contentRow.spacing; + let used = 0; + if (horizontalAppIcon.visible) + used += horizontalAppIcon.width + sp; + else if (horizontalSteamIcon.visible) + used += horizontalSteamIcon.width + sp; + if (appText.visible) + used += appText.width + sp; + if (appSeparator.visible) + used += appSeparator.width + sp; + const budget = root.maxWidth - root.horizontalPadding * 2 - used; + return Math.min(implicitWidth, Math.max(0, budget)); + } visible: text.length > 0 } } diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index 79b62f28a..5319fc12a 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -411,6 +411,7 @@ DankPopout { anchors.fill: parent active: root.currentTabId === "media" visible: active + asynchronous: true sourceComponent: Component { MediaPlayerTab { targetScreen: root.screen @@ -462,7 +463,7 @@ DankPopout { DankSpinner { anchors.centerIn: parent size: 40 - visible: wallpaperLoader.active && wallpaperLoader.status === Loader.Loading + visible: (wallpaperLoader.active && wallpaperLoader.status === Loader.Loading) || (mediaLoader.active && mediaLoader.status === Loader.Loading) || (weatherLoader.active && weatherLoader.status === Loader.Loading) } Loader { @@ -470,6 +471,7 @@ DankPopout { anchors.fill: parent active: root.currentTabId === "weather" visible: active + asynchronous: true sourceComponent: Component { WeatherTab {} } diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 2f29fff5a..2293dd55f 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -44,6 +44,8 @@ Item { property int __panelHoverCount: 0 + readonly property bool overlayBlurActive: dropdownBlur.active + onDropdownTypeChanged: { if (dropdownType === 0) { __panelHoverCount = 0; @@ -75,8 +77,10 @@ Item { } WindowBlur { + id: dropdownBlur targetWindow: root.targetWindow readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0 + blurEnabled: active && Theme.connectedSurfaceBlurEnabled readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1 blurX: root.__activePanel ? root.__activePanel.x + root.__activePanel.width * (1 - s) * 0.5 : 0 blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0 diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index 2f857701d..0a57f8746 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Effects import QtQuick.Layouts import Quickshell.Services.Mpris +import Quickshell.Widgets import qs.Common import qs.Services import qs.Widgets @@ -70,7 +71,7 @@ Item { property bool _switchHold: false Timer { id: _switchHoldTimer - interval: 650 + interval: 1500 repeat: false onTriggered: _switchHold = false } @@ -78,7 +79,8 @@ Item { onActivePlayerChanged: { if (!activePlayer) { isSwitching = false; - _switchHold = false; + _switchHold = true; + _switchHoldTimer.restart(); return; } isSwitching = true; @@ -117,14 +119,10 @@ Item { Connections { target: MprisController function onAvailablePlayersChanged() { - const count = (MprisController.availablePlayers?.length || 0); - if (count === 0) { + if ((MprisController.availablePlayers?.length || 0) === 0) isSwitching = false; - _switchHold = false; - } else { - _switchHold = true; - _switchHoldTimer.restart(); - } + _switchHold = true; + _switchHoldTimer.restart(); } } @@ -290,34 +288,64 @@ Item { Item { id: bgContainer anchors.fill: parent - visible: TrackArtService.resolvedArtUrl !== "" - Image { - id: bgImage - anchors.centerIn: parent - width: Math.max(parent.width, parent.height) * 1.1 - height: width - source: TrackArtService.resolvedArtUrl - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - visible: false - onStatusChanged: { - if (status === Image.Ready) - maybeFinishSwitch(); - } + readonly property string curArt: TrackArtService.resolvedArtUrl + // Two layers crossfade: new art loads into the hidden one and fades in once decoded. + property bool _showA: true + visible: layerA.ready || layerB.ready + + onCurArtChanged: syncArt() + Component.onCompleted: syncArt() + + function syncArt() { + if (curArt === "" || layerA.art == curArt || layerB.art == curArt) + return; + if (_showA) + layerB.art = curArt; + else + layerA.art = curArt; } - Item { - id: blurredBg + component BgBlurLayer: ClippingRectangle { + id: layer + property alias art: layerImg.source + readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != "" + property bool front: false + signal loaded + anchors.fill: parent - visible: false + radius: Theme.cornerRadius + color: "transparent" + antialiasing: true + opacity: front ? 0.7 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 350 + easing.type: Easing.InOutQuad + } + } + + Image { + id: layerImg + anchors.centerIn: parent + width: Math.max(parent.width, parent.height) * 1.1 + height: width + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + visible: false + onStatusChanged: { + if (status === Image.Ready && source != "") + layer.loaded(); + } + } MultiEffect { anchors.centerIn: parent - width: bgImage.width - height: bgImage.height - source: bgImage + width: layerImg.width + height: layerImg.height + source: layerImg blurEnabled: true blurMax: 64 blur: 0.8 @@ -326,22 +354,26 @@ Item { } } - Rectangle { - id: bgMask - anchors.fill: parent - radius: Theme.cornerRadius - visible: false - layer.enabled: true + BgBlurLayer { + id: layerA + front: bgContainer._showA + onLoaded: { + if (!bgContainer._showA) { + bgContainer._showA = true; + root.maybeFinishSwitch(); + } + } } - MultiEffect { - anchors.fill: parent - source: blurredBg - maskEnabled: true - maskSource: bgMask - maskThresholdMin: 0.5 - maskSpreadAtMin: 1.0 - opacity: 0.7 + BgBlurLayer { + id: layerB + front: !bgContainer._showA + onLoaded: { + if (bgContainer._showA) { + bgContainer._showA = false; + root.maybeFinishSwitch(); + } + } } Rectangle { @@ -442,7 +474,8 @@ Item { elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 1 - visible: text.length > 0 + // Reserve the line so late album metadata doesn't shift the seekbar. + height: Math.max(implicitHeight, Theme.fontSizeSmall * 1.4) } } diff --git a/quickshell/Modules/DankDash/OverviewTab.qml b/quickshell/Modules/DankDash/OverviewTab.qml index 94976d86e..2d545f283 100644 --- a/quickshell/Modules/DankDash/OverviewTab.qml +++ b/quickshell/Modules/DankDash/OverviewTab.qml @@ -1,6 +1,7 @@ import QtQuick import qs.Common import qs.Modules.DankDash.Overview +import qs.Widgets Item { id: root @@ -17,7 +18,7 @@ Item { signal navFocusRequested function handleKeyEvent(event) { - return calendarCard.handleKeyEvent(event); + return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false; } Item { @@ -57,16 +58,26 @@ Item { height: 220 } - // Calendar - bottom middle (wider and taller) - CalendarOverviewCard { - id: calendarCard + // Calendar - bottom middle; deferred so the grid stays off the emerge frame. + Loader { + id: calendarLoader x: parent.width * 0.2 - Theme.spacingM y: 100 + Theme.spacingM width: parent.width * 0.6 height: 300 + asynchronous: true + sourceComponent: Component { + CalendarOverviewCard { + onCloseDash: root.closeDash() + onNavFocusRequested: root.navFocusRequested() + } + } - onCloseDash: root.closeDash() - onNavFocusRequested: root.navFocusRequested() + DankSpinner { + anchors.centerIn: parent + size: 32 + visible: calendarLoader.status === Loader.Loading + } } // Media - bottom right (narrow and taller) diff --git a/quickshell/Modules/DankDash/WeatherTab.qml b/quickshell/Modules/DankDash/WeatherTab.qml index 1f920d534..6b673e446 100644 --- a/quickshell/Modules/DankDash/WeatherTab.qml +++ b/quickshell/Modules/DankDash/WeatherTab.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import QtQuick.Shapes import qs.Common import qs.Services @@ -245,17 +244,6 @@ Item { size: Theme.iconSize * 2 color: Theme.primary anchors.verticalCenter: parent.verticalCenter - - layer.enabled: Theme.elevationEnabled - layer.effect: MultiEffect { - shadowEnabled: Theme.elevationEnabled - shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1) - shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1) - shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0 - blurMax: Theme.elevationBlurMax - shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1) - shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 - } } Column { @@ -820,16 +808,6 @@ Item { property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false) x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin - - layer.enabled: Theme.elevationEnabled - layer.effect: MultiEffect { - shadowEnabled: Theme.elevationEnabled - shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2) - shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4) - shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0 - blurMax: Theme.elevationBlurMax - shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2) - } } DankIcon { @@ -842,16 +820,6 @@ Item { property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true) x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin - - layer.enabled: Theme.elevationEnabled - layer.effect: MultiEffect { - shadowEnabled: Theme.elevationEnabled - shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2) - shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4) - shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0 - blurMax: Theme.elevationBlurMax - shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2) - } } } } @@ -993,6 +961,7 @@ Item { ListView { id: hourlyList anchors.fill: parent + reuseItems: true orientation: ListView.Horizontal spacing: Theme.spacingS clip: true @@ -1079,6 +1048,7 @@ Item { ListView { id: dailyList anchors.fill: parent + reuseItems: true orientation: ListView.Horizontal spacing: Theme.spacingS clip: true diff --git a/quickshell/Services/ClipboardService.qml b/quickshell/Services/ClipboardService.qml index c8af91e84..2738d5e43 100644 --- a/quickshell/Services/ClipboardService.qml +++ b/quickshell/Services/ClipboardService.qml @@ -84,31 +84,30 @@ Singleton { } function updateFilteredModel() { - let filtered = internalEntries; + const query = searchText.trim().toLowerCase(); + const filterAll = activeFilter === "all"; + const unpinned = []; + const pinned = []; - if (activeFilter !== "all") { - filtered = filtered.filter(entry => getEntryType(entry) === activeFilter); + for (let i = 0; i < internalEntries.length; i++) { + const entry = internalEntries[i]; + if (!filterAll && getEntryType(entry) !== activeFilter) + continue; + if (query.length > 0 && !entry.preview.toLowerCase().includes(query)) + continue; + (entry.pinned ? pinned : unpinned).push(entry); } - const query = searchText.trim(); + const byIdDesc = (a, b) => b.id - a.id; + pinned.sort(byIdDesc); + unpinned.sort(byIdDesc); - if (query.length > 0) { - const lowerQuery = query.toLowerCase(); - filtered = filtered.filter(entry => entry.preview.toLowerCase().includes(lowerQuery)); - } - - filtered.sort((a, b) => { - if (a.pinned !== b.pinned) - return b.pinned ? 1 : -1; - return b.id - a.id; - }); - - clipboardEntries = filtered; - unpinnedEntries = filtered.filter(e => !e.pinned); - pinnedEntries = filtered.filter(e => e.pinned); + pinnedEntries = pinned; + unpinnedEntries = unpinned; + clipboardEntries = pinned.concat(unpinned); totalCount = clipboardEntries.length; - const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length); + const activeCount = Math.max(unpinned.length, pinned.length); if (activeCount === 0) { keyboardNavigationActive = false; @@ -116,9 +115,8 @@ Singleton { return; } - if (selectedIndex >= activeCount) { + if (selectedIndex >= activeCount) selectedIndex = activeCount - 1; - } } function refresh() { diff --git a/quickshell/Services/TrackArtService.qml b/quickshell/Services/TrackArtService.qml index 81c0b9c44..0d6f8e3fc 100644 --- a/quickshell/Services/TrackArtService.qml +++ b/quickshell/Services/TrackArtService.qml @@ -56,18 +56,19 @@ Singleton { function loadArtwork(url) { if (!url || url === "") { - resolvedArtUrl = ""; + // Keep stale art; only blank once the empty url debounce settles. _lastArtUrl = ""; loading = false; + _clearDebounce.restart(); return; } + _clearDebounce.stop(); if (url === _lastArtUrl) return; _lastArtUrl = url; if (url.startsWith("http://") || url.startsWith("https://")) { loading = true; - resolvedArtUrl = ""; // Clear stale artwork immediately while loading const targetUrl = url; const hash = djb2Hash(url); const cacheDir = Paths.strip(Paths.imagecache); @@ -134,19 +135,30 @@ Singleton { } loading = true; - resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file const localUrl = url; const filePath = url.startsWith("file://") ? url.substring(7) : url; - Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => { + // Cover file often lands after the metadata update, so poll briefly. + Proc.runCommand(null, ["sh", "-c", "for i in $(seq 20); do [ -f \"$1\" ] && exit 0; sleep 0.15; done; exit 1", "sh", filePath], (output, exitCode) => { if (_lastArtUrl !== localUrl) return; resolvedArtUrl = exitCode === 0 ? localUrl : ""; loading = false; - }, 200); + }, 50, 5000); + } + + Timer { + id: _clearDebounce + interval: 800 + onTriggered: { + if (root._lastArtUrl === "") + root.resolvedArtUrl = ""; + } } property MprisPlayer activePlayer: MprisController.activePlayer + property string _resolvedTrackKey: "" + onActivePlayerChanged: _updateArtUrl() Connections { @@ -157,8 +169,21 @@ Singleton { function onMetadataChanged() { root._updateArtUrl(); } } + function _trackKey() { + const p = activePlayer; + if (!p) + return ""; + return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || ""); + } + function _updateArtUrl() { const url = getArtworkUrl(activePlayer); + const key = _trackKey(); + // Ignore metadata jitter once resolved, but only when the url still matches + // (trackArtUrl can update before the title, and skipping then wedges old art). + if (key !== "" && key === _resolvedTrackKey && url === _lastArtUrl && resolvedArtUrl !== "") + return; + _resolvedTrackKey = key; loadArtwork(url); } } diff --git a/quickshell/Shaders/frag/blob.frag b/quickshell/Shaders/frag/blob.frag new file mode 100644 index 000000000..0505db0da --- /dev/null +++ b/quickshell/Shaders/frag/blob.frag @@ -0,0 +1,87 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float phase; + float spin; + float sizePx; + float baseRadiusPx; + float amplitudePx; + float activation; + float energy; + vec2 bandsB; + vec4 bandsA; + vec4 fillColor; +} ubuf; + +const float TAU = 6.28318530718; +const float NOISE_FREQ = 1.25; +const float ORBIT_RADIUS = 1.0; +const float WARP_STRENGTH = 0.75; +const float RING_BIAS = 0.25; +const float MID_FREQ = 2.4; +const float MID_GAIN = 0.34; +const float HIGH_FREQ = 5.0; +const float HIGH_GAIN = 0.16; +const float LOBE_MAX = 1.3; + +float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float vnoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float fbm(vec2 p) { + return 0.55 * vnoise(p) + 0.3 * vnoise(p * 2.0 + vec2(17.1, 9.2)) + 0.15 * vnoise(p * 3.9 + vec2(4.3, 21.7)); +} + +void main() { + vec2 p = (qt_TexCoord0 - 0.5) * ubuf.sizePx; + float r = length(p); + float aa = max(fwidth(r), 0.75); + + float T = ubuf.phase * TAU; + vec2 dir = r > 0.001 ? p / r : vec2(1.0, 0.0); + float cs = cos(ubuf.spin); + float sn = sin(ubuf.spin); + dir = vec2(dir.x * cs - dir.y * sn, dir.x * sn + dir.y * cs); + vec2 q = dir * NOISE_FREQ; + + vec2 orbitA = vec2(cos(T), sin(T)) * ORBIT_RADIUS; + vec2 orbitB = vec2(cos(2.0 * T + 2.1), sin(2.0 * T + 2.1)) * (ORBIT_RADIUS * 0.6); + float w1 = fbm(q + orbitA); + float w2 = fbm(q * 1.3 + orbitB + vec2(3.7, 7.3)); + float n = fbm(q + orbitA + WARP_STRENGTH * (vec2(w1, w2) - 0.5)); + + float mid = max(ubuf.bandsA.z, ubuf.bandsA.w); + float high = max(ubuf.bandsB.x, ubuf.bandsB.y); + + vec2 orbitM = vec2(cos(3.0 * T + 0.7), sin(3.0 * T + 0.7)) * 0.9; + vec2 orbitH = vec2(cos(5.0 * T + 4.2), sin(5.0 * T + 4.2)) * 1.2; + float midTerm = (vnoise(dir * MID_FREQ + orbitM) - 0.5) * MID_GAIN * mid; + float highTerm = (vnoise(dir * HIGH_FREQ + orbitH) - 0.5) * HIGH_GAIN * high; + + float shaped = smoothstep(0.34, 0.7, n); + float body = ubuf.energy * (RING_BIAS + (1.0 - RING_BIAS) * shaped); + float lobe = clamp(body + midTerm + highTerm, 0.0, LOBE_MAX); + float blobR = ubuf.baseRadiusPx + ubuf.activation * ubuf.amplitudePx * lobe; + + float mask = 1.0 - smoothstep(-aa, aa, r - blobR); + float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity; + fragColor = vec4(ubuf.fillColor.rgb * a, a); +} diff --git a/quickshell/Shaders/frag/viz_bars.frag b/quickshell/Shaders/frag/viz_bars.frag new file mode 100644 index 000000000..6cd56ddce --- /dev/null +++ b/quickshell/Shaders/frag/viz_bars.frag @@ -0,0 +1,50 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float widthPx; + float heightPx; + float minH; + float maxH; + vec2 bandsB; + vec4 bandsA; + vec4 fillColor; +} ubuf; + +const float BAR_W = 2.0; +const float GAP = 1.5; +const float RADIUS = 1.0; + +float sdRoundBar(vec2 p, vec2 halfSize, float r) { + vec2 q = abs(p) - halfSize + vec2(r); + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; +} + +void main() { + vec2 px = vec2(qt_TexCoord0.x * ubuf.widthPx, qt_TexCoord0.y * ubuf.heightPx); + float total = 6.0 * BAR_W + 5.0 * GAP; + float x0 = (ubuf.widthPx - total) * 0.5; + float slot = BAR_W + GAP; + float fi = floor((px.x - x0) / slot); + + if (fi < 0.0 || fi > 5.0) { + fragColor = vec4(0.0); + return; + } + + int i = int(fi); + float lvl = i == 0 ? ubuf.bandsA.x : i == 1 ? ubuf.bandsA.y : i == 2 ? ubuf.bandsA.z : i == 3 ? ubuf.bandsA.w : i == 4 ? ubuf.bandsB.x : ubuf.bandsB.y; + float h = ubuf.minH + clamp(lvl, 0.0, 1.0) * (ubuf.maxH - ubuf.minH); + + float lx = px.x - x0 - fi * slot; + vec2 p = vec2(lx - BAR_W * 0.5, px.y - ubuf.heightPx * 0.5); + float d = sdRoundBar(p, vec2(BAR_W * 0.5, h * 0.5), RADIUS); + float mask = 1.0 - smoothstep(-0.6, 0.6, d); + + float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity; + fragColor = vec4(ubuf.fillColor.rgb * a, a); +} diff --git a/quickshell/Shaders/qsb/blob.frag.qsb b/quickshell/Shaders/qsb/blob.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..0e2f5945c722b0de4125df5c1c66e07cf9459e77 GIT binary patch literal 5825 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 Lp8WhTA1d3_aa7~i literal 0 HcmV?d00001 diff --git a/quickshell/Shaders/qsb/viz_bars.frag.qsb b/quickshell/Shaders/qsb/viz_bars.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..9db8a7c7ff3141645643b54627b89b3a87292ca9 GIT binary patch literal 3625 zcmV+^4%YDi07oKtob6nTn;TUXzumUG%eG3Pyo!QDD>Z>_NcPd~Yx@RDZE0yMRBM?| zH;)~fOwwf1%@!7l2o%N2Ly-a^BBCOSf+(U0h#(@Og7`$l*WaM>eLl{)_ji*!lVnTf zqaTHYZ<;y3bM866`=$b&+31X-4g)KITl&FXmB2WJmMN-7c*G8B4 zFFvBdhZ7tv($v5a8QC~1o2&E(Y}ziii!wDAqLaS${T?Ig2w6ZeTuViKO70R*A*LW= zsS!O*-yDM`>qLPX4N((cjH2jL%`W_Trx>BYB!v!(^|A(suWS}RjlxpYAVc52(O^}x z2Av{D;hqfOz%}CqI+RZbu5T7NmB!4Wj7d=v%Y~Y+%`(9Of|nuVHen2!`Qb@*X)?2Q zlUiujd(Fl*v67#R_4V_Dr8%4^ zXiCdqmoxY#}d_w6!IyXeAwCocA%PH~LQK;2`> zJ_$7h{3jW_w0#Q3n(K7qo@*_HJ;(6Jgx-A=?*#67yicG%d+0j>`(#>aJa#>Z311zE zO(1t4`rnJ%Z1)4>zRX2mCV)HJ1IC!m!+pEqUySvk$7ZGM`EARzy;=>$5^b-Q=C-BB zP^Jrvb{~@X9ZR{6jhqSaWP2O1Zt4}|4(7qUxa|=7J=fsq8uP)nhZt>_j6U;x?Lj}a zeT~saj^Bsz*Y=IZnDF{+JBc&&xZE@gsxlm&v;bZ&nUYZ|8t?%9MAu6kb5Hf z_gipF82SFHy0=u@f0O28OXgD5Hc{K2ImbT4EvrNDLW zWoXCvdcv6ha@1TV+fJ10Vma=4O{_F(oYjE;*}%2E3Tx>K)I0|I!|=p_lNqv z*NDAEsIy4V*)5?tyUd8aRj6}JueDo4Yi-1cy-lbyP(LHLiQqGmGh#n0)cMJ=8Q6MH z{v5_*pAmltaBY8HsPlL#?QCp+L1^8he7twvY2^GOa^7X+xEr{R`BH;&bQR}(0yf`2iFm$$3hm}o z3(oyx!M}f%-r~5Y5cep2n$u5((r9{(J}cB3WxdO~`5DCMe*X-0o<+W=f$4qX7Z^|0 z)aJhTOZc??yinRskHrg!dES_tUm;h?X!jy;-R{?D`y%)}2aI#DcK@BhXAr;deh+*d z^_lhlAAs>C(Em20=1W59&V7`VbKTk z(Eb(pUluhT!amj^^XQt39PdOO{tZ1qajc`J823|g&(Fj=gxcqh6g3Ub?=3n+ZM_|Z z_N>p?J{p`S!KdS4Qx-l-tqNZNo#{uK(a$v`!?Q!VmG+?^U3E-%6oG5C) zL8lVJ*t@_ng7_0fO<%qnzU7G1`@?&X*WBMM;5ud|V$3~c7VxQf*4}H>w&2&kQ{g)m zaWh11|D6qPry9B6hw;+STLf{8|A*#{?Rhezn1uIOY$WkbpUzij?Mf9(Zr-yJ@pxCE zm>P1WHSA`F3syRtcl?EQuZ0wPIq10Wzi?+_k7`0>yMh~h@13>*9QWJQ&hkYVma4a6|kKO>|Lp$9&5YfB}>bzK%WAZ89>_2W>@61 z`QpN^G2&hJ+;qN}W8zks{vc?VQyg`@j7mskH>D~o^7&#iPCiw%l-!Und&%WaF(QKs z-70w4$)-2DyHnjDSBzpXR4QxGia6U#kwVm(Q;m&Ur%QLF7*#BbDZgCwtVlNHW&Gg? zbuhA1<&VT;@o1DA&?qQJtkP}mjEzRSE$O4Y+%M`w-c4FO%aH(uLWMZURA#?o73(zF z`4}dlJJ9XesCMfKiJCLu=Y1zjX%q8f{U%wBq5S97C<;K&aIitC;Ci*-|Rh zL?=%S)Ob}3B%)QU)R0$L<8GGmbY1R$C@KIrPIuPOYO&c zJI-=sg3C~=QH~F6xN5_U2AVoWCr8Z-Va!hGq%z+y+M4`STn}&BvAF3-5@Z~1GOk;v z)i*tXTsfz-LnR{CrCdJm510H@Au=Z(n;Vy(p5A$ViFy44^Aqtz|JZkL$nkwpvx0Fsx>9mwi zOX;+fj!C$sbgY)rX(^qO(XX zNQZx2Xm#y`V|Ulb$~7O38)*>7%RU$vSXn(POzjilz+mA%BXn6PbZ&#d>imUb2ck;S zkrs7)cS!RH$~e;()N)X;8`qz+;e2~VF<&a#=Xiy(kLhiYjp}bYbMuOI>o%;6tI`c} ztSkGWBt}`vun;u%s1V8BbNx?v|#IaWrK*EGoGx zc0gHbz7-|b=)SSKw8vMM_5sypy0N^p8_UZ;bWL9d;DZ*J>7c?)k1H`Tx)tM`ej_EO zeW)a+{brDu_M2T|+K02mv=6<+wBHUA({3y=(^6xmt0L1jM5g`!iA=pK*EVG>G}A$) zX$O^NIvPq46q_b0Mb5G43Ggyj(seRkzQhvErJG_ZY0aE-LC!6eTrV@HP|VYj zK3YhXW_ESWa=oE!IhnE+X=O zmM!HH{oKjshdi^n6w|Jkb-k2j+m278wY}`8Y&#NhvLnuD$yQDhMs>p0A1XGBgr8ryeEEJ%Ab;>D=R^P1ajC>7R7mo$^VncYlUhDvGcEb^G znU}ly@J^*R;zqA{Ikxqh_w{S=)@$C@Yu>|8gI8bks$KmJe%aexhrgy5z6Y(s-;S?+ vn{55#z5L~7=3$^-22YkpE#3x)-U#de5JLSgXX=%0onK?&IQjV>+9Cb{imyk6 literal 0 HcmV?d00001 diff --git a/quickshell/Widgets/DankAlbumArt.qml b/quickshell/Widgets/DankAlbumArt.qml index 0a332625a..b01c13e7b 100644 --- a/quickshell/Widgets/DankAlbumArt.qml +++ b/quickshell/Widgets/DankAlbumArt.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Shapes import Quickshell.Services.Mpris import qs.Common import qs.Services @@ -15,6 +14,36 @@ Item { property bool showAnimation: true property real animationScale: 1.0 + readonly property real blobBaseRadiusFactor: 0.43 + readonly property real blobAmplitudeFactor: 0.115 + readonly property real blobOvershoot: 1.15 + readonly property real blobEnergySensitivity: 1.15 + readonly property real cavaFullScale: 45 + readonly property real blobAttack: 0.75 + readonly property real blobRelease: 0.2 + readonly property real blobBeatBoost: 2.5 + readonly property real blobBeatKick: 4 + readonly property real blobOnsetThreshold: 1.4 + readonly property real blobSpringStiffness: 220 + readonly property real blobSpringDamping: 19 + readonly property real blobMorphSpeed: 0.05 + readonly property real blobMorphBoost: 1.7 + readonly property real blobSpinSpeed: 0.03 + + readonly property bool blobActive: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation && albumArtStatus === Image.Ready + property var smoothedBands: [0, 0, 0, 0, 0, 0] + property var slowBands: [0, 0, 0, 0, 0, 0] + property var bandTargets: [0, 0, 0, 0, 0, 0] + property var bandDisplay: [0, 0, 0, 0, 0, 0] + property var prevLevels: [0, 0, 0, 0, 0, 0] + readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35] + property real fluxAvg: 0.02 + property real loudCtx: 0.1 + property int beatCooldown: 0 + property real energyTarget: 0 + property real energyPos: 0 + property real energyVel: 0 + onActivePlayerChanged: { lastValidArtUrl = ""; } @@ -25,6 +54,62 @@ Item { } } + function updateBands() { + const vals = CavaService.values; + if (!vals || vals.length < 6) + return; + + const s = smoothedBands; + const slow = slowBands; + const out = bandTargets; + const prev = prevLevels; + const w = fluxWeights; + let flux = 0; + for (let i = 0; i < 6; i++) { + const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale; + flux += Math.max(0, level - prev[i]) * w[i]; + prev[i] = level; + const alpha = level > s[i] ? blobAttack : blobRelease; + s[i] += alpha * (level - s[i]); + slow[i] += 0.05 * (level - slow[i]); + const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost; + out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity); + } + + const ratio = flux / Math.max(fluxAvg, 0.004); + fluxAvg += 0.06 * (flux - fluxAvg); + if (beatCooldown > 0) { + beatCooldown--; + } else if (ratio > blobOnsetThreshold && flux > 0.008) { + energyVel += blobBeatKick * Math.min(2.5, ratio - 1); + beatCooldown = 3; + } + + const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]); + loudCtx += 0.03 * (loud - loudCtx); + const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1); + energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge)); + } + + function stepBlob(dt) { + energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt; + energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt)); + blobEffect.energy = energyPos; + + const d = bandDisplay; + const t = bandTargets; + const f = Math.min(1, dt * 14); + for (let i = 0; i < 6; i++) { + d[i] += f * (t[i] - d[i]); + } + blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]); + blobEffect.bandsB = Qt.vector2d(d[4], d[5]); + + const speed = 1 + energyPos * blobMorphBoost; + blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1; + blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718; + } + Loader { active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation sourceComponent: Component { @@ -34,119 +119,49 @@ Item { } } - Shape { - id: morphingBlob - width: parent.width * 1.1 - height: parent.height * 1.1 + Connections { + target: CavaService + enabled: blobEffect.visible + function onValuesChanged() { + root.updateBands(); + } + } + + FrameAnimation { + running: blobEffect.visible + onTriggered: root.stepBlob(Math.min(frameTime, 0.05)) + } + + ShaderEffect { + id: blobEffect + + readonly property real span: Math.min(root.width, root.height) + + width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4 + height: width anchors.centerIn: parent - visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation - asynchronous: false - antialiasing: true - preferredRendererType: Shape.CurveRenderer z: 0 - layer.enabled: false + visible: root.blobActive || activation > 0.004 - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 - readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale - readonly property int segments: 28 + property real phase: 0 + property real spin: 0 + property real sizePx: width + property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale + property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale + property real activation: root.blobActive ? 1 : 0 + property real energy: 0 + property vector4d bandsA: Qt.vector4d(0, 0, 0, 0) + property vector2d bandsB: Qt.vector2d(0, 0) + property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a) - property var audioLevels: { - if (!CavaService.cavaAvailable || CavaService.values.length === 0) { - return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]; - } - return CavaService.values; - } - - property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6] - property var cubics: [] - - Connections { - target: CavaService - function onValuesChanged() { - if (morphingBlob.visible) { - morphingBlob.updatePath(); - } + Behavior on activation { + NumberAnimation { + duration: 550 + easing.type: Easing.InOutQuad } } - Component { - id: cubicSegment - PathCubic {} - } - - Component { - id: pathMoveComp - PathMove {} - } - - Component.onCompleted: { - shapePath.pathElements.push(pathMoveComp.createObject(shapePath)); - - for (let i = 0; i < segments; i++) { - const seg = cubicSegment.createObject(shapePath); - shapePath.pathElements.push(seg); - cubics.push(seg); - } - - updatePath(); - } - - function updatePath() { - if (cubics.length === 0) - return; - - const alpha = 0.35; - const minLen = Math.min(smoothedLevels.length, audioLevels.length); - for (let i = 0; i < minLen; i++) { - smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]); - } - - const angleStep = 2 * Math.PI / segments; - const tension3 = 0.16666667; - const startMove = shapePath.pathElements[0]; - - const points = new Array(segments); - for (let i = 0; i < segments; i++) { - const angle = i * angleStep; - const audioIndex = i % 10; - const rawLevel = smoothedLevels[audioIndex] || 0; - const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel); - const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5; - const radius = baseRadius * (1.0 + audioLevel); - points[i] = { - x: centerX + Math.cos(angle) * radius, - y: centerY + Math.sin(angle) * radius - }; - } - - startMove.x = points[0].x; - startMove.y = points[0].y; - - for (let i = 0; i < segments; i++) { - const p0 = points[(i + segments - 1) % segments]; - const p1 = points[i]; - const p2 = points[(i + 1) % segments]; - const p3 = points[(i + 2) % segments]; - - const seg = cubics[i]; - seg.control1X = p1.x + (p2.x - p0.x) * tension3; - seg.control1Y = p1.y + (p2.y - p0.y) * tension3; - seg.control2X = p2.x - (p3.x - p1.x) * tension3; - seg.control2Y = p2.y - (p3.y - p1.y) * tension3; - seg.x = p2.x; - seg.y = p2.y; - } - } - - ShapePath { - id: shapePath - fillColor: Theme.primary - strokeColor: "transparent" - strokeWidth: 0 - joinStyle: ShapePath.RoundJoin - fillRule: ShapePath.WindingFill - } + fragmentShader: Qt.resolvedUrl("../Shaders/qsb/blob.frag.qsb") } DankCircularImage { diff --git a/quickshell/Widgets/DankCircularImage.qml b/quickshell/Widgets/DankCircularImage.qml index c6d75dce1..09805c33b 100644 --- a/quickshell/Widgets/DankCircularImage.qml +++ b/quickshell/Widgets/DankCircularImage.qml @@ -13,9 +13,17 @@ Rectangle { property bool cacheImages: true property bool hasImage: imageSource !== "" readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://") - // Probe with AnimatedImage first; once loaded, check frameCount to decide. readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1 - readonly property var activeImage: isAnimated ? probe : staticImage + readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error + readonly property var activeImage: { + if (isAnimated) + return probe; + if (staticImage.status === Image.Ready) + return staticImage; + if (probe.status === Image.Ready && probe.source !== "") + return probe; + return staticImage; + } property int imageStatus: activeImage.status signal imageSaved(string filePath) @@ -57,7 +65,7 @@ Rectangle { border.color: "transparent" border.width: 0 - // Probe: loads as AnimatedImage to detect frame count. + // Probes as AnimatedImage to read frameCount; retires once staticImage is ready. AnimatedImage { id: probe anchors.fill: parent @@ -68,10 +76,10 @@ Rectangle { mipmap: true cache: root.cacheImages visible: false - source: root.shouldProbe ? root.imageSource : "" + source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : "" } - // Static fallback: used once probe confirms the image is not animated. + // Takes over once the probe settles on a non-animated image, then latches. Image { id: staticImage anchors.fill: parent @@ -84,38 +92,12 @@ Rectangle { visible: false sourceSize.width: Math.max(width * 2, 128) sourceSize.height: Math.max(height * 2, 128) - source: !root.shouldProbe ? root.imageSource : "" - } - - // Once the probe loads, if not animated, hand off to Image and unload probe. - Connections { - target: probe - function onStatusChanged() { + source: { if (!root.shouldProbe) - return; - switch (probe.status) { - case Image.Ready: - if (probe.frameCount <= 1) { - staticImage.source = root.imageSource; - probe.source = ""; - } - break; - case Image.Error: - staticImage.source = root.imageSource; - probe.source = ""; - break; - } - } - } - - // If imageSource changes, reset: re-probe with AnimatedImage. - onImageSourceChanged: { - if (root.shouldProbe) { - staticImage.source = ""; - probe.source = root.imageSource; - } else { - probe.source = ""; - staticImage.source = root.imageSource; + return root.imageSource; + if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null) + return root.imageSource; + return ""; } } diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index cd7f1bb3e..9bdbc5b54 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -1249,14 +1249,14 @@ Item { // Fast fade duration for superseded close. readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)) - property real publishedOpacity: _targetOpacity + readonly property real publishedOpacity: opacity opacity: _targetOpacity visible: _renderActive scale: contentContainer.scaleValue - x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr) - y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr) + x: Theme.snap(contentContainer.animX, root.dpr) + y: Theme.snap(contentContainer.animY, root.dpr) layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1) layer.smooth: false @@ -1276,15 +1276,6 @@ Item { } } - Behavior on publishedOpacity { - enabled: contentWrapper._fadeWithOpacity - NumberAnimation { - duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) - easing.type: Easing.BezierSpline - easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve - } - } - Connections { target: root function onShouldBeVisibleChanged() { diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index 919101df4..da90df263 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -220,6 +220,31 @@ Item { onTriggered: root._bgCommitWindow = false } + // An idle layer surface won't commit the cleared blur region on auto-close, so the + // blur sticks; pulse updatesEnabled false->true to force a re-commit. + property bool _blurCommitSuppress: false + + Timer { + id: blurCommitPulseTimer + interval: 16 + onTriggered: root._blurCommitSuppress = false + } + + function _pulseBlurCommit() { + if (!backgroundWindow.visible) + return; + _blurCommitSuppress = true; + blurCommitPulseTimer.restart(); + } + + Connections { + target: overlayLoader.item + ignoreUnknownSignals: true + function onOverlayBlurActiveChanged() { + root._pulseBlurCommit(); + } + } + function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) { const newX = Theme.snap(bodyX, dpr); const newY = Theme.snap(bodyY, dpr); @@ -540,7 +565,7 @@ Item { // Skip buffer updates when there's nothing to render. Briefly flipped // true via _bgCommitWindow when _surfaceBodyW/H changes so the // contentHoleRect mask carve-out actually commits to the compositor. - updatesEnabled: root.overlayContent !== null || root._bgCommitWindow + updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow) WlrLayershell.namespace: root.layerNamespace + ":background" WlrLayershell.layer: root.effectivePopoutLayer