Keep overlays sharp without parked surfaces (#14458)
* Keep overlays sharp without parked surfaces * Refresh overlay monitor scale after live changes * Reset menu layout when its monitor changes
This commit is contained in:
1 parent
d9b970dd62
commit
0a55a3c554
9 files changed
+299
-69
No files matched your search
@@ -6,10 +6,9 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
|
||||
|
||||
-- Launcher, image selector, emojis, clipboard overlays, the OSD, reminders, the
|
||||
-- Wi-Fi QR code and keyboard-driven panels should pop without compositor layer
|
||||
-- animations. The overlays stay mapped between opens and grow from a parked
|
||||
-- 1x1 when shown, which Hyprland would otherwise animate as a slide in from
|
||||
-- the corner. Panels keep their own QML opacity transition for normal
|
||||
-- open/close, and skip it for panel handoff.
|
||||
-- animations. Overlays open as soon as their scale is ready, without a
|
||||
-- compositor fade or slide. Panels keep their own QML opacity transition
|
||||
-- for normal open/close, and skip it for panel handoff.
|
||||
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel|omarchy-osd|omarchy-reminders|omarchy-network-qr)$" }, no_anim = true, animation = "none" })
|
||||
|
||||
-- Dev gallery is the main shell workbench; open it maximized like
|
||||
|
||||
+57
-24
@@ -3,12 +3,9 @@ import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
|
||||
// A fullscreen overlay whose surface outlives each open. A fresh surface draws
|
||||
// its first frames before the compositor sends its fractional scale, so an
|
||||
// overlay mapped per open flashed blurry until the scale arrived. Closed, the
|
||||
// surface parks as a 1x1, input-less, content-less layer below windows: small
|
||||
// enough to cost nothing, off the overlay layer so it never blocks direct
|
||||
// scanout for fullscreen apps. Opening only resizes and raises it.
|
||||
// Map a fresh fullscreen surface on each open. Keep its first frames
|
||||
// transparent until Qt receives the output's scale, so it opens sharp without
|
||||
// retaining a parked surface that can be closed when a monitor disconnects.
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
@@ -17,11 +14,31 @@ PanelWindow {
|
||||
property int shownLayer: WlrLayer.Overlay
|
||||
property int shownKeyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
// The surface no longer lands on the focused output by being mapped there,
|
||||
// so it follows the focused monitor each time it is shown. Unset until the
|
||||
// first show lets the compositor choose.
|
||||
property var targetScreen: null
|
||||
property Region emptyRegion: Region {}
|
||||
readonly property var targetMonitor: {
|
||||
var monitors = Hyprland.monitors.values
|
||||
for (var i = 0; i < monitors.length; i++) {
|
||||
if (targetScreen && monitors[i].name === targetScreen.name) return monitors[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Wait for fullscreen geometry as well as scale so no smaller opening frame
|
||||
// is stretched across the output. Fractional scale uses units of 1/120;
|
||||
// compare in those units to tolerate floating point rounding.
|
||||
readonly property bool contentReady: shown && backingWindowVisible && !!targetScreen
|
||||
&& width === targetScreen.width && height === targetScreen.height
|
||||
&& !!targetMonitor && Math.round(devicePixelRatio * 120) === Math.round(targetMonitor.scale * 120)
|
||||
property bool contentRevealed: false
|
||||
|
||||
onContentReadyChanged: {
|
||||
contentRevealed = false
|
||||
// Resize and scale notifications arrive while Qt is still updating the
|
||||
// window. Reveal on the next turn, once its content layout has caught up.
|
||||
if (contentReady) Qt.callLater(function() {
|
||||
if (window.contentReady) window.contentRevealed = true
|
||||
})
|
||||
}
|
||||
|
||||
function focusedScreen() {
|
||||
var monitor = Hyprland.focusedMonitor
|
||||
@@ -29,28 +46,44 @@ PanelWindow {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === name) return Quickshell.screens[i]
|
||||
}
|
||||
return null
|
||||
return Quickshell.screens.length > 0 ? Quickshell.screens[0] : null
|
||||
}
|
||||
|
||||
onShownChanged: if (shown) targetScreen = focusedScreen() || targetScreen
|
||||
// Live monitor scaling does not emit a Hyprland monitor event, so its cached
|
||||
// scale can lag behind Qt. Refresh on opening and on scale changes while open.
|
||||
onShownChanged: {
|
||||
targetScreen = shown ? focusedScreen() : null
|
||||
if (shown) Hyprland.refreshMonitors()
|
||||
}
|
||||
onDevicePixelRatioChanged: if (shown) Hyprland.refreshMonitors()
|
||||
|
||||
visible: true
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (window.shown && Quickshell.screens.indexOf(window.targetScreen) < 0) {
|
||||
window.targetScreen = null
|
||||
// Let the removed output finish closing its old window before mapping
|
||||
// a replacement, rather than recreating it during Qt's teardown.
|
||||
Qt.callLater(function() {
|
||||
if (window.shown && !window.targetScreen) window.targetScreen = window.focusedScreen()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible: shown && !!targetScreen && !!targetMonitor
|
||||
screen: targetScreen
|
||||
anchors { top: true; left: true; bottom: shown; right: shown }
|
||||
implicitWidth: 1
|
||||
implicitHeight: 1
|
||||
mask: shown ? null : emptyRegion
|
||||
anchors { top: true; left: true; bottom: true; right: true }
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: shown ? shownLayer : WlrLayer.Bottom
|
||||
WlrLayershell.keyboardFocus: shown ? shownKeyboardFocus : WlrKeyboardFocus.None
|
||||
WlrLayershell.layer: shownLayer
|
||||
WlrLayershell.keyboardFocus: shownKeyboardFocus
|
||||
|
||||
// Draw nothing until the surface has actually grown. A frame drawn while it
|
||||
// is still 1x1 holds only the scrim's color, which the compositor would
|
||||
// stretch across the whole screen until the fullscreen frame arrives.
|
||||
// Opacity preserves keyboard handling while the scale arrives, including
|
||||
// search keystrokes and Escape pressed immediately after opening.
|
||||
Binding {
|
||||
target: window.contentItem
|
||||
property: "visible"
|
||||
value: window.shown && window.width > 1 && window.height > 1
|
||||
property: "opacity"
|
||||
value: window.contentRevealed ? 1 : 0
|
||||
}
|
||||
}
|
||||
@@ -1075,7 +1075,7 @@ Item {
|
||||
// then on the card grows and shrinks downward instead of re-centering
|
||||
// on every resize, which made the menu jump around. The rows height is
|
||||
// frozen at the same moment, so the starting menu also caps how tall the
|
||||
// card may grow from there. Closing unfreezes both.
|
||||
// card may grow from there. Closing or changing screens unfreezes both.
|
||||
property int cardTop: -1
|
||||
property int maxRowsHeight: -1
|
||||
readonly property int centeredTop: Math.max(Style.gapsOut, Math.round((height - root.cardHeight) / 2))
|
||||
@@ -1086,8 +1086,8 @@ Item {
|
||||
maxRowsHeight = root.visibleRowsHeight
|
||||
}
|
||||
}
|
||||
// The surface stays mapped between opens, so closing is shown going false.
|
||||
onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }
|
||||
onTargetScreenChanged: { cardTop = -1; maxRowsHeight = -1 }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -81,17 +81,11 @@ layer_absent() {
|
||||
! layer_present "$1"
|
||||
}
|
||||
|
||||
# Overlays built on OverlayWindow (the menu, image selector, emoji picker,
|
||||
# clipboard and the like) stay mapped between opens, parked 1x1 on the bottom
|
||||
# layer, so they keep their fractional scale. Showing means on the overlay.
|
||||
# Fullscreen overlays map on the overlay layer and unmap when closed.
|
||||
layer_on_overlay() {
|
||||
hyprctl -j layers | jq -e --arg ns "$1" '[.[].levels["3"][]? | select(.namespace == $ns)] | length > 0'
|
||||
}
|
||||
|
||||
layer_off_overlay() {
|
||||
! layer_on_overlay "$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
|
||||
|
||||
@@ -86,7 +86,7 @@ screenshot "success-menu-05-position-submenu"
|
||||
wtype -k Down -k Down -k Return
|
||||
wait_until "menu bar position changes to left" 20 bar_position_is "left"
|
||||
wait_until "menu bar becomes vertical" 20 bar_is_vertical
|
||||
wait_until "menu closes after selecting a position" 15 layer_off_overlay "omarchy-menu"
|
||||
wait_until "menu closes after selecting a position" 15 layer_absent "omarchy-menu"
|
||||
screenshot "success-menu-06-bar-left"
|
||||
|
||||
if ((config_existed)); then
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
original_monitors=$(hyprctl -j monitors)
|
||||
remaining_monitor=$(jq -r 'map(select(.focused))[0].name // .[0].name' <<<"$original_monitors")
|
||||
remaining_height=$(jq -r --arg name "$remaining_monitor" '.[] | select(.name == $name) | (if (.transform // 0) % 2 == 1 then .width else .height end) / .scale | ceil' <<<"$original_monitors")
|
||||
# Make the source tall enough that a frozen centered menu would land below
|
||||
# the remaining display, so a mapped but misplaced menu cannot pass recovery.
|
||||
tall_height=$((remaining_height * 2 + 1600))
|
||||
virtual_monitor=""
|
||||
|
||||
cleanup() {
|
||||
omarchy-shell shell hide omarchy.menu >/dev/null 2>&1 || true
|
||||
if [[ -n $virtual_monitor ]]; then
|
||||
hyprctl output remove "$virtual_monitor" >/dev/null 2>&1 || true
|
||||
fi
|
||||
hyprctl dispatch "hl.dsp.focus({ monitor = \"$remaining_monitor\" })" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
find_virtual_monitor() {
|
||||
virtual_monitor=$(hyprctl -j monitors | jq -r --argjson previous "$original_monitors" '[.[].name | select(. as $name | $previous | map(.name) | index($name) == null)][0] // empty')
|
||||
[[ -n $virtual_monitor ]]
|
||||
}
|
||||
|
||||
monitor_is_tall() {
|
||||
hyprctl -j monitors | jq -e --arg name "$virtual_monitor" --argjson height "$tall_height" 'any(.[]; .name == $name and .height == $height and .scale == 1)'
|
||||
}
|
||||
|
||||
monitor_is_focused() {
|
||||
hyprctl -j monitors | jq -e --arg name "$1" 'any(.[]; .name == $name and .focused)'
|
||||
}
|
||||
|
||||
menu_on_monitor() {
|
||||
hyprctl -j layers | jq -e --arg name "$1" '[.[$name].levels["3"][]? | select(.namespace == "omarchy-menu")] | length == 1'
|
||||
}
|
||||
|
||||
monitor_contains() {
|
||||
local monitor="$1" text="$2"
|
||||
local snapshot="$ARTIFACTS/monitor-text-$$.png"
|
||||
local status=0
|
||||
|
||||
if timeout 10 grim -o "$monitor" -s 2 "$snapshot" 2>/dev/null; then
|
||||
tesseract "$snapshot" stdout --psm 11 2>/dev/null | grep -Fi -- "$text" >/dev/null || status=$?
|
||||
else
|
||||
status=1
|
||||
fi
|
||||
rm -f "$snapshot"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
create_source_monitor() {
|
||||
hyprctl output create headless >/dev/null
|
||||
wait_until "temporary overlay monitor appears" 15 find_virtual_monitor
|
||||
hyprctl eval "hl.monitor({ output = \"$virtual_monitor\", mode = \"1920x${tall_height}@60\", position = \"auto\", scale = 1 })" >/dev/null
|
||||
wait_until "temporary overlay monitor has tall geometry" 15 monitor_is_tall
|
||||
hyprctl dispatch "hl.dsp.focus({ monitor = \"$virtual_monitor\" })" >/dev/null
|
||||
wait_until "temporary overlay monitor is focused" 15 monitor_is_focused "$virtual_monitor"
|
||||
}
|
||||
|
||||
remove_source_monitor() {
|
||||
hyprctl output remove "$virtual_monitor" >/dev/null
|
||||
virtual_monitor=""
|
||||
hyprctl dispatch "hl.dsp.focus({ monitor = \"$remaining_monitor\" })" >/dev/null
|
||||
wait_until "remaining monitor is focused" 15 monitor_is_focused "$remaining_monitor"
|
||||
}
|
||||
|
||||
omarchy-shell shell hide omarchy.menu >/dev/null
|
||||
wait_until "menu starts closed" 15 layer_absent "omarchy-menu"
|
||||
|
||||
# A closed overlay must drop its old output and reopen on a connected one.
|
||||
create_source_monitor
|
||||
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
|
||||
wait_until "menu opens on temporary monitor" 15 menu_on_monitor "$virtual_monitor"
|
||||
wait_until "menu renders on temporary monitor" 15 monitor_contains "$virtual_monitor" "Apps"
|
||||
screenshot "success-overlay-monitor-01-closed-source"
|
||||
omarchy-shell shell hide omarchy.menu >/dev/null
|
||||
wait_until "menu unmaps before monitor removal" 15 layer_absent "omarchy-menu"
|
||||
remove_source_monitor
|
||||
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
|
||||
wait_until "closed menu reopens on remaining monitor" 15 menu_on_monitor "$remaining_monitor"
|
||||
wait_until "reopened menu renders on remaining monitor" 15 monitor_contains "$remaining_monitor" "Apps"
|
||||
screenshot "success-overlay-monitor-02-closed-recovered"
|
||||
omarchy-shell shell hide omarchy.menu >/dev/null
|
||||
wait_until "reopened menu closes" 15 layer_absent "omarchy-menu"
|
||||
|
||||
# Typing freezes the menu's position. Recovery onto a shorter screen must
|
||||
# unfreeze that position while preserving the search and keyboard handling.
|
||||
create_source_monitor
|
||||
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
|
||||
wait_until "open recovery menu maps on tall monitor" 15 menu_on_monitor "$virtual_monitor"
|
||||
wait_until "open recovery menu renders" 15 monitor_contains "$virtual_monitor" "Apps"
|
||||
screenshot "success-overlay-monitor-03-open-root"
|
||||
wtype "keybindings"
|
||||
wait_until "menu search renders on tall monitor" 15 monitor_contains "$virtual_monitor" "Keybindings"
|
||||
screenshot "success-overlay-monitor-04-open-search"
|
||||
remove_source_monitor
|
||||
wait_until "open menu moves to remaining monitor" 15 menu_on_monitor "$remaining_monitor"
|
||||
wait_until "open menu search renders on shorter monitor" 15 monitor_contains "$remaining_monitor" "Keybindings"
|
||||
screenshot "success-overlay-monitor-05-open-recovered"
|
||||
wtype -k Escape
|
||||
wait_until "recovered menu clears search through keyboard" 15 monitor_contains "$remaining_monitor" "Apps"
|
||||
wtype -k Escape
|
||||
wait_until "recovered menu closes through keyboard" 15 layer_absent "omarchy-menu"
|
||||
@@ -29,7 +29,7 @@ wtype "rocket"
|
||||
sleep 1
|
||||
screenshot "success-emoji-picker-search"
|
||||
wtype -k Return
|
||||
wait_until "emoji picker selection closes" 15 layer_off_overlay "omarchy-emojis"
|
||||
wait_until "emoji picker selection closes" 15 layer_absent "omarchy-emojis"
|
||||
|
||||
# Seed two clipboard entries, search for the older one, and copy it back out.
|
||||
clipboard_token="Omarchy acceptance clipboard $(date +%s)"
|
||||
@@ -44,7 +44,7 @@ wtype "$clipboard_token"
|
||||
wait_until "clipboard search finds test text" 15 screen_contains "Omarchy acceptance clipboard"
|
||||
screenshot "success-clipboard-search"
|
||||
wtype -M shift -k Return -m shift
|
||||
wait_until "clipboard selection closes" 15 layer_off_overlay "omarchy-clipboard"
|
||||
wait_until "clipboard selection closes" 15 layer_absent "omarchy-clipboard"
|
||||
wait_until "clipboard selection restores test text" 15 bash -c '[[ $(wl-paste --no-newline) == "$1" ]]' _ "$clipboard_token"
|
||||
|
||||
# Exercise the system branch without invoking any destructive action.
|
||||
@@ -53,7 +53,7 @@ wait_until "system menu opens" 15 layer_on_overlay "omarchy-menu"
|
||||
wait_until "system menu content is visible" 15 screen_contains "Shutdown"
|
||||
screenshot "success-system-menu"
|
||||
wtype -k Escape
|
||||
wait_until "system menu closes" 15 layer_off_overlay "omarchy-menu"
|
||||
wait_until "system menu closes" 15 layer_absent "omarchy-menu"
|
||||
|
||||
# Preview both visual selectors and cancel without changing user state. These
|
||||
# cover thumbnail generation, the image-grid overlay, and current selection.
|
||||
@@ -62,14 +62,14 @@ wait_until "background selector opens" 30 layer_on_overlay "omarchy-image-select
|
||||
sleep 1
|
||||
screenshot "success-background-selector"
|
||||
wtype -k Escape
|
||||
wait_until "background selector closes" 15 layer_off_overlay "omarchy-image-selector"
|
||||
wait_until "background selector closes" 15 layer_absent "omarchy-image-selector"
|
||||
|
||||
launch_app "omarchy-theme-switcher"
|
||||
wait_until "theme selector opens" 30 layer_on_overlay "omarchy-image-selector"
|
||||
sleep 1
|
||||
screenshot "success-theme-selector"
|
||||
wtype -k Escape
|
||||
wait_until "theme selector closes" 15 layer_off_overlay "omarchy-image-selector"
|
||||
wait_until "theme selector closes" 15 layer_absent "omarchy-image-selector"
|
||||
|
||||
# Walk the reminder flow through each input screen, but dismiss before it
|
||||
# schedules a real timer in the test user's session.
|
||||
@@ -83,7 +83,7 @@ wtype -k Return
|
||||
wait_until "reminder message prompt opens" 15 screen_contains "Reminder message"
|
||||
screenshot "success-reminder-03-message-prompt"
|
||||
wtype -k Escape
|
||||
wait_until "reminder flow closes" 15 layer_off_overlay "omarchy-reminders"
|
||||
wait_until "reminder flow closes" 15 layer_absent "omarchy-reminders"
|
||||
|
||||
# Render a real shell notification and clear it through the notification IPC.
|
||||
omarchy-shell notifications dismissAll >/dev/null
|
||||
@@ -111,7 +111,7 @@ screenshot "success-apps-menu-search"
|
||||
wtype -k Return
|
||||
|
||||
wait_until "apps menu launches the top search hit" 60 window_present "(?i)omawrite"
|
||||
wait_until "apps menu closes after launching" 15 layer_off_overlay "omarchy-menu"
|
||||
wait_until "apps menu closes after launching" 15 layer_absent "omarchy-menu"
|
||||
|
||||
close_windows "(?i)omawrite"
|
||||
wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite"
|
||||
@@ -147,7 +147,7 @@ assert(
|
||||
)
|
||||
assert(
|
||||
/OverlayWindow \{\s*id: panel\s*shown: root\.opened\s*shownKeyboardFocus: root\.imagesLoaded \? WlrKeyboardFocus\.Exclusive : WlrKeyboardFocus\.None/.test(imagePickerQml),
|
||||
'image picker parks on OverlayWindow and takes the keyboard once images load'
|
||||
'image picker uses OverlayWindow and takes the keyboard once images load'
|
||||
)
|
||||
assert(
|
||||
/model: visibleImages/.test(imagePickerQml) &&
|
||||
|
||||
@@ -9,31 +9,122 @@ const fs = require('fs')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
const overlay = read('shell/Ui/OverlayWindow.qml')
|
||||
|
||||
// Closed, the surface parks 1x1, input-less and without keyboard below
|
||||
// windows: off the overlay layer, so it never blocks direct scanout.
|
||||
// Closing must unmap, not leave a surface on an output that may disappear.
|
||||
assert(
|
||||
overlay.includes('visible: true') &&
|
||||
overlay.includes('anchors { top: true; left: true; bottom: shown; right: shown }') &&
|
||||
overlay.includes('implicitWidth: 1') &&
|
||||
overlay.includes('implicitHeight: 1') &&
|
||||
overlay.includes('mask: shown ? null : emptyRegion') &&
|
||||
overlay.includes('WlrLayershell.layer: shown ? shownLayer : WlrLayer.Bottom') &&
|
||||
overlay.includes('WlrLayershell.keyboardFocus: shown ? shownKeyboardFocus : WlrKeyboardFocus.None'),
|
||||
'overlay window parks a 1x1 input-less surface below windows while hidden'
|
||||
overlay.includes('visible: shown && !!targetScreen && !!targetMonitor') &&
|
||||
overlay.includes('anchors { top: true; left: true; bottom: true; right: true }') &&
|
||||
!overlay.includes('implicitWidth: 1') && !overlay.includes('WlrLayer.Bottom'),
|
||||
'closed overlays unmap instead of parking a surface'
|
||||
)
|
||||
|
||||
// Content stays hidden until the surface has grown, so no frame drawn at 1x1
|
||||
// is stretched across the screen.
|
||||
const readyExpression = /readonly property bool contentReady: ([^\n]*(?:\n &&[^\n]*)*)/.exec(overlay)[1]
|
||||
const ready = new Function('shown', 'backingWindowVisible', 'width', 'height', 'targetScreen', 'targetMonitor', 'devicePixelRatio', `return ${readyExpression}`)
|
||||
const screen = { name: 'eDP-1', width: 1200, height: 750 }
|
||||
const monitor = { name: 'eDP-1', scale: 1.6 }
|
||||
for (const scale of [2, 1, 1.6]) {
|
||||
assertEqual(ready(true, true, 1200, 750, screen, monitor, scale), scale === 1.6,
|
||||
`opening frames at ${scale}x wait for the monitor's 1.6x scale`)
|
||||
}
|
||||
for (const scale of [1, 1.25, 1.5, 2]) {
|
||||
assert(ready(true, true, 1200, 750, screen, { scale }, scale),
|
||||
`overlay reveals at the correct ${scale}x scale`)
|
||||
}
|
||||
assert(ready(true, true, 1200, 750, screen, { scale: 1.333333 }, 160 / 120),
|
||||
'scale comparison tolerates compositor floating point rounding')
|
||||
assert(!ready(true, true, 500, 500, screen, monitor, 1.6),
|
||||
'initial window geometry never stretches content across the screen')
|
||||
assert(!ready(true, false, 1200, 750, screen, monitor, 1.6),
|
||||
'content waits for a mapped backing window')
|
||||
assert(!ready(false, true, 1200, 750, screen, monitor, 1.6),
|
||||
'closed content stays transparent even at the correct scale')
|
||||
assert(!ready(true, true, 1200, 750, null, monitor, 1.6) &&
|
||||
!ready(true, true, 1200, 750, screen, null, 1.6),
|
||||
'content waits for a connected screen and its monitor information')
|
||||
assert(
|
||||
/target: window\.contentItem\s*property: "visible"\s*value: window\.shown && window\.width > 1 && window\.height > 1/.test(overlay),
|
||||
'overlay window draws nothing until the surface has grown'
|
||||
/target: window\.contentItem\s*property: "opacity"\s*value: window\.contentRevealed \? 1 : 0/.test(overlay),
|
||||
'transparent opening frames preserve immediate keyboard handling'
|
||||
)
|
||||
|
||||
assert(
|
||||
/onShownChanged: if \(shown\) targetScreen = focusedScreen\(\) \|\| targetScreen/.test(overlay) &&
|
||||
overlay.includes('screen: targetScreen'),
|
||||
'overlay window follows the focused monitor each time it is shown'
|
||||
)
|
||||
const revealBody = /onContentReadyChanged: \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
const reveal = new Function('window', 'Qt', `with (window) { ${revealBody} }`)
|
||||
const reveals = []
|
||||
const revealQt = { callLater(callback) { reveals.push(callback) } }
|
||||
const content = { contentReady: true, contentRevealed: false }
|
||||
reveal(content, revealQt)
|
||||
assert(!content.contentRevealed, 'scale and resize notifications finish before content reveals')
|
||||
reveals.shift()()
|
||||
assert(content.contentRevealed, 'settled content reveals on the next event-loop turn')
|
||||
content.contentReady = false
|
||||
reveal(content, revealQt)
|
||||
assert(!content.contentRevealed, 'losing readiness immediately hides content')
|
||||
content.contentReady = true
|
||||
reveal(content, revealQt)
|
||||
content.contentReady = false
|
||||
reveal(content, revealQt)
|
||||
reveals.shift()()
|
||||
assert(!content.contentRevealed, 'a close or screen loss cancels a pending reveal')
|
||||
|
||||
const focusedScreenBody = /function focusedScreen\(\) \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
const focusedScreen = new Function('Hyprland', 'Quickshell', focusedScreenBody)
|
||||
const external = { name: 'DP-1', width: 1920, height: 1080 }
|
||||
assertEqual(focusedScreen({ focusedMonitor: { name: 'DP-1' } }, { screens: [screen, external] }), external,
|
||||
'overlay selects the focused output')
|
||||
assertEqual(focusedScreen({ focusedMonitor: { name: 'DP-1' } }, { screens: [screen] }), screen,
|
||||
'disconnecting the focused output falls back to a connected screen')
|
||||
assertEqual(focusedScreen({ focusedMonitor: null }, { screens: [] }), null,
|
||||
'no outputs leaves the overlay unmapped')
|
||||
|
||||
const shownHandler = /onShownChanged: \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
let refreshed = 0
|
||||
const Hyprland = { refreshMonitors() { refreshed++ } }
|
||||
const changeShown = new Function('shown', 'focusedScreen', 'Hyprland', `let targetScreen = null; ${shownHandler}; return targetScreen`)
|
||||
assertEqual(changeShown(false, () => external, Hyprland), null, 'closing drops the previous screen reference')
|
||||
assertEqual(refreshed, 0, 'closing does not refresh monitor data')
|
||||
assertEqual(changeShown(true, () => screen, Hyprland), screen, 'reopening selects a current screen')
|
||||
assertEqual(refreshed, 1, 'opening refreshes monitor scales after a live change while closed')
|
||||
const scaleHandler = /onDevicePixelRatioChanged: ([^\n]+)/.exec(overlay)[1]
|
||||
const changeScale = new Function('shown', 'Hyprland', scaleHandler)
|
||||
changeScale(false, Hyprland)
|
||||
assertEqual(refreshed, 1, 'scale changes on hidden windows do not refresh monitor data')
|
||||
const changedScreen = { name: 'eDP-1', width: 960, height: 600 }
|
||||
assert(!ready(true, true, 960, 600, changedScreen, monitor, 2),
|
||||
'a stale monitor scale keeps content transparent after a live scale change')
|
||||
Hyprland.refreshMonitors = () => { refreshed++; monitor.scale = 2 }
|
||||
changeScale(true, Hyprland)
|
||||
assertEqual(refreshed, 2, 'a showing window refreshes monitor scales when Qt receives a live change')
|
||||
assert(ready(true, true, 960, 600, changedScreen, monitor, 2),
|
||||
'fresh monitor data lets content reveal at the new scale')
|
||||
|
||||
const screensChangedBody = /function onScreensChanged\(\) \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
const screensChanged = new Function('window', 'Quickshell', 'Qt', screensChangedBody)
|
||||
const pending = []
|
||||
const Qt = { callLater(callback) { pending.push(callback) } }
|
||||
const window = { shown: true, targetScreen: external, focusedScreen: () => screen }
|
||||
screensChanged(window, { screens: [screen] }, Qt)
|
||||
assertEqual(window.targetScreen, null, 'screen removal unmaps before Qt finishes destroying the old window')
|
||||
pending.shift()()
|
||||
assertEqual(window.targetScreen, screen, 'a showing overlay moves off a disconnected screen')
|
||||
window.targetScreen = screen
|
||||
window.focusedScreen = () => external
|
||||
screensChanged(window, { screens: [screen, external] }, Qt)
|
||||
assertEqual(window.targetScreen, screen, 'connecting another screen does not move a showing overlay')
|
||||
window.shown = false
|
||||
window.targetScreen = null
|
||||
screensChanged(window, { screens: [screen, external] }, Qt)
|
||||
assertEqual(window.targetScreen, null, 'screen changes leave a closed overlay unmapped')
|
||||
window.shown = true
|
||||
window.targetScreen = external
|
||||
screensChanged(window, { screens: [screen] }, Qt)
|
||||
window.shown = false
|
||||
pending.shift()()
|
||||
assertEqual(window.targetScreen, null, 'closing before a deferred remap prevents a stale reopen')
|
||||
window.shown = true
|
||||
window.targetScreen = external
|
||||
screensChanged(window, { screens: [screen] }, Qt)
|
||||
window.targetScreen = screen
|
||||
window.focusedScreen = () => external
|
||||
pending.shift()()
|
||||
assertEqual(window.targetScreen, screen, 'a deferred remap preserves the screen chosen by a newer open')
|
||||
|
||||
const overlays = {
|
||||
'shell/plugins/menu/Menu.qml': 'shown: root.opened && root.rowsLoaded',
|
||||
@@ -48,11 +139,11 @@ for (const [file, shown] of Object.entries(overlays)) {
|
||||
const qml = read(file)
|
||||
assert(
|
||||
qml.includes('OverlayWindow {') && qml.includes(shown) && !/PanelWindow \{\s*(id: panel\s*)?visible: root\.opened/.test(qml),
|
||||
`${file} keeps its surface through OverlayWindow`
|
||||
`${file} uses the shared overlay scale and lifecycle handling`
|
||||
)
|
||||
}
|
||||
|
||||
// Window visibility never changes now, so close-time resets key off shown.
|
||||
// Layout resets on a requested close, independent of mapping readiness.
|
||||
const menuQml = read('shell/plugins/menu/Menu.qml')
|
||||
assert(
|
||||
menuQml.includes('onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }') &&
|
||||
@@ -60,9 +151,15 @@ assert(
|
||||
!/onVisibleChanged: if \(!visible\) \{ cardTop/.test(menuQml),
|
||||
'the menu unfreezes its layout when the overlay hides'
|
||||
)
|
||||
const screenHandler = /onTargetScreenChanged: \{([^\n]*)\}/.exec(menuQml)[1]
|
||||
const changeScreen = new Function('panel', `with (panel) { ${screenHandler} }`)
|
||||
const menuPanel = { shown: true, cardTop: 945, maxRowsHeight: 510 }
|
||||
changeScreen(menuPanel)
|
||||
assertEqual(menuPanel.cardTop, -1, 'moving an open menu off a disconnected output unfreezes its old top edge')
|
||||
assertEqual(menuPanel.maxRowsHeight, -1, 'moving an open menu drops the old output row-height limit')
|
||||
assert(menuPanel.shown, 'resetting the layout keeps the menu open')
|
||||
|
||||
// A kept surface grows from its parked 1x1 when shown, which Hyprland animates
|
||||
// as a slide in from the corner unless its layer rule turns animation off.
|
||||
// Fullscreen overlays open without an additional compositor animation.
|
||||
const shellRules = read('default/hypr/apps/omarchy-shell.lua')
|
||||
const noAnim = /namespace = "\^\(([^)]*)\)\$" \}, no_anim = true/.exec(shellRules)
|
||||
assert(noAnim, 'the shell overlays share one no-animation layer rule')
|
||||
|
||||
Reference in new issue
Block a user