From a84e84c39b6760c4e23bbd34bd5d7d2d7ef63688 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 21 Sep 2026 18:09:25 -0400 Subject: [PATCH] layout: let an overview-only bar share the row of a bar that hides in the overview fixes #3527 --- quickshell/Common/LayoutResolver.js | 121 ++++++++++++++++++---- quickshell/tests/layout-resolver.test.mjs | 16 +++ 2 files changed, 115 insertions(+), 22 deletions(-) diff --git a/quickshell/Common/LayoutResolver.js b/quickshell/Common/LayoutResolver.js index de70723c2..43870a27e 100644 --- a/quickshell/Common/LayoutResolver.js +++ b/quickshell/Common/LayoutResolver.js @@ -41,13 +41,25 @@ function islandMetrics(config, defaults) { const reserve = Math.max(24, Math.min(128, value("islandReserveThickness"))); const compact = Math.max(24, Math.min(72, value("islandCompactThickness"))); const gap = Math.max(0, Math.min(48, value("islandOuterGap"))); - return { reserve, compact, gap, thickness: Math.max(reserve, gap + compact) }; + return { + reserve, + compact, + gap, + thickness: Math.max(reserve, gap + compact) + }; } function islandThickness(config, defaults) { return islandMetrics(config, defaults).thickness; } +function overviewStandInHost(input, selected) { + const config = input.config; + if (config.island === true || config.visible !== false || !config.openOnOverview) + return null; + return selected.find(other => other.config.island !== true && !other.config.openOnOverview) ?? null; +} + function resolveScreen(inputs, screen, options) { const assigned = inputs.filter(input => coversScreen(input.config, screen, options.screens, options.displayNameMode)); const enabled = assigned.filter(input => input.config.enabled); @@ -64,30 +76,47 @@ function resolveScreen(inputs, screen, options) { const hosted = active.filter(input => !input.config.useOverlayLayer); let offset = 0; let reservation = 0; - for (const input of selected) { + const rowOffsets = new Map(); + const place = (input, rowOffset) => { const config = input.config; const kind = config.island === true ? "island" : frameHosted && !config.useOverlayLayer ? "frame" : "bar"; const spacing = frameStyled || config.attachToScreenEdge ? 0 : config.spacing ?? 4; const thickness = kind === "island" ? input.islandThickness : frameStyled ? Math.round(Math.round(options.frameBarSize * (screen.scale || 1)) / (screen.scale || 1)) : input.barThickness + spacing + (config.bottomGap ?? 0); const paintedThickness = thickness + (kind !== "island" && !frameStyled ? input.wingSize ?? 0 : 0); - const reserves = kind === "island" ? !(input.islandFloating ?? config.islandFloating) - : config.visible !== false && (!config.autoHide || frameStyled); - const contribution = reserves ? Math.max(0, offset + thickness - reservation) : 0; + const reserves = kind === "island" ? !(input.islandFloating ?? config.islandFloating) : config.visible !== false && (!config.autoHide || frameStyled); + const contribution = reserves ? Math.max(0, rowOffset + thickness - reservation) : 0; instances.push({ - key: JSON.stringify([screen.name, config.id]), screenName: screen.name, - barId: config.id, configOrder: inputs.indexOf(input), edge, kind, - row: selected.indexOf(input), rowThickness: paintedThickness, rowOffset: offset, + key: JSON.stringify([screen.name, config.id]), + screenName: screen.name, + barId: config.id, + configOrder: inputs.indexOf(input), + edge, + kind, + row: selected.indexOf(input), + rowThickness: paintedThickness, + rowOffset, reservation: contribution }); - offset += paintedThickness; reservation += contribution; + rowOffsets.set(input, rowOffset); + return paintedThickness; + }; + const standIns = frameStyled ? [] : selected.filter(input => overviewStandInHost(input, selected)); + for (const input of selected) { + if (standIns.includes(input)) + continue; + offset += place(input, offset); } + for (const input of standIns) + place(input, rowOffsets.get(overviewStandInHost(input, selected))); const island = selected.find(input => input.config.island === true); bands[edge] = { island: island?.config ?? null, islandThickness: selected.filter(input => input.config.island === true).reduce((sum, input) => sum + input.islandThickness, 0), - bars: active.map(input => input.config), hostedBars: hosted.map(input => input.config), - occupancy: offset, reservation: Math.max(frameStyled ? options.frameThickness : 0, reservation), + bars: active.map(input => input.config), + 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))) }; @@ -108,7 +137,12 @@ function resolveScreen(inputs, screen, options) { instance.exclusiveZone = manualPlacement || instance.kind === "frame" ? -1 : instance.reservation || -1; instance.exclusionSize = manualPlacement && !frameStyled && instance.row === 0 ? band.reservation : 0; instance.paintedBounds = rowBounds(screen, instance.edge, instance.rowOffset, instance.rowThickness, bands); - instance.margins = { top: 0, bottom: 0, left: 0, right: 0 }; + instance.margins = { + top: 0, + bottom: 0, + left: 0, + right: 0 + }; if (!manualPlacement) continue; instance.margins[instance.edge] = instance.rowOffset; @@ -122,7 +156,17 @@ function resolveScreen(inputs, screen, options) { bands[edge].frameExclusionEnabled = true; } instances.sort((a, b) => a.configOrder - b.configOrder); - return { screen, assigned, bars, islands, instances, edges: bands, frameConfigured, frameStyled, manualPlacement }; + return { + screen, + assigned, + bars, + islands, + instances, + edges: bands, + frameConfigured, + frameStyled, + manualPlacement + }; } function adjacentBar(layout, edge, config) { @@ -133,7 +177,12 @@ function adjacentBar(layout, edge, config) { } function adjacentInfo(layout, config, defaults) { - const result = { topBar: 0, bottomBar: 0, leftBar: 0, rightBar: 0 }; + const result = { + topBar: 0, + bottomBar: 0, + leftBar: 0, + rightBar: 0 + }; if (!layout || !config || config.autoHide) return result; for (const edge of edges) { @@ -154,11 +203,19 @@ function adjacentInfo(layout, config, defaults) { } function barBounds(layout, thickness, position, config, defaults, connected, wingSize) { - const empty = { x: 0, y: 0, width: 0, height: 0, wingSize: 0 }; + const empty = { + x: 0, + y: 0, + width: 0, + height: 0, + wingSize: 0 + }; if (!layout || !edgeName(position)) return empty; const instance = layout.instances.find(instance => instance.barId === config?.id); - return Object.assign(rowBounds(layout.screen, edgeName(position), instance?.rowOffset ?? 0, thickness + wingSize, layout.edges), { wingSize }); + return Object.assign(rowBounds(layout.screen, edgeName(position), instance?.rowOffset ?? 0, thickness + wingSize, layout.edges), { + wingSize + }); } function rowBounds(screen, edge, offset, thickness, bands) { @@ -168,7 +225,8 @@ function rowBounds(screen, edge, offset, thickness, bands) { return { x: horizontal ? 0 : edge === "right" ? screen.width - width - offset : offset, y: horizontal ? edge === "bottom" ? screen.height - height - offset : offset : bands.top.occupancy, - width, height + width, + height }; } @@ -184,7 +242,10 @@ function surfaceOrigin(layout, width, height, anchors, margins) { x += Math.min(Math.max(0, screen.width - width - left - right), layout.edges?.left?.reservation ?? 0); if (anchors.top && anchors.bottom) y += Math.min(Math.max(0, screen.height - height - top - bottom), layout.edges?.top?.reservation ?? 0); - return { x, y }; + return { + x, + y + }; } function popupTrigger(pos, screen, thickness, width, spacing, position, config, defaults, connected) { @@ -194,13 +255,29 @@ function popupTrigger(pos, screen, thickness, width, spacing, position, config, const offset = thickness + (connected ? 0 : spacing) + gap; switch (position) { case 2: - return { x: offset, y: pos.y, width }; + return { + x: offset, + y: pos.y, + width + }; case 3: - return { x: (screen?.width || 0) - offset, y: pos.y, width }; + return { + x: (screen?.width || 0) - offset, + y: pos.y, + width + }; case 1: - return { x: pos.x, y: (screen?.height || 0) - offset - bottomGap, width }; + return { + x: pos.x, + y: (screen?.height || 0) - offset - bottomGap, + width + }; default: - return { x: pos.x, y: offset + bottomGap, width }; + return { + x: pos.x, + y: offset + bottomGap, + width + }; } } diff --git a/quickshell/tests/layout-resolver.test.mjs b/quickshell/tests/layout-resolver.test.mjs index e32437abc..0136fcdd2 100644 --- a/quickshell/tests/layout-resolver.test.mjs +++ b/quickshell/tests/layout-resolver.test.mjs @@ -145,3 +145,19 @@ test("surface origins include native cross-edge exclusions without adding manual assert.equal(frameIsland.instances[0].rowOffset, 12); assert.equal(frameIsland.edges.top.reservation, 54); }); + +test("an overview-only bar shares the row of a same-edge bar that retracts for the overview", () => { + const main = bar("main"); + const standIn = bar("standin", 0, { visible: false, openOnOverview: true }); + const always = bar("always", 0, { openOnOverview: true }); + const offsets = layout => Object.fromEntries(layout.instances.map(instance => [instance.barId, instance.rowOffset])); + const shared = resolve([main, standIn]); + assert.deepEqual(offsets(shared), { main: 0, standin: 0 }); + assert.equal(shared.instances.find(instance => instance.barId === "standin").margins.top, 0); + assert.equal(shared.edges.top.occupancy, 44); + assert.equal(shared.edges.top.reservation, 44); + assert.deepEqual(offsets(resolve([standIn, main])), { standin: 0, main: 0 }); + assert.deepEqual(offsets(resolve([main, always, standIn])), { main: 0, always: 44, standin: 0 }); + assert.deepEqual(offsets(resolve([always, standIn])), { always: 0, standin: 44 }); + assert.deepEqual(offsets(resolve([main, standIn], { effectiveFrameEnabled: true })), { main: 0, standin: 48 }); +});