From c4ea1a1dbdbb6eb3f9104dab181cfadd0a4a512a Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 30 Sep 2026 18:31:48 -0400 Subject: [PATCH] frame: collapse a hidden bar's band to the gutter fixes #3630 --- quickshell/Common/LayoutResolver.js | 37 +++++++++++++++++++---- quickshell/Common/ShellLayout.qml | 5 ++- quickshell/tests/layout-resolver.test.mjs | 10 ++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/quickshell/Common/LayoutResolver.js b/quickshell/Common/LayoutResolver.js index c793b904a..09c7bbb4b 100644 --- a/quickshell/Common/LayoutResolver.js +++ b/quickshell/Common/LayoutResolver.js @@ -93,16 +93,40 @@ function resolveScreen(inputs, screen, options) { // A free island keeps its satellites in a bar window on its configured edge; that band never reserves or shadows. const satellites = !dot && (input.islandSatellites ?? input.config.islandSatellitesEnabled ?? true); instances.push({ - key: JSON.stringify([screen.name, input.config.id]), screenName: screen.name, - barId: input.config.id, configOrder: inputs.indexOf(input), edge: "", kind: "island", free: true, dot, hostsIsland: true, + key: JSON.stringify([screen.name, input.config.id]), + screenName: screen.name, + barId: input.config.id, + configOrder: inputs.indexOf(input), + edge: "", + kind: "island", + free: true, + dot, + hostsIsland: true, satelliteEdge: satellites ? edgeName(input.config.position ?? 0) || "top" : "", - row: 0, rowThickness: 0, rowOffset: 0, reservation: 0, exclusiveZone: -1, exclusionSize: 0, - paintedBounds: { x: 0, y: 0, width: 0, height: 0 }, margins: { top: 0, bottom: 0, left: 0, right: 0 } + row: 0, + rowThickness: 0, + rowOffset: 0, + reservation: 0, + exclusiveZone: -1, + exclusionSize: 0, + paintedBounds: { + x: 0, + y: 0, + width: 0, + height: 0 + }, + margins: { + top: 0, + bottom: 0, + left: 0, + right: 0 + } }); } for (const edge of edges) { const selected = enabled.filter(input => !isFree(input) && (edgeName(input.config.position ?? 0) || "top") === edge); const active = selected.filter(input => !isIsland(input.config)); + const shown = active.filter(input => input.config.visible !== false); const hosted = active.filter(input => !input.config.useOverlayLayer); let offset = 0; let reservation = 0; @@ -148,8 +172,9 @@ function resolveScreen(inputs, screen, options) { hostedBars: hosted.map(input => input.config), occupancy: offset, reservation: Math.max(frameStyled ? options.frameThickness : 0, reservation), - frameReservation: active.length ? Math.max(options.frameThickness, offset) : options.frameThickness, - frameExclusionEnabled: frameStyled && (!active.length || (frameHosted && !active.some(input => input.config.useOverlayLayer))) + frameReservation: shown.length ? Math.max(options.frameThickness, offset) : options.frameThickness, + overviewFrameReservation: active.length ? Math.max(options.frameThickness, offset) : options.frameThickness, + frameExclusionEnabled: frameStyled && (!shown.length || (frameHosted && !shown.some(input => input.config.useOverlayLayer))) }; } const manualPlacement = edges.some(edge => instances.filter(instance => instance.edge === edge).length > 1); diff --git a/quickshell/Common/ShellLayout.qml b/quickshell/Common/ShellLayout.qml index 2cd7fdd07..e8feff2de 100644 --- a/quickshell/Common/ShellLayout.qml +++ b/quickshell/Common/ShellLayout.qml @@ -97,7 +97,10 @@ Singleton { } function frameReservation(screen, side) { - return edge(screen, side)?.frameReservation ?? 0; + const band = edge(screen, side); + if (SettingsData.frameShowOnOverview && CompositorService.overviewActiveOnScreen(screen)) + return band?.overviewFrameReservation ?? 0; + return band?.frameReservation ?? 0; } function frameContentInset(screen, barId, side) { diff --git a/quickshell/tests/layout-resolver.test.mjs b/quickshell/tests/layout-resolver.test.mjs index 0aad91e7c..eb7ddc17f 100644 --- a/quickshell/tests/layout-resolver.test.mjs +++ b/quickshell/tests/layout-resolver.test.mjs @@ -120,6 +120,16 @@ test("frame overlays reserve once and every row retains its position", () => { assert.equal(resolve(configs, { effectiveFrameEnabled: true, effectiveConnected: true, framePreferences: [] }).instances[0].kind, "bar"); }); +test("a hidden bar collapses its frame band to a reserved gutter outside the overview", () => { + for (const effectiveConnected of [false, true]) { + const options = { effectiveFrameEnabled: true, effectiveConnected }; + const shown = resolve([bar("main")], options).edges.top; + const hidden = resolve([bar("main", 0, { visible: false })], options).edges.top; + assert.deepEqual([shown.frameReservation, shown.overviewFrameReservation], [48, 48]); + assert.deepEqual([hidden.frameReservation, hidden.frameExclusionEnabled, hidden.overviewFrameReservation], [12, true, 48]); + } +}); + test("popup triggers preserve each edge and connected gap policy", () => { const config = { bottomGap: 8, popupGapsAuto: false, popupGapsManual: 6 }; const expected = [{ x: 20, y: 58, width: 24 }, { x: 20, y: 1022, width: 24 }, { x: 50, y: 30, width: 24 }, { x: 1870, y: 30, width: 24 }];