From 4a021a73bfe002dcef17ddcfa968e24e11b967a1 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 22 Aug 2026 11:12:47 -0400 Subject: [PATCH] fix(shell): re-commit layer surfaces on screen reconnect instead of rebuilding bars --- dank-qml-common | 2 +- quickshell/Common/SurfaceRecovery.qml | 88 ++++++++++++++++++++ quickshell/Modules/DankBar/DankBarWindow.qml | 10 ++- quickshell/Modules/Dock/Dock.qml | 6 ++ quickshell/Modules/Frame/FrameExclusions.qml | 4 + quickshell/ShellCore.qml | 27 +++++- 6 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 quickshell/Common/SurfaceRecovery.qml diff --git a/dank-qml-common b/dank-qml-common index 720d75089..f61341cc1 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 720d75089580f1f3b7a2d6ed4b39e823734ceefa +Subproject commit f61341cc12e79c8c414589fbf987d7d70e6f3f12 diff --git a/quickshell/Common/SurfaceRecovery.qml b/quickshell/Common/SurfaceRecovery.qml new file mode 100644 index 000000000..60567d3f3 --- /dev/null +++ b/quickshell/Common/SurfaceRecovery.qml @@ -0,0 +1,88 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Services + +Singleton { + id: root + + readonly property var log: Log.scoped("SurfaceRecovery") + property var _windows: [] + + function track(window) { + if (!window || _windows.includes(window)) + return; + _windows = _windows.concat([window]); + } + + function untrack(window) { + if (!_windows.includes(window)) + return; + _windows = _windows.filter(w => w !== window); + } + + function freeEdge(window) { + const a = window.anchors; + if (!a.bottom) + return "bottom"; + if (!a.top) + return "top"; + if (!a.right) + return "right"; + if (!a.left) + return "left"; + return ""; + } + + // wlr-layer-shell set_margin: "Setting this value for edges you are not anchored to has no effect" + function refresh(window) { + if (!window) + return false; + const edge = freeEdge(window); + if (edge === "") + return false; + switch (edge) { + case "bottom": + window.margins.bottom = window.margins.bottom ? 0 : 1; + break; + case "top": + window.margins.top = window.margins.top ? 0 : 1; + break; + case "right": + window.margins.right = window.margins.right ? 0 : 1; + break; + case "left": + window.margins.left = window.margins.left ? 0 : 1; + break; + } + return true; + } + + function refreshAll() { + let refreshed = 0; + for (const window of _windows) { + if (refresh(window)) + refreshed++; + } + log.info("Refreshed", refreshed, "of", _windows.length, "layer surfaces"); + return refreshed; + } + + function fitsScreen(window) { + if (!window?.screen || !window.visible) + return true; + if (window.width <= 0 || window.height <= 0) + return false; + const a = window.anchors; + if (a.left && a.right && window.width > window.screen.width) + return false; + if (a.top && a.bottom && window.height > window.screen.height) + return false; + return true; + } + + function staleWindows() { + return _windows.filter(w => !fitsScreen(w)); + } +} diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index 7c74e6852..5e04dbcc2 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -69,8 +69,14 @@ PanelWindow { BackgroundEffect.blurRegion: body.blurRegion - Component.onCompleted: KeyboardFocus.registerBarWindow(barWindow) - Component.onDestruction: KeyboardFocus.unregisterBarWindow(barWindow) + Component.onCompleted: { + KeyboardFocus.registerBarWindow(barWindow); + SurfaceRecovery.track(barWindow); + } + Component.onDestruction: { + KeyboardFocus.unregisterBarWindow(barWindow); + SurfaceRecovery.untrack(barWindow); + } IdleInhibitor { window: barWindow diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 2ea80f0bb..863582363 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -43,6 +43,9 @@ Variants { implicitHeight: body.surfaceImplicitHeight exclusiveZone: body.surfaceExclusiveZone + Component.onCompleted: SurfaceRecovery.track(dock) + Component.onDestruction: SurfaceRecovery.untrack(dock) + WindowBlur { targetWindow: dock blurEnabled: body.effectiveBlurEnabled && !body.usesConnectedFrameChrome @@ -80,6 +83,9 @@ Variants { WlrLayershell.namespace: "dms:dock-exclusion" WlrLayershell.layer: WlrLayer.Top + Component.onCompleted: SurfaceRecovery.track(dockExclusion) + Component.onDestruction: SurfaceRecovery.untrack(dockExclusion) + anchors { top: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true bottom: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true diff --git a/quickshell/Modules/Frame/FrameExclusions.qml b/quickshell/Modules/Frame/FrameExclusions.qml index 3f7a06363..389344c57 100644 --- a/quickshell/Modules/Frame/FrameExclusions.qml +++ b/quickshell/Modules/Frame/FrameExclusions.qml @@ -86,6 +86,7 @@ Scope { } component EdgeExclusion: PanelWindow { + id: edgeExclusion required property var targetScreen property real exclusionSize: SettingsData.frameThickness @@ -103,6 +104,9 @@ Scope { implicitWidth: 1 implicitHeight: 1 + Component.onCompleted: SurfaceRecovery.track(edgeExclusion) + Component.onDestruction: SurfaceRecovery.untrack(edgeExclusion) + anchors { top: anchorTop bottom: anchorBottom diff --git a/quickshell/ShellCore.qml b/quickshell/ShellCore.qml index 8ef7090f6..94c939a4a 100644 --- a/quickshell/ShellCore.qml +++ b/quickshell/ShellCore.qml @@ -226,6 +226,12 @@ Item { screenReconnectDebounce.restart(); } + function refreshScreenSurfaces() { + log.info("Refreshing layer surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(",")); + SurfaceRecovery.refreshAll(); + surfaceRefreshVerifyTimer.restart(); + } + Timer { id: screenReconnectDebounce // Wide enough to collapse the output-remove + output-re-add pair that one @@ -236,15 +242,27 @@ Item { root._screenRecoveryCooldown = true; root._screenRecoveryPending = false; screenReconnectCooldown.restart(); - root.triggerSurfaceRecovery("screen-reconnect"); + root.refreshScreenSurfaces(); + } + } + + Timer { + id: surfaceRefreshVerifyTimer + interval: 800 + repeat: false + onTriggered: { + const stale = SurfaceRecovery.staleWindows(); + if (stale.length === 0) + return; + log.warn("Layer surfaces still stale after refresh:", stale.map(w => (w.screen?.name ?? "?") + ":" + w.width + "x" + w.height).join(",")); + root.triggerSurfaceRecovery("stale-surfaces"); } } Timer { id: screenReconnectCooldown - // Must exceed the full two-pass surfaceResumeRecoveryTimer sequence - // (800 + 2000 ms) so the cooldown still covers an in-flight recovery; - // raise this if those passes are lengthened. + // Must exceed surfaceRefreshVerifyTimer plus the two-pass + // surfaceResumeRecoveryTimer sequence (800 + 800 + 2000 ms). interval: 4000 repeat: false onTriggered: { @@ -295,6 +313,7 @@ Item { // its cooldown expires. screenReconnectDebounce.stop(); screenReconnectCooldown.stop(); + surfaceRefreshVerifyTimer.stop(); root._screenRecoveryCooldown = false; root._screenRecoveryPending = false;