diff --git a/tests/smoke.sh b/tests/smoke.sh index 310c0fb..2a6f69f 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -24,28 +24,22 @@ search zzzz-no-match | grep -q '"id":"fallback-commands"' && pass "fallback sect count=$($cli call system.info | python3 -I -c 'import json,sys; print(json.load(sys.stdin)["data"]["candidates"])') (( count > 300 )) && pass "$count candidates (apps + Omarchy menu)" || bad "only $count candidates" -echo "toggle latency (show -> layer mapped, hide -> unmapped; budget 100 ms)" -sock="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" -events=$(mktemp) -socat -u "UNIX-CONNECT:$sock" - 2>/dev/null | while IFS= read -r line; do - case $line in *layer\>\>omacast*) echo "$(date +%s%N) ${line%%>>*}" >>"$events" ;; esac -done & -watcher=$! -sleep 0.3 -$cli hide +# The surface stays mapped between opens, so time the commit that takes or +# gives back the keyboard (Hyprland sends no event for it), polled over IPC. +echo "toggle latency (show -> keyboard taken, hide -> given back; budget 100 ms)" +ki() { hyprctl repl "for _, l in ipairs(hl.get_layers()) do if l.namespace == 'omacast' then return l.keyboard_interactivity end end return 0" 2>/dev/null; } +until_ki() { local deadline=$(( $(date +%s%N) + 1000000000 )); until [[ $(ki) == "$1" ]]; do (( $(date +%s%N) < deadline )) || return 1; done; } +$cli hide; sleep 0.3 worst=0 for i in 1 2 3 4 5; do - t0=$(date +%s%N); $cli show; sleep 0.3 - t1=$(date +%s%N); $cli hide; sleep 0.3 - open=$(grep openlayer "$events" | sed -n "${i}p" | cut -d' ' -f1) - close=$(grep closelayer "$events" | sed -n "${i}p" | cut -d' ' -f1) - if [[ -z $open || -z $close ]]; then bad "run $i: no layer event seen"; continue; fi - show_ms=$(( (open - t0) / 1000000 )); hide_ms=$(( (close - t1) / 1000000 )) + t0=$(date +%s%N); $cli show; until_ki 1 || { bad "run $i: launcher never took the keyboard"; continue; } + show_ms=$(( ($(date +%s%N) - t0) / 1000000 )); sleep 0.3 + t1=$(date +%s%N); $cli hide; until_ki 0 || { bad "run $i: launcher kept the keyboard"; continue; } + hide_ms=$(( ($(date +%s%N) - t1) / 1000000 )); sleep 0.3 printf ' run %d: show %3d ms, hide %3d ms\n' "$i" "$show_ms" "$hide_ms" (( show_ms > worst )) && worst=$show_ms (( hide_ms > worst )) && worst=$hide_ms done -kill "$watcher" 2>/dev/null; pkill -f "socat -u UNIX-CONNECT:$sock" 2>/dev/null; rm -f "$events" (( worst > 0 && worst < 100 )) && pass "worst toggle ${worst} ms" || bad "worst toggle ${worst} ms" exit $fail diff --git a/ui/LauncherWindow.qml b/ui/LauncherWindow.qml index 618a8ad..8a03702 100644 --- a/ui/LauncherWindow.qml +++ b/ui/LauncherWindow.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Hyprland import Quickshell.Wayland import qs.Commons import qs.Commons as Commons @@ -33,13 +34,30 @@ PanelWindow { signal dismissRequested() - visible: window.shown + // The surface stays mapped once opened: hidden, it is fully transparent, + // takes no input and no keyboard. Mapping a new surface costs ~130 ms (the + // output's size and scale arrive a few frames late); flipping these is one + // commit. It is remapped only to move to another monitor. + property bool mapped: false + visible: window.mapped + screen: window.pinnedScreen + property var pinnedScreen: null + function focusedScreen() { + var monitor = Hyprland.focusedMonitor + var screens = Quickshell.screens + for (var i = 0; i < screens.length; i++) { + if (monitor && screens[i].name === monitor.name) return screens[i] + } + return window.pinnedScreen || (screens.length ? screens[0] : null) + } + mask: window.shown ? null : noInput + Region { id: noInput } anchors { top: true; bottom: true; left: true; right: true } color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "omacast" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.keyboardFocus: window.shown ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None readonly property var general: { window.backend.storeRevision @@ -126,11 +144,26 @@ PanelWindow { if (typeof payload.query === "string") window.query = payload.query window.closePopups() window.historyIndex = -1 - window.rebuild(true) + // Hiding pops to root and rebuilds, so an unchanged root list is reused + // and the card shows on the next frame instead of after a search. + if (typeof payload.query === "string" || window.rootKey() !== window.builtRootKey) window.rebuild(true) + else window.select(window.nextSelectable(-1, 1), 0) Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } + // Hidden, the mapped surface draws nothing. + Binding { + target: window.contentItem + property: "opacity" + value: window.shown ? 1 : 0 + } + onShownChanged: { + if (window.shown) { + var target = window.focusedScreen() + if (target !== window.pinnedScreen) window.pinnedScreen = target + window.mapped = true + } if (window.shown) return window.searchSerial++ window.backend.cancelSearch() @@ -172,8 +205,17 @@ PanelWindow { } + // What the root list depends on; "" when not at an empty root. + property string builtRootKey: "" + function rootKey() { + return window.atRoot && window.query === "" && window.rootFilter === "all" + ? window.backend.candidatesRevision + ":" + window.backend.storeRevision : "" + } + function rebuild(resetSelection, restoreIndex) { var serial = ++window.searchSerial + window.builtRootKey = "" + var key = window.rootKey() window.backend.cancelSearch() pointerGate.reset() var keepId = !resetSelection && window.selectedRow ? window.selectedRow.itemId : "" @@ -185,6 +227,7 @@ PanelWindow { if (serial !== window.searchSerial || revision !== window.backend.candidatesRevision) return window.applySearch(result, keepId, restoreIndex) window.searchPending = false + if (result.ok) window.builtRootKey = key }) }