Keep overlay surfaces mapped so they open sharp (#13419)
The menu, emoji picker, clipboard, OSD, reminder flow and Wi-Fi QR code mapped a fresh surface on every open. Qt drew its first frames before Hyprland sent the surface's fractional scale: the pixel ratio stepped 2, 1, then 1.6, so on a 1.6 display each overlay showed blurry for ~350ms before going sharp. A new OverlayWindow keeps the surface. Hidden, it parks as a 1x1, input-less layer below windows, off the overlay layer so it never blocks direct scanout. Showing only resizes and raises it, so the scale is already settled. Content stays hidden until the surface has grown, so no 1x1 frame is stretched across the screen, and the window follows the focused monitor each time it is shown. The image picker's own fullscreen parking moves onto it too. Each parked overlay keeps a Qt window alive: at rest the shell holds ~40 MiB more RSS and ~20 MiB more GPU memory. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6762769bbf
commit
48de7823da
14 files changed
+164
-96
No files matched your search
@@ -81,8 +81,9 @@ layer_absent() {
|
||||
! layer_present "$1"
|
||||
}
|
||||
|
||||
# The image selector stays mapped between opens, parked on the bottom layer, so
|
||||
# it keeps its textures and fractional scale. Showing means on the overlay.
|
||||
# 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.
|
||||
layer_on_overlay() {
|
||||
hyprctl -j layers | jq -e --arg ns "$1" '[.[].levels["3"][]? | select(.namespace == $ns)] | length > 0'
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ source_file=$DEFAULTS_FILE
|
||||
original_position=$(jq -r '.bar.position // "top"' "$source_file")
|
||||
|
||||
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
|
||||
wait_until "root menu opens" 15 layer_present "omarchy-menu"
|
||||
wait_until "root menu opens" 15 layer_on_overlay "omarchy-menu"
|
||||
wait_until "root menu content is visible" 15 screen_contains "Apps"
|
||||
screenshot "success-menu-01-root"
|
||||
|
||||
@@ -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_absent "omarchy-menu"
|
||||
wait_until "menu closes after selecting a position" 15 layer_off_overlay "omarchy-menu"
|
||||
screenshot "success-menu-06-bar-left"
|
||||
|
||||
if ((config_existed)); then
|
||||
|
||||
@@ -24,12 +24,12 @@ open_and_close() {
|
||||
# Search and select an emoji. The host harness separately proves the shortcut
|
||||
# with a QMP hardware key chord, while this test focuses on UI behavior.
|
||||
omarchy-shell shell summon omarchy.emojis >/dev/null
|
||||
wait_until "emoji picker opens" 15 layer_present "omarchy-emojis"
|
||||
wait_until "emoji picker opens" 15 layer_on_overlay "omarchy-emojis"
|
||||
wtype "rocket"
|
||||
sleep 1
|
||||
screenshot "success-emoji-picker-search"
|
||||
wtype -k Return
|
||||
wait_until "emoji picker selection closes" 15 layer_absent "omarchy-emojis"
|
||||
wait_until "emoji picker selection closes" 15 layer_off_overlay "omarchy-emojis"
|
||||
|
||||
# Seed two clipboard entries, search for the older one, and copy it back out.
|
||||
clipboard_token="Omarchy acceptance clipboard $(date +%s)"
|
||||
@@ -39,21 +39,21 @@ printf '%s' "clipboard decoy" | wl-copy
|
||||
sleep 1
|
||||
|
||||
omarchy-shell shell summon omarchy.clipboard >/dev/null
|
||||
wait_until "clipboard opens" 15 layer_present "omarchy-clipboard"
|
||||
wait_until "clipboard opens" 15 layer_on_overlay "omarchy-clipboard"
|
||||
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_absent "omarchy-clipboard"
|
||||
wait_until "clipboard selection closes" 15 layer_off_overlay "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.
|
||||
omarchy-shell shell summon omarchy.menu '{"menu":"system"}' >/dev/null
|
||||
wait_until "system menu opens" 15 layer_present "omarchy-menu"
|
||||
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_absent "omarchy-menu"
|
||||
wait_until "system menu closes" 15 layer_off_overlay "omarchy-menu"
|
||||
|
||||
# Preview both visual selectors and cancel without changing user state. These
|
||||
# cover thumbnail generation, the image-grid overlay, and current selection.
|
||||
@@ -74,7 +74,7 @@ wait_until "theme selector closes" 15 layer_off_overlay "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.
|
||||
omarchy-shell shell summon omarchy.reminders >/dev/null
|
||||
wait_until "reminder flow opens" 15 layer_present "omarchy-reminders"
|
||||
wait_until "reminder flow opens" 15 layer_on_overlay "omarchy-reminders"
|
||||
screenshot "success-reminder-01-minutes-prompt"
|
||||
wtype "5"
|
||||
sleep 1
|
||||
@@ -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_absent "omarchy-reminders"
|
||||
wait_until "reminder flow closes" 15 layer_off_overlay "omarchy-reminders"
|
||||
|
||||
# Render a real shell notification and clear it through the notification IPC.
|
||||
omarchy-shell notifications dismissAll >/dev/null
|
||||
@@ -101,7 +101,7 @@ if window_present "(?i)omawrite" >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
omarchy-menu summon apps >/dev/null
|
||||
wait_until "apps menu opens" 15 layer_present "omarchy-menu"
|
||||
wait_until "apps menu opens" 15 layer_on_overlay "omarchy-menu"
|
||||
sleep 1
|
||||
screenshot "success-apps-menu-open"
|
||||
|
||||
@@ -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_absent "omarchy-menu"
|
||||
wait_until "apps menu closes after launching" 15 layer_off_overlay "omarchy-menu"
|
||||
|
||||
close_windows "(?i)omawrite"
|
||||
wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite"
|
||||
@@ -70,14 +70,8 @@ assert(
|
||||
'image picker leaves theme mode when another caller opens it'
|
||||
)
|
||||
assert(
|
||||
/PanelWindow \{[\s\S]*?visible: true[\s\S]*?mask: root\.opened \? null : closedMask[\s\S]*?WlrLayershell\.layer: root\.opened \? WlrLayer\.Overlay : WlrLayer\.Bottom/.test(imagePickerQml) &&
|
||||
/Region \{ id: closedMask \}/.test(imagePickerQml),
|
||||
'image picker keeps its surface mapped, parked input-less below windows while closed'
|
||||
)
|
||||
assert(
|
||||
/function openSelector[\s\S]*?targetScreen = focusedScreen\(\) \|\| targetScreen/.test(imagePickerQml) &&
|
||||
/screen: root\.targetScreen/.test(imagePickerQml),
|
||||
'image picker follows the focused monitor on each open'
|
||||
/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'
|
||||
)
|
||||
assert(
|
||||
/source: item\.sourceActivated && item\.thumbnailPath \? Util\.fileUrl\(item\.thumbnailPath\) : ""[\s\S]*asynchronous: false/.test(imagePickerQml),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
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.
|
||||
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'
|
||||
)
|
||||
|
||||
// Content stays hidden until the surface has grown, so no frame drawn at 1x1
|
||||
// is stretched across the screen.
|
||||
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'
|
||||
)
|
||||
|
||||
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 overlays = {
|
||||
'shell/plugins/menu/Menu.qml': 'shown: root.opened && root.rowsLoaded',
|
||||
'shell/plugins/emojis/Emojis.qml': 'shown: root.opened',
|
||||
'shell/plugins/clipboard/Clipboard.qml': 'shown: root.opened',
|
||||
'shell/plugins/osd/Osd.qml': 'shown: root.opened',
|
||||
'shell/plugins/reminders/ReminderFlow.qml': 'shown: root.opened',
|
||||
'shell/plugins/panels/wifiqr/Panel.qml': 'shown: root.opened',
|
||||
'shell/plugins/image-picker/ImagePicker.qml': 'shown: root.opened',
|
||||
}
|
||||
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`
|
||||
)
|
||||
}
|
||||
|
||||
// Window visibility never changes now, so close-time resets key off shown.
|
||||
const menuQml = read('shell/plugins/menu/Menu.qml')
|
||||
assert(
|
||||
menuQml.includes('onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }') &&
|
||||
menuQml.includes('if (shown && cardTop < 0) {') &&
|
||||
!/onVisibleChanged: if \(!visible\) \{ cardTop/.test(menuQml),
|
||||
'the menu unfreezes its layout when the overlay hides'
|
||||
)
|
||||
|
||||
// The OSD never takes the keyboard or input, shown or not.
|
||||
const osd = read('shell/plugins/osd/Osd.qml')
|
||||
assert(
|
||||
osd.includes('shownKeyboardFocus: WlrKeyboardFocus.None') && osd.includes('mask: Region {}'),
|
||||
'the OSD stays click-through and keyboard-free while shown'
|
||||
)
|
||||
JS
|
||||
Reference in new issue
Block a user