fix(shell): re-commit layer surfaces on screen reconnect instead of rebuilding bars

This commit is contained in:
bbedward
2026-08-22 11:12:47 -04:00
parent b482e9b898
commit 4a021a73bf
6 changed files with 130 additions and 7 deletions
+88
View File
@@ -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));
}
}
+8 -2
View File
@@ -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
+6
View File
@@ -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
@@ -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
+23 -4
View File
@@ -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;