diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index b1827e4a..78d50668 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -27,8 +27,8 @@ Item { // diagnostics; the built-in bar does not otherwise need it. property var manifest: null // Mirrors the on-disk `bar-off` flag so the user can hide the bar without - // killing the entire shell. Wired to BarPanel.visible below; updated by the - // FileView watcher further down. + // killing the entire shell. Hidden panels stay mapped but park off-screen + // without an exclusion zone; updated by the FileView watcher further down. property bool barHidden: false property string home: Quickshell.env("HOME") property string stateHome: home + "/.local/state" @@ -956,13 +956,26 @@ Item { component BarPanel: PanelWindow { id: barWindow - visible: !root.barHidden && !remapGuard.remapping + // Hiding parks the bar just past its screen edge instead of unmapping it. + // Unmapping frees the layer surface and the whole scene graph, so every + // reveal has to rebuild them — new surface, re-shaped glyphs, re-uploaded + // textures — which measures ~150ms against ~20ms to tear down. Parking + // keeps the surface alive, so showing is only a margin change. + visible: !remapGuard.remapping + exclusionMode: root.barHidden ? ExclusionMode.Ignore : ExclusionMode.Auto ScreenMoveRemap { id: remapGuard window: barWindow } + margins { + top: root.barHidden && root.position === "top" ? -root.barSize : 0 + bottom: root.barHidden && root.position === "bottom" ? -root.barSize : 0 + left: root.barHidden && root.position === "left" ? -root.barSize : 0 + right: root.barHidden && root.position === "right" ? -root.barSize : 0 + } + anchors { top: root.position === "top" || root.vertical bottom: root.position === "bottom" || root.vertical diff --git a/test/acceptance.d/base-test.sh b/test/acceptance.d/base-test.sh index eff2a52d..717007c9 100644 --- a/test/acceptance.d/base-test.sh +++ b/test/acceptance.d/base-test.sh @@ -79,6 +79,34 @@ layer_absent() { ! layer_present "$1" } +# A layer can be mapped but parked off the monitor: the bar hides that way so +# revealing it does not have to rebuild the surface. Assert on geometry when +# what matters is that the user can actually see it. Layer boxes are local to +# their monitor and in logical coordinates, so compare them with local bounds +# derived from the monitor's scaled pixel size. +layer_on_screen() { + local monitors + monitors=$(hyprctl -j monitors) || return 1 + + hyprctl -j layers | jq -e --arg ns "$1" --argjson monitors "$monitors" ' + to_entries[] + | .key as $name + | .value as $levels + | ($monitors[] | select(.name == $name)) as $m + | (if ($m.transform // 0) % 2 == 1 then $m.height else $m.width end) / $m.scale | round as $width + | (if ($m.transform // 0) % 2 == 1 then $m.width else $m.height end) / $m.scale | round as $height + | [$levels | .. | objects | select(.namespace? == $ns)][] + | select( + .x + .w > 0 and .x < $width and + .y + .h > 0 and .y < $height + ) + ' >/dev/null +} + +layer_off_screen() { + ! layer_on_screen "$1" +} + # Close every window matching a class regex, by address so multi-window apps # are fully closed. Tries the quattro Lua dispatcher first, then classic. close_windows() { diff --git a/test/acceptance.d/session-test.sh b/test/acceptance.d/session-test.sh index 8d8f53b5..16c3171d 100644 --- a/test/acceptance.d/session-test.sh +++ b/test/acceptance.d/session-test.sh @@ -24,8 +24,25 @@ for plugin in \ done # The bar and background are actually on screen -wait_until "bar layer is on screen" 30 layer_present "omarchy-bar" -wait_until "background layer is on screen" 30 layer_present "omarchy-background" +wait_until "bar layer is on screen" 30 layer_on_screen "omarchy-bar" +wait_until "background layer is on screen" 30 layer_on_screen "omarchy-background" + +# Hiding parks the bar off-screen without unmapping its layer surface, and +# revealing brings that same surface back on-screen. +restore_bar_visibility() { + omarchy-toggle-bar off >/dev/null 2>&1 || true +} +trap restore_bar_visibility EXIT + +omarchy-toggle-bar on +wait_until "hidden bar layer stays mapped" 15 layer_present "omarchy-bar" +wait_until "hidden bar layer parks off screen" 15 layer_off_screen "omarchy-bar" +screenshot "success-bar-hidden" + +omarchy-toggle-bar off +wait_until "revealed bar layer returns on screen" 15 layer_on_screen "omarchy-bar" +screenshot "success-bar-revealed" +trap - EXIT # Audio stack is up wait_until "pipewire is running" 30 wpctl status diff --git a/test/shell.d/acceptance-helpers-test.sh b/test/shell.d/acceptance-helpers-test.sh new file mode 100644 index 00000000..eb32953a --- /dev/null +++ b/test/shell.d/acceptance-helpers-test.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +source "$(dirname "$0")/base-test.sh" +source "$ROOT/test/acceptance.d/base-test.sh" + +monitor_json='[ + {"name":"DP-1","x":1920,"y":0,"width":3840,"height":2160,"scale":2}, + {"name":"DP-2","x":-2560,"y":-1440,"width":2560,"height":1440,"scale":1}, + {"name":"DP-3","x":0,"y":1080,"width":1920,"height":1080,"scale":1,"transform":1} +]' +layer_json='{ + "DP-1":{"levels":{"2":[ + {"x":0,"y":0,"w":1920,"h":26,"namespace":"visible-positive-offset"}, + {"x":1920,"y":0,"w":26,"h":1080,"namespace":"parked-positive-offset"} + ]}}, + "DP-2":{"levels":{"2":[ + {"x":0,"y":0,"w":2560,"h":26,"namespace":"visible-negative-offset"}, + {"x":-26,"y":0,"w":26,"h":1440,"namespace":"parked-negative-offset"} + ]}}, + "DP-3":{"levels":{"2":[ + {"x":0,"y":1500,"w":26,"h":26,"namespace":"visible-rotated"}, + {"x":1080,"y":0,"w":26,"h":1920,"namespace":"parked-rotated"} + ]}} +}' + +hyprctl() { + if [[ $2 == "monitors" ]]; then + printf '%s\n' "$monitor_json" + elif [[ $2 == "layers" ]]; then + printf '%s\n' "$layer_json" + else + return 1 + fi +} + +assert_layer_on_screen() { + local namespace="$1" description="$2" + + layer_on_screen "$namespace" && pass "$description" || fail "$description" +} + +assert_layer_off_screen() { + local namespace="$1" description="$2" + + layer_off_screen "$namespace" && pass "$description" || fail "$description" +} + +assert_layer_on_screen "visible-positive-offset" "visible layer is found on a positively offset monitor" +assert_layer_off_screen "parked-positive-offset" "right-parked layer stays off a positively offset monitor" +assert_layer_on_screen "visible-negative-offset" "visible layer is found on a negatively offset monitor" +assert_layer_off_screen "parked-negative-offset" "left-parked layer stays off a negatively offset monitor" +assert_layer_on_screen "visible-rotated" "visible layer uses the transformed monitor height" +assert_layer_off_screen "parked-rotated" "parked layer uses the transformed monitor width" diff --git a/test/shell.d/bar-test.sh b/test/shell.d/bar-test.sh index ccac7fea..0dede450 100644 --- a/test/shell.d/bar-test.sh +++ b/test/shell.d/bar-test.sh @@ -22,6 +22,24 @@ const shellSource = fs.readFileSync(root + '/shell/shell.qml', 'utf8') assert(/function toggleBarTransparency\(\): string \{[\s\S]*?shell\.bar\.toggleTransparency\(\)/.test(shellSource), 'shell exposes the bar transparency toggle over IPC') +// Hiding must not unmap the bar. An unmapped layer surface has to be rebuilt on +// every reveal, which measured ~150ms against ~20ms to tear it down; parking it +// past the screen edge keeps show and hide symmetric at ~12ms. +assert( + /visible: !remapGuard\.remapping/.test(barSource), + 'bar stays mapped while hidden so revealing it does not rebuild the surface' +) +assert( + /exclusionMode: root\.barHidden \? ExclusionMode\.Ignore : ExclusionMode\.Auto/.test(barSource), + 'a hidden bar reserves no space for itself' +) +for (const edge of ['top', 'bottom', 'left', 'right']) { + assert( + new RegExp(`${edge}: root\\.barHidden && root\\.position === "${edge}" \\? -root\\.barSize : 0`).test(barSource), + `a hidden bar parks past the ${edge} edge` + ) +} + // The center section declares two arrangements and shows one; the hidden one // must not build its modules or every center widget exists twice. const moduleList = barSource.slice(barSource.indexOf('component ModuleList'), barSource.indexOf('component ModuleSlot'))