From 8dadf73d5218390087102e95740ca2f061564e21 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 3 Jul 2026 22:20:10 -0400 Subject: [PATCH] refactor: (modals/popouts): sync functions & improve blur calculations across modals --- .../Modals/Common/DankModalConnected.qml | 40 ++++--------- .../Modals/Common/DankModalStandalone.qml | 10 ++-- .../DankLauncherV2ModalStandalone.qml | 10 ++-- quickshell/Services/CompositorService.qml | 8 ++- quickshell/Widgets/DankPopoutConnected.qml | 56 +++++++------------ quickshell/Widgets/DankPopoutStandalone.qml | 10 ++-- quickshell/Widgets/WindowBlur.qml | 3 +- 7 files changed, 53 insertions(+), 84 deletions(-) diff --git a/quickshell/Modals/Common/DankModalConnected.qml b/quickshell/Modals/Common/DankModalConnected.qml index dff99d163..4ee2370f7 100644 --- a/quickshell/Modals/Common/DankModalConnected.qml +++ b/quickshell/Modals/Common/DankModalConnected.qml @@ -162,37 +162,16 @@ Item { _publishModalChromeState(); } - property bool _animSyncQueued: false - property bool _bodySyncQueued: false - function _queueFullSync() { _fullSyncPending = true; if (!_syncTimer.running) _syncTimer.restart(); } - function _queueAnimSync() { - _animSyncQueued = true; - if (!_syncTimer.running) - _syncTimer.restart(); - } - function _queueBodySync() { - _bodySyncQueued = true; - if (!_syncTimer.running) - _syncTimer.restart(); - } function _flushSync() { - const fullDirty = _fullSyncPending; - const animDirty = _animSyncQueued; - const bodyDirty = _bodySyncQueued; + if (!_fullSyncPending) + return; _fullSyncPending = false; - _animSyncQueued = false; - _bodySyncQueued = false; - if (fullDirty) - _syncModalChromeState(); - if (animDirty) - _syncModalAnim(); - if (bodyDirty) - _syncModalBody(); + _syncModalChromeState(); } function _syncModalAnim() { @@ -216,10 +195,11 @@ Item { onFrameOwnsConnectedChromeChanged: _syncModalChromeState() onResolvedConnectedBarSideChanged: _queueFullSync() onShouldBeVisibleChanged: _queueFullSync() - onAlignedXChanged: _queueBodySync() - onAlignedYChanged: _queueBodySync() - onAlignedWidthChanged: _queueBodySync() - onAlignedHeightChanged: _queueBodySync() + // Low resource scalar writes: publish synchronously to stay in the same frame + onAlignedXChanged: _syncModalBody() + onAlignedYChanged: _syncModalBody() + onAlignedWidthChanged: _syncModalBody() + onAlignedHeightChanged: _syncModalBody() Connections { target: contentWindow @@ -613,9 +593,9 @@ Item { readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress onAnimXChanged: if (root.frameOwnsConnectedChrome) - root._queueAnimSync() + root._syncModalAnim() onAnimYChanged: if (root.frameOwnsConnectedChrome) - root._queueAnimSync() + root._syncModalAnim() Item { id: contentContainer diff --git a/quickshell/Modals/Common/DankModalStandalone.qml b/quickshell/Modals/Common/DankModalStandalone.qml index 7dab56105..f0cb217ff 100644 --- a/quickshell/Modals/Common/DankModalStandalone.qml +++ b/quickshell/Modals/Common/DankModalStandalone.qml @@ -244,11 +244,11 @@ Item { WindowBlur { targetWindow: contentWindow readonly property real s: Math.min(1, modalContainer.scaleValue) - readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.06) * 2)) - blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) - blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) - blurWidth: root.shouldBeVisible ? modalContainer.width * s * op : 0 - blurHeight: root.shouldBeVisible ? modalContainer.height * s * op : 0 + // Blur tracks the surface's scaled rect, matching the connected backend. + blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) + blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) + blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0 + blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0 blurRadius: root.cornerRadius } diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml index eeb68c2eb..74f37bae3 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml @@ -364,11 +364,11 @@ Item { WindowBlur { targetWindow: launcherWindow readonly property real s: Math.min(1, modalContainer.publishedScale) - readonly property real op: Math.max(0, Math.min(1, (modalContainer.publishedOpacity - 0.06) * 2)) - blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5 - blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5 - blurWidth: contentVisible ? modalContainer.width * s * op : 0 - blurHeight: contentVisible ? modalContainer.height * s * op : 0 + // Blur tracks the surface's scaled rect + blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + blurWidth: contentVisible ? modalContainer.width * s : 0 + blurHeight: contentVisible ? modalContainer.height * s : 0 blurRadius: root.cornerRadius } diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index 69f211dcd..f19632439 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -154,10 +154,12 @@ Singleton { continue; if (!x || !y) return false; - // Only niri's enriched snapshots support value comparison. - if (x.niriWindowId === undefined || y.niriWindowId === undefined) + // Only niri/mango enriched snapshots support value comparison + const xKey = x.niriWindowId !== undefined ? x.niriWindowId : x.mangoWindowId; + const yKey = y.niriWindowId !== undefined ? y.niriWindowId : y.mangoWindowId; + if (xKey === undefined || yKey === undefined) return false; - if (x.niriWindowId !== y.niriWindowId || x.niriWorkspaceId !== y.niriWorkspaceId || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized) + if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized) return false; } return true; diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index f314f2dec..cd7f1bb3e 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -39,6 +39,8 @@ Item { property bool fullHeightSurface: false property bool _primeContent: false property bool _contentWarm: false + // Keyboard focus grabbed one tick after emerge starts, to avoid stalling first frames. + property bool _keyboardReady: false property bool _resizeActive: false property real _chromeAnimTravelX: 1 property real _chromeAnimTravelY: 1 @@ -241,7 +243,8 @@ Item { } function _connectedChromeState(visibleOverride) { - const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible; + // Track intent rather than the transient wl_surface state during remap. + const visible = visibleOverride !== undefined ? !!visibleOverride : (contentWindow.visible || root.shouldBeVisible); const presented = contentWindow.visible || root.shouldBeVisible; const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open")); const bodyX = Theme.snap(root.pubBodyX, root.dpr); @@ -353,48 +356,26 @@ Item { _publishedBodyValid = false; } - property bool _animSyncQueued: false - property bool _bodySyncQueued: false - function _queueFullSync() { _fullSyncQueued = true; if (!_syncTimer.running) _syncTimer.restart(); } - function _queueAnimSync() { - _animSyncQueued = true; - if (!_syncTimer.running) - _syncTimer.restart(); - } - function _queueBodySync() { - _bodySyncQueued = true; - if (!_syncTimer.running) - _syncTimer.restart(); - } function _flushSync() { - const fullDirty = _fullSyncQueued; - const animDirty = _animSyncQueued; - const bodyDirty = _bodySyncQueued; + if (!_fullSyncQueued) + return; _fullSyncQueued = false; - _animSyncQueued = false; - _bodySyncQueued = false; - if (fullDirty) - _syncPopoutChromeState(); - if (animDirty) { - _syncPopoutAnim("x"); - _syncPopoutAnim("y"); - } - if (bodyDirty) - _syncPopoutBody(); + _syncPopoutChromeState(); } onAlignedXChanged: _queueFullSync() onAlignedYChanged: _queueFullSync() onAlignedWidthChanged: _queueFullSync() - onContentAnimXChanged: _queueAnimSync() - onContentAnimYChanged: _queueAnimSync() - onRenderedAlignedYChanged: _queueBodySync() - onRenderedAlignedHeightChanged: _queueBodySync() + // Cheap deduped scalar writes: publish synchronously to stay in the same frame. + onContentAnimXChanged: _syncPopoutAnim("x") + onContentAnimYChanged: _syncPopoutAnim("y") + onRenderedAlignedYChanged: _syncPopoutBody() + onRenderedAlignedHeightChanged: _syncPopoutBody() onScreenChanged: { _resetPublishedBody(); _queueFullSync(); @@ -517,6 +498,10 @@ Item { animationsEnabled = true; shouldBeVisible = true; + Qt.callLater(() => { + if (root.shouldBeVisible) + root._keyboardReady = true; + }); if (shouldBeVisible && screen) { opened(); } @@ -530,6 +515,7 @@ Item { _resetPublishedBody(); isClosing = true; shouldBeVisible = false; + _keyboardReady = false; _primeContent = false; PopoutManager.popoutChanged(); closeTimer.restart(); @@ -743,9 +729,9 @@ Item { readonly property real pubBodyW: morphSeedW + (alignedWidth - morphSeedW) * morphProgress readonly property real pubBodyH: morphSeedH + (renderedAlignedHeight - morphSeedH) * morphProgress - // One animation drives all four coordinates, so queue one coalesced state update - // per progress tick instead of reacting independently to each derived property. - onMorphProgressChanged: _queueBodySync() + // One animation drives all four coordinates, so publish once per progress + // tick instead of reacting independently to each derived property. + onMorphProgressChanged: _syncPopoutBody() function _beginMorphTravel() { morphTravelEnabled = false; @@ -967,7 +953,7 @@ Item { WlrLayershell.namespace: root.layerNamespace WlrLayershell.layer: root.effectivePopoutLayer WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldBeVisible, customKeyboardFocus) + WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldBeVisible && root._keyboardReady, customKeyboardFocus) readonly property bool _fullHeight: root.fullHeightSurface anchors { diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index 35083ed8f..914520e25 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -620,13 +620,13 @@ Item { id: popoutBlur targetWindow: contentWindow readonly property real s: Math.min(1, contentContainer.scaleValue) - readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.08) * 1.6)) readonly property bool revealClipActive: root.fluidStandaloneActive - blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - s * op) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - s * op) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * s * op) : 0 - blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * s * op) : 0 + // Blur tracks the surface's scaled rect, matching the connected backend + blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) + blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) + blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * s) : 0 + blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * s) : 0 blurRadius: Theme.cornerRadius clipEnabled: revealClipActive clipX: contentContainer.x + contentContainer.revealX diff --git a/quickshell/Widgets/WindowBlur.qml b/quickshell/Widgets/WindowBlur.qml index d4095b5d5..de337ac0e 100644 --- a/quickshell/Widgets/WindowBlur.qml +++ b/quickshell/Widgets/WindowBlur.qml @@ -99,8 +99,9 @@ Item { root._clear(); } function onResourcesLost() { - lifecycleKickAction.cancel(); root._clear(); + // Re-arm so blur self-heals since instances are no longer recreated per open + root._scheduleLifecycleKick(); } function onWindowConnected() { root._scheduleLifecycleKick();