From d08c7c5e5528133d84aac80f34935931ae3423c9 Mon Sep 17 00:00:00 2001 From: purian23 Date: Sun, 7 Jun 2026 17:47:24 -0400 Subject: [PATCH] refactor(frame): improve connected mode surface recovery - share modal and launcher ownership handling - recover missing background and blur layers --- quickshell/Common/ConnectedModalChrome.qml | 125 ++++++++++++++++++ quickshell/Common/ConnectedModeState.qml | 82 ++++++++++-- quickshell/Common/ModalManager.qml | 5 + quickshell/Common/PopoutManager.qml | 5 + .../Modals/Common/DankModalConnected.qml | 74 ++++------- .../DankLauncherV2ModalConnected.qml | 72 ++++------ quickshell/Modules/Frame/FrameWindow.qml | 87 +++++++++++- quickshell/Widgets/DankPopoutConnected.qml | 64 +++++++-- quickshell/Widgets/WindowBlur.qml | 49 ++++++- 9 files changed, 439 insertions(+), 124 deletions(-) create mode 100644 quickshell/Common/ConnectedModalChrome.qml diff --git a/quickshell/Common/ConnectedModalChrome.qml b/quickshell/Common/ConnectedModalChrome.qml new file mode 100644 index 00000000..e11ef30a --- /dev/null +++ b/quickshell/Common/ConnectedModalChrome.qml @@ -0,0 +1,125 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +Item { + id: root + + required property var modalHandle + required property string claimPrefix + property string screenName: "" + property bool enabled: false + property bool active: false + property bool presented: false + property bool dockBlocked: false + property string dockSide: "" + + property string claimId: "" + property string claimedScreenName: "" + + signal recoveryRequested + + visible: false + + function _nextClaimId() { + return claimPrefix + ":" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000); + } + + function _isCurrentModal(name) { + return !!name && ModalManager.isCurrentModal(modalHandle, name); + } + + function _shouldRecover() { + return active && enabled && _isCurrentModal(screenName); + } + + function _requestRecovery() { + if (_shouldRecover()) + recoveryRequested(); + } + + function publish(state) { + if (!enabled || !screenName || !state) { + release(); + return false; + } + if (claimedScreenName && claimedScreenName !== screenName) + release(); + + const isCurrent = _isCurrentModal(screenName); + let isClaim = !claimId; + if (isClaim && !isCurrent) + return false; + if (isClaim) + claimId = _nextClaimId(); + + let published = isClaim ? ConnectedModeState.claimModalState(screenName, state, claimId) : ConnectedModeState.ensureModalState(screenName, state, claimId); + if (!published && !isClaim && isCurrent) { + ConnectedModeState.releaseDockRetract(claimId); + claimId = _nextClaimId(); + published = ConnectedModeState.claimModalState(screenName, state, claimId); + } + if (!published) + return false; + + claimedScreenName = screenName; + if (dockBlocked && presented) + ConnectedModeState.requestDockRetract(claimId, screenName, dockSide); + else + ConnectedModeState.releaseDockRetract(claimId); + return true; + } + + function updateAnim(animX, animY) { + if (!enabled || !claimId || !claimedScreenName) + return false; + if (!ConnectedModeState.hasModalOwner(claimedScreenName, claimId)) { + _requestRecovery(); + return false; + } + return ConnectedModeState.setModalAnim(claimedScreenName, animX, animY, claimId); + } + + function updateBody(bodyX, bodyY, bodyW, bodyH) { + if (!enabled || !claimId || !claimedScreenName) + return false; + if (!ConnectedModeState.hasModalOwner(claimedScreenName, claimId)) { + _requestRecovery(); + return false; + } + return ConnectedModeState.setModalBody(claimedScreenName, bodyX, bodyY, bodyW, bodyH, claimId); + } + + function release() { + if (!claimId) + return; + ConnectedModeState.releaseDockRetract(claimId); + const releasedClaimId = claimId; + const releasedScreenName = claimedScreenName; + claimId = ""; + claimedScreenName = ""; + if (releasedScreenName) + ConnectedModeState.clearModalState(releasedScreenName, releasedClaimId); + } + + Component.onDestruction: release() + + Connections { + target: ModalManager + function onModalChanged() { + root._requestRecovery(); + } + } + + Connections { + target: ConnectedModeState + function onModalOwnersChanged() { + if (!ConnectedModeState.hasModalOwner(root.screenName, root.claimId)) + root._requestRecovery(); + } + function onModalStatesChanged() { + if (!ConnectedModeState.modalStates[root.screenName]) + root._requestRecovery(); + } + } +} diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml index 488dd04b..80e5353b 100644 --- a/quickshell/Common/ConnectedModeState.qml +++ b/quickshell/Common/ConnectedModeState.qml @@ -38,6 +38,10 @@ Singleton { // Dock slide offsets — hot-path updates separated from full geometry state property var dockSlides: ({}) + // Surfaces are keyed by screen.name. FrameWindow watches to refresh connected chrome + // after claim/release boundaries without tracking each animation frame + property var surfaceRevisions: ({}) + function _cloneDict(src) { const next = {}; for (const k in src) @@ -45,16 +49,31 @@ Singleton { return next; } + function _bumpSurfaceRevision(screenName) { + if (!screenName) + return; + const next = _cloneDict(surfaceRevisions); + next[screenName] = Number(next[screenName] || 0) + 1; + surfaceRevisions = next; + } + function hasPopoutOwner(claimId) { return !!claimId && popoutOwnerId === claimId; } function claimPopout(claimId, state) { - if (!claimId) + if (!claimId || !state) return false; + const previousScreen = popoutScreen; popoutOwnerId = claimId; - return updatePopout(claimId, state); + const ok = updatePopout(claimId, state); + if (ok) { + if (previousScreen && previousScreen !== popoutScreen) + _bumpSurfaceRevision(previousScreen); + _bumpSurfaceRevision(popoutScreen); + } + return ok; } function updatePopout(claimId, state) { @@ -91,6 +110,7 @@ Singleton { if (!hasPopoutOwner(claimId)) return false; + const releasedScreen = popoutScreen; popoutOwnerId = ""; popoutVisible = false; popoutBarSide = "top"; @@ -103,6 +123,7 @@ Singleton { popoutScreen = ""; popoutOmitStartConnector = false; popoutOmitEndConnector = false; + _bumpSurfaceRevision(releasedScreen); return true; } @@ -174,10 +195,13 @@ Singleton { const normalized = _normalizeDockState(state); if (_sameDockState(dockStates[screenName], normalized)) return true; + const previous = dockStates[screenName] || emptyDockState; const next = _cloneDict(dockStates); next[screenName] = normalized; dockStates = next; + if (!!previous.reveal !== !!normalized.reveal) + _bumpSurfaceRevision(screenName); return true; } @@ -195,6 +219,7 @@ Singleton { delete nextSlides[screenName]; dockSlides = nextSlides; } + _bumpSurfaceRevision(screenName); return true; } @@ -260,10 +285,13 @@ Singleton { const normalized = _normalizeNotificationState(state); if (_sameNotificationState(notificationStates[screenName], normalized)) return true; + const previous = notificationStates[screenName] || emptyNotificationState; const next = _cloneDict(notificationStates); next[screenName] = normalized; notificationStates = next; + if (!!previous.visible !== !!normalized.visible) + _bumpSurfaceRevision(screenName); return true; } @@ -274,6 +302,7 @@ Singleton { const next = _cloneDict(notificationStates); delete next[screenName]; notificationStates = next; + _bumpSurfaceRevision(screenName); return true; } @@ -330,18 +359,17 @@ Singleton { modalOwners = nextOwners; } const normalized = _normalizeModalState(state); - if (_sameModalState(modalStates[screenName], normalized)) - return true; const next = _cloneDict(modalStates); next[screenName] = normalized; modalStates = next; + _bumpSurfaceRevision(screenName); return true; } function updateModalState(screenName, state, ownerId) { if (!screenName || !state) return false; - if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId) + if (ownerId && modalOwners[screenName] !== ownerId) return false; const normalized = _normalizeModalState(state); if (_sameModalState(modalStates[screenName], normalized)) @@ -352,30 +380,50 @@ Singleton { return true; } + function hasModalOwner(screenName, ownerId) { + return !!screenName && !!ownerId && modalOwners[screenName] === ownerId; + } + + function ensureModalState(screenName, state, ownerId) { + if (!screenName || !state || !ownerId) + return false; + const currentOwner = modalOwners[screenName] || ""; + if (currentOwner && currentOwner !== ownerId) + return false; + if (!currentOwner) + return claimModalState(screenName, state, ownerId); + return updateModalState(screenName, state, ownerId); + } + function setModalState(screenName, state) { return updateModalState(screenName, state, null); } function clearModalState(screenName, ownerId) { - if (!screenName || !modalStates[screenName]) + if (!screenName) return false; - if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId) + if (ownerId && modalOwners[screenName] !== ownerId) + return false; + if (!modalStates[screenName] && !modalOwners[screenName]) return false; - const next = _cloneDict(modalStates); - delete next[screenName]; - modalStates = next; + if (modalStates[screenName]) { + const next = _cloneDict(modalStates); + delete next[screenName]; + modalStates = next; + } if (modalOwners[screenName]) { const nextOwners = _cloneDict(modalOwners); delete nextOwners[screenName]; modalOwners = nextOwners; } + _bumpSurfaceRevision(screenName); return true; } function setModalAnim(screenName, animX, animY, ownerId) { - if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId) + if (ownerId && modalOwners[screenName] !== ownerId) return false; const cur = screenName ? modalStates[screenName] : null; if (!cur) @@ -394,7 +442,7 @@ Singleton { } function setModalBody(screenName, bodyX, bodyY, bodyW, bodyH, ownerId) { - if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId) + if (ownerId && modalOwners[screenName] !== ownerId) return false; const cur = screenName ? modalStates[screenName] : null; if (!cur) @@ -492,6 +540,9 @@ Singleton { const nextModalOwners = pruneKeyed(modalOwners); if (nextModalOwners !== null) modalOwners = nextModalOwners; + const nextSurfaceRevisions = pruneKeyed(surfaceRevisions); + if (nextSurfaceRevisions !== null) + surfaceRevisions = nextSurfaceRevisions; let retractChanged = false; const nextRetract = {}; @@ -512,7 +563,12 @@ Singleton { Connections { target: Quickshell function onScreensChanged() { - root._pruneToLiveScreens(); + screenPruneAction.schedule(); } } + + DeferredAction { + id: screenPruneAction + onTriggered: root._pruneToLiveScreens() + } } diff --git a/quickshell/Common/ModalManager.qml b/quickshell/Common/ModalManager.qml index d6f2c370..a2d3198a 100644 --- a/quickshell/Common/ModalManager.qml +++ b/quickshell/Common/ModalManager.qml @@ -26,6 +26,11 @@ Singleton { }); } + function isCurrentModal(modal, screenName) { + const name = screenName || modal?.effectiveScreen?.name || "unknown"; + return currentModalsByScreen[name] === modal; + } + function closeModal(modal) { const screenName = modal.effectiveScreen?.name ?? "unknown"; if (currentModalsByScreen[screenName] === modal) { diff --git a/quickshell/Common/PopoutManager.qml b/quickshell/Common/PopoutManager.qml index 1c4c7aa5..f7dcc49a 100644 --- a/quickshell/Common/PopoutManager.qml +++ b/quickshell/Common/PopoutManager.qml @@ -98,6 +98,11 @@ Singleton { return currentPopoutsByScreen[screen.name] || null; } + function isCurrentPopout(popout, screenName) { + const name = screenName || popout?.screen?.name || ""; + return !!name && currentPopoutsByScreen[name] === popout; + } + function requestPopout(popout, tabIndex, triggerSource) { if (!popout || !popout.screen) return; diff --git a/quickshell/Modals/Common/DankModalConnected.qml b/quickshell/Modals/Common/DankModalConnected.qml index b0648a16..30618bb1 100644 --- a/quickshell/Modals/Common/DankModalConnected.qml +++ b/quickshell/Modals/Common/DankModalConnected.qml @@ -105,21 +105,26 @@ Item { property bool animationsEnabled: true - property string _chromeClaimId: "" property bool _fullSyncPending: false - function _nextChromeClaimId() { - return layerNamespace + ":modal:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000); - } - function _currentScreenName() { return effectiveScreen ? effectiveScreen.name : ""; } - function _publishModalChromeState(isClaim) { - const screenName = _currentScreenName(); - if (!screenName) - return; + ConnectedModalChrome { + id: modalChrome + modalHandle: root.modalHandle + claimPrefix: root.layerNamespace + ":modal" + screenName: root._currentScreenName() + enabled: root.frameOwnsConnectedChrome + active: root.shouldBeVisible + presented: root.shouldBeVisible || contentWindow.visible + dockBlocked: root._dockBlocksEmergence + dockSide: root.resolvedConnectedBarSide + onRecoveryRequested: root._queueFullSync() + } + + function _publishModalChromeState() { const state = { "visible": shouldBeVisible || contentWindow.visible, "barSide": resolvedConnectedBarSide, @@ -132,25 +137,11 @@ Item { "omitStartConnector": false, "omitEndConnector": false }; - if (isClaim) - ConnectedModeState.claimModalState(screenName, state, _chromeClaimId); - else - ConnectedModeState.updateModalState(screenName, state, _chromeClaimId); + return modalChrome.publish(state); } function _syncModalChromeState() { - if (!frameOwnsConnectedChrome) { - _releaseModalChrome(); - return; - } - const isClaim = !_chromeClaimId; - if (!_chromeClaimId) - _chromeClaimId = _nextChromeClaimId(); - _publishModalChromeState(isClaim); - if (_dockBlocksEmergence && (shouldBeVisible || contentWindow.visible)) - ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide); - else - ConnectedModeState.releaseDockRetract(_chromeClaimId); + _publishModalChromeState(); } property bool _animSyncQueued: false @@ -187,32 +178,21 @@ Item { } function _syncModalAnim() { - if (!frameOwnsConnectedChrome || !_chromeClaimId) + if (!frameOwnsConnectedChrome) return; - const screenName = _currentScreenName(); - if (!screenName || !modalContainer) + if (!modalContainer) return; - ConnectedModeState.setModalAnim(screenName, modalContainer.animX, modalContainer.animY, _chromeClaimId); + modalChrome.updateAnim(modalContainer.animX, modalContainer.animY); } function _syncModalBody() { - if (!frameOwnsConnectedChrome || !_chromeClaimId) + if (!frameOwnsConnectedChrome) return; - const screenName = _currentScreenName(); - if (!screenName) - return; - ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight, _chromeClaimId); + modalChrome.updateBody(alignedX, alignedY, alignedWidth, alignedHeight); } function _releaseModalChrome() { - if (!_chromeClaimId) - return; - ConnectedModeState.releaseDockRetract(_chromeClaimId); - const claimId = _chromeClaimId; - _chromeClaimId = ""; - const screenName = _currentScreenName(); - if (screenName) - ConnectedModeState.clearModalState(screenName, claimId); + modalChrome.release(); } onFrameOwnsConnectedChromeChanged: _syncModalChromeState() @@ -223,8 +203,6 @@ Item { onAlignedWidthChanged: _queueBodySync() onAlignedHeightChanged: _queueBodySync() - Component.onDestruction: _releaseModalChrome() - Connections { target: contentWindow function onVisibleChanged() { @@ -248,12 +226,12 @@ Item { clickCatcher.screen = focusedScreen; } + ModalManager.openModal(modalHandle); if (Theme.isDirectionalEffect || root.useBackground) { if (!useSingleWindow) clickCatcher.visible = true; contentWindow.visible = true; } - ModalManager.openModal(modalHandle); Qt.callLater(() => { animationsEnabled = true; @@ -317,8 +295,12 @@ Item { break; } } - if (screenStillExists) + if (screenStillExists) { + if (root.shouldBeVisible) + root._queueFullSync(); return; + } + root._releaseModalChrome(); const newScreen = CompositorService.getFocusedScreen(); if (newScreen) { contentWindow.screen = newScreen; diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml index f3b74274..fef28659 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml @@ -232,21 +232,26 @@ Item { onTriggered: root._flushSync() } - property string _chromeClaimId: "" property bool _fullSyncPending: false - function _nextChromeClaimId() { - return "dms:launcher-v2:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000); - } - function _currentScreenName() { return effectiveScreen ? effectiveScreen.name : ""; } - function _publishModalChromeState(isClaim) { - const screenName = _currentScreenName(); - if (!screenName) - return; + ConnectedModalChrome { + id: modalChrome + modalHandle: root.modalHandle + claimPrefix: "dms:launcher-v2" + screenName: root._currentScreenName() + enabled: root.frameOwnsConnectedChrome + active: root.spotlightOpen + presented: root.spotlightOpen || contentWindow.visible + dockBlocked: root._dockBlocksEmergence + dockSide: root.resolvedConnectedBarSide + onRecoveryRequested: root._queueFullSync() + } + + function _publishModalChromeState() { const state = { "visible": spotlightOpen || contentWindow.visible, "barSide": resolvedConnectedBarSide, @@ -259,25 +264,11 @@ Item { "omitStartConnector": false, "omitEndConnector": false }; - if (isClaim) - ConnectedModeState.claimModalState(screenName, state, _chromeClaimId); - else - ConnectedModeState.updateModalState(screenName, state, _chromeClaimId); + return modalChrome.publish(state); } function _syncModalChromeState() { - if (!frameOwnsConnectedChrome) { - _releaseModalChrome(); - return; - } - const isClaim = !_chromeClaimId; - if (!_chromeClaimId) - _chromeClaimId = _nextChromeClaimId(); - _publishModalChromeState(isClaim); - if (_dockBlocksEmergence && (spotlightOpen || contentWindow.visible)) - ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide); - else - ConnectedModeState.releaseDockRetract(_chromeClaimId); + _publishModalChromeState(); } property bool _animSyncQueued: false @@ -314,32 +305,21 @@ Item { } function _syncModalAnim() { - if (!frameOwnsConnectedChrome || !_chromeClaimId) + if (!frameOwnsConnectedChrome) return; - const screenName = _currentScreenName(); - if (!screenName || !contentContainer) + if (!contentContainer) return; - ConnectedModeState.setModalAnim(screenName, contentContainer.animX, contentContainer.animY, _chromeClaimId); + modalChrome.updateAnim(contentContainer.animX, contentContainer.animY); } function _syncModalBody() { - if (!frameOwnsConnectedChrome || !_chromeClaimId) + if (!frameOwnsConnectedChrome) return; - const screenName = _currentScreenName(); - if (!screenName) - return; - ConnectedModeState.setModalBody(screenName, _connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight, _chromeClaimId); + modalChrome.updateBody(_connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight); } function _releaseModalChrome() { - if (!_chromeClaimId) - return; - ConnectedModeState.releaseDockRetract(_chromeClaimId); - const claimId = _chromeClaimId; - _chromeClaimId = ""; - const screenName = _currentScreenName(); - if (screenName) - ConnectedModeState.clearModalState(screenName, claimId); + modalChrome.release(); } onFrameOwnsConnectedChromeChanged: _syncModalChromeState() @@ -351,8 +331,6 @@ Item { onAlignedWidthChanged: _queueBodySync() onAlignedHeightChanged: _queueBodySync() - Component.onDestruction: _releaseModalChrome() - Connections { target: contentWindow function onVisibleChanged() { @@ -579,13 +557,17 @@ Item { } } - if (!needsReset) + if (!needsReset) { + if (root.spotlightOpen) + root._queueFullSync(); return; + } const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; if (!newScreen) return; + root._releaseModalChrome(); root._windowEnabled = false; backgroundWindow.screen = newScreen; contentWindow.screen = newScreen; diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 70541383..c67c8d43 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -45,6 +45,7 @@ PanelWindow { readonly property int _windowRegionWidth: win._regionInt(win.width) readonly property int _windowRegionHeight: win._regionInt(win.height) readonly property string _screenName: win.targetScreen ? win.targetScreen.name : "" + readonly property int _surfaceRevision: Number(ConnectedModeState.surfaceRevisions[win._screenName] || 0) readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({ "x": 0, @@ -150,6 +151,8 @@ PanelWindow { readonly property real _surfaceRadius: Theme.connectedSurfaceRadius readonly property real _seamOverlap: Theme.hairline(win._dpr) readonly property bool _disableLayer: Quickshell.env("DMS_DISABLE_LAYER") === "true" || Quickshell.env("DMS_DISABLE_LAYER") === "1" + property bool _surfaceRefreshNeedsLayerRecreate: false + property bool _surfaceLayerRecoveryActive: false function _regionInt(value) { return Math.max(0, Math.round(Theme.px(value, win._dpr))); @@ -1187,12 +1190,14 @@ PanelWindow { return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r; } - function _buildBlur() { + function _buildBlur(forceRepublish) { try { if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) { win.BackgroundEffect.blurRegion = null; return; } + if (forceRepublish) + win.BackgroundEffect.blurRegion = null; win.BackgroundEffect.blurRegion = _staticBlurRegion; } catch (e) { win.log.warn("Failed to set blur region:", e); @@ -1216,7 +1221,54 @@ PanelWindow { blurRebuildAction.schedule(); } function _runBlurRebuild() { - _buildBlur(); + _buildBlur(false); + } + + function _republishFrameBlur() { + _buildBlur(true); + } + + function _requestContentUpdate() { + try { + if (win.contentItem && typeof win.contentItem.update === "function") + win.contentItem.update(); + } catch (e) {} + } + + function _scheduleSurfaceRefresh(recreateLayer) { + if (recreateLayer) + _surfaceRefreshNeedsLayerRecreate = true; + surfaceRefreshAction.restart(); + } + + function _runSurfaceRefresh() { + if (!win.visible) + return; + if (_surfaceRefreshNeedsLayerRecreate) { + _surfaceRefreshNeedsLayerRecreate = false; + if (win._connectedActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1)) { + _surfaceLayerRecoveryActive = true; + surfaceLayerRestoreAction.restart(); + } + } + _requestContentUpdate(); + _republishFrameBlur(); + } + + function _finishSurfaceLayerRecovery() { + _surfaceLayerRecoveryActive = false; + _requestContentUpdate(); + _republishFrameBlur(); + } + + DeferredAction { + id: surfaceRefreshAction + onTriggered: win._runSurfaceRefresh() + } + + DeferredAction { + id: surfaceLayerRestoreAction + onTriggered: win._finishSurfaceLayerRecovery() } Connections { @@ -1263,14 +1315,41 @@ PanelWindow { onVisibleChanged: { if (visible) { win._scheduleBlurRebuild(); + win._scheduleSurfaceRefresh(false); } else { + surfaceRefreshAction.cancel(); + surfaceLayerRestoreAction.cancel(); + _surfaceLayerRecoveryActive = false; + _surfaceRefreshNeedsLayerRecreate = false; _teardownBlur(); } } - Component.onCompleted: win._scheduleBlurRebuild() + on_SurfaceRevisionChanged: win._scheduleSurfaceRefresh(false) + + onResourcesLost: { + blurRebuildAction.cancel(); + surfaceRefreshAction.cancel(); + surfaceLayerRestoreAction.cancel(); + _surfaceRefreshNeedsLayerRecreate = true; + if (win._connectedActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1)) + _surfaceLayerRecoveryActive = true; + win._teardownBlur(); + } + + onWindowConnected: { + win._scheduleSurfaceRefresh(true); + win._scheduleBlurRebuild(); + } + + Component.onCompleted: { + win._scheduleBlurRebuild(); + win._scheduleSurfaceRefresh(false); + } Component.onDestruction: { blurRebuildAction.cancel(); + surfaceRefreshAction.cancel(); + surfaceLayerRestoreAction.cancel(); win._teardownBlur(); } @@ -1290,7 +1369,7 @@ PanelWindow { visible: win._connectedActive opacity: win._surfaceOpacity // Skip FBO when disabled, invisible, or when neither elevation nor alpha blend is active - layer.enabled: win._connectedActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1) + layer.enabled: win._connectedActive && !win._surfaceLayerRecoveryActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1) layer.smooth: false layer.effect: MultiEffect { diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index e944e31c..9f7a2f54 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -220,14 +220,20 @@ Item { function _publishConnectedChromeState(forceClaim, visibleOverride) { if (!root.frameOwnsConnectedChrome || !root.screen || !_chromeClaimId) - return; + return false; + + const screenName = root.screen.name; + const isCurrent = PopoutManager.isCurrentPopout(popoutHandle, screenName); + if (!ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { + if (!isCurrent) + return false; + forceClaim = true; + } else if (forceClaim && !isCurrent) { + return false; + } const state = _connectedChromeState(visibleOverride); - if (forceClaim || !ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { - ConnectedModeState.claimPopout(_chromeClaimId, state); - } else { - ConnectedModeState.updatePopout(_chromeClaimId, state); - } + return forceClaim ? ConnectedModeState.claimPopout(_chromeClaimId, state) : ConnectedModeState.updatePopout(_chromeClaimId, state); } function _releaseConnectedChromeState() { @@ -252,9 +258,12 @@ Item { } if (!contentWindow.visible && !shouldBeVisible) return; - if (!_chromeClaimId) + if (!_chromeClaimId) { + if (!PopoutManager.isCurrentPopout(popoutHandle, root.screen.name)) + return; _chromeClaimId = _nextChromeClaimId(); - _publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_chromeClaimId)); + } + _publishConnectedChromeState(!ConnectedModeState.hasPopoutOwner(_chromeClaimId)); } function _syncPopoutAnim(axis) { @@ -267,6 +276,11 @@ Item { const syncY = axis === "y" && (barSide === "top" || barSide === "bottom"); if (!syncX && !syncY) return; + if (!ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { + if (root.screen && PopoutManager.isCurrentPopout(popoutHandle, root.screen.name)) + _queueFullSync(); + return; + } ConnectedModeState.setPopoutAnim(_chromeClaimId, syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined); } @@ -275,6 +289,11 @@ Item { return; if (!contentWindow.visible && !shouldBeVisible) return; + if (!ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { + if (root.screen && PopoutManager.isCurrentPopout(popoutHandle, root.screen.name)) + _queueFullSync(); + return; + } ConnectedModeState.setPopoutBody(_chromeClaimId, root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight); } @@ -326,10 +345,13 @@ Item { Connections { target: contentWindow function onVisibleChanged() { - if (contentWindow.visible) + if (contentWindow.visible) { + if (!root._chromeClaimId) + root._chromeClaimId = root._nextChromeClaimId(); root._publishConnectedChromeState(true); - else + } else { root._releaseConnectedChromeState(); + } } } @@ -337,7 +359,7 @@ Item { target: SettingsData function onConnectedFrameModeActiveChanged() { if (root.frameOwnsConnectedChrome) { - if (contentWindow.visible || root.shouldBeVisible) { + if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name)) { if (!root._chromeClaimId) root._chromeClaimId = root._nextChromeClaimId(); root._publishConnectedChromeState(true); @@ -351,6 +373,22 @@ Item { } } + Connections { + target: ConnectedModeState + function onPopoutOwnerIdChanged() { + if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name) && !ConnectedModeState.hasPopoutOwner(root._chromeClaimId)) + root._queueFullSync(); + } + } + + Connections { + target: PopoutManager + function onPopoutChanged() { + if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name)) + root._queueFullSync(); + } + } + readonly property bool frameOwnsConnectedChrome: CompositorService.usesConnectedFrameChromeForScreen(root.screen) readonly property bool usesConnectedSurfaceChrome: Theme.isConnectedEffect && !CompositorService.connectedFrameBlockedOnScreen(root.screen) readonly property bool usesLocalConnectedSurfaceChrome: usesConnectedSurfaceChrome && !frameOwnsConnectedChrome @@ -373,6 +411,7 @@ Item { contentWindow.visible = false; } _lastOpenedScreen = screen; + PopoutManager.showPopout(popoutHandle); if (contentContainer) { // Snap morph closed only on a fresh open; on screen-change re-open we stay at 1 @@ -408,7 +447,6 @@ Item { animationsEnabled = true; shouldBeVisible = true; if (shouldBeVisible && screen) { - PopoutManager.showPopout(popoutHandle); opened(); } } @@ -440,6 +478,8 @@ Item { } if (!screenStillExists) { close(); + } else { + root._queueFullSync(); } } } diff --git a/quickshell/Widgets/WindowBlur.qml b/quickshell/Widgets/WindowBlur.qml index 2b894658..aa8b8290 100644 --- a/quickshell/Widgets/WindowBlur.qml +++ b/quickshell/Widgets/WindowBlur.qml @@ -34,6 +34,11 @@ Item { targetWindow.BackgroundEffect.blurRegion = _active ? blurRegion : null; } + function _clear() { + if (targetWindow) + targetWindow.BackgroundEffect.blurRegion = null; + } + // Force BackgroundEffect to re-publish the blur region on the current wl_surface. // Clearing first bypasses Quickshell's same-Region dedup in BackgroundEffect::setBlurRegion, // setting pendingBlurRegion=true so the next polish actually ships the region — needed @@ -45,20 +50,56 @@ Item { targetWindow.BackgroundEffect.blurRegion = _active ? blurRegion : null; } - on_ActiveChanged: _apply() - onTargetWindowChanged: _apply() + function _scheduleLifecycleKick() { + lifecycleKickAction.restart(); + } + + function _runLifecycleKick() { + if (!targetWindow) + return; + if (targetWindow.visible) + kick(); + else + _apply(); + } + + on_ActiveChanged: { + if (_active) + _scheduleLifecycleKick(); + else + _clear(); + } + onTargetWindowChanged: { + lifecycleKickAction.cancel(); + _apply(); + } + + DeferredAction { + id: lifecycleKickAction + onTriggered: root._runLifecycleKick() + } Connections { target: root.targetWindow ?? null ignoreUnknownSignals: true function onVisibleChanged() { if (root.targetWindow && root.targetWindow.visible) - root._apply(); + root._scheduleLifecycleKick(); + else + root._clear(); + } + function onResourcesLost() { + lifecycleKickAction.cancel(); + root._clear(); + } + function onWindowConnected() { + root._scheduleLifecycleKick(); } } - Component.onCompleted: _apply() + Component.onCompleted: _scheduleLifecycleKick() Component.onDestruction: { + lifecycleKickAction.cancel(); if (targetWindow) targetWindow.BackgroundEffect.blurRegion = null; }