From ca93c4262845feed7c247b8836625d00f7ab68ee Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 23 Oct 2026 01:23:42 -0700 Subject: [PATCH 1/3] A touchscreen drives the pointer: a tap is a click, a drag a drag dwl 0.8 drops touch devices, so a laptop's touchscreen did nothing. dwl now attaches one to its cursor and turns the first finger into the pointer: down moves it there and presses the left button, moving drags, lifting releases. The press goes through dwl's own buttonpress, so a tap picks a window as a click does, under the same focus rules and the lock's. Zones are offered no touch, only those pointer events, so the proxy and its policy are unchanged. A touchscreen covers the built-in panel (eDP, LVDS or DSI) when there is one, and every screen otherwise. The desktop suite proves it on QEMU's virtio-multitouch: run-ovmf.sh's --touch adds one, bound to the GPU's first head; vm-drive.py's touch: step taps it through QMP; and gui-check's touch-picks-window has a tap give an unasked zone window the keyboard. vm-drive's fixture checks the step's events. --- build/guest-tests/gui-check.sh | 28 +++++++ docs/user-guide.md | 5 +- tools/desktop/dwl-zone-borders.py | 123 ++++++++++++++++++++++++++++++ tools/image/gui-test.sh | 5 +- tools/image/run-ovmf.sh | 17 ++++- tools/image/vm-drive.py | 23 ++++++ tools/tests/vm-drive.py | 24 ++++++ 7 files changed, 219 insertions(+), 6 deletions(-) diff --git a/build/guest-tests/gui-check.sh b/build/guest-tests/gui-check.sh index 77b566fc..239dea6c 100755 --- a/build/guest-tests/gui-check.sh +++ b/build/guest-tests/gui-check.sh @@ -21,6 +21,7 @@ # GT CONSENT-CODE 1 NN type NN and Enter (the question's code) # GT CONSENT-WAIT 2 type y and Enter (not the code: refused) # GT POINTER-ZONE0, GT POINTER-UNTRUSTED move the pointer onto the newest window +# GT TOUCH-UNTRUSTED tap the screen's left half (the newest window) # GT SCREENSHOT-POPUP take a screenshot # GT KEY-MENU-POPUP press Alt+p (the chrome menu) # GT KEY-FOCUS-POPUP press Alt+j @@ -1184,6 +1185,33 @@ if since_mark cursor untrusted | grep -q 'asked for the text cursor by name' && else fail "zone-cursor-by-name" "$(since_mark cursor untrusted | tail -4 | tr '\n' ' ')" fi +# --- a touchscreen drives the pointer ------------------------------------------------------- +# dwl takes a touchscreen as the pointer, so a tap is a click: it gives a window the keyboard as +# a click does, and zones are offered no touch. The VM's touchscreen is QEMU's virtio-multitouch +# (run-ovmf.sh --touch). untrusted's window maps beside zone 0's, unasked, where dwl puts the +# newest: the screen's left half, which the host taps. +stop_zone untrusted +as_user "/usr/libexec/kryptik/wlprobe cursor 40" > "$LOG/touch-zone0.out" 2>&1 & +touch0=$! +wait_for 20 grep -q committed "$LOG/touch-zone0.out" +wait_for 10 grep -qx 'zone=0' "$RT/kryptik/focus" +mark touch untrusted +launch_plain untrusted "/usr/libexec/kryptik/wlprobe cursor 30" > "$LOG/launch-touch.out" 2>&1 +wait_for 20 probe_committed touch +sleep 2 +touch_before="$(grep '^zone=' "$RT/kryptik/focus" 2>/dev/null)" +echo "GT TOUCH-UNTRUSTED" +if ! grep -q 'Name="QEMU Virtio MultiTouch"' /proc/bus/input/devices; then + fail "touch-picks-window" "no touchscreen in this guest: $(grep '^N: ' /proc/bus/input/devices | tr '\n' ' ')" +elif [[ "$touch_before" != "zone=0" ]]; then + fail "touch-picks-window" "zone 0's window did not have the keyboard before the tap: ${touch_before:-no focus record}" +elif wait_for 10 grep -qx 'zone=untrusted' "$RT/kryptik/focus"; then + pass "touch-picks-window" "a tap on untrusted's window gave it the keyboard, as a click does" +else + fail "touch-picks-window" "after the tap: $(tr '\n' ' ' < "$RT/kryptik/focus" 2>/dev/null); $(since_mark touch untrusted | tail -2 | tr '\n' ' ')" +fi +pkill -u "$USER_NAME" -f 'wlprobe cursor 40' 2>/dev/null; wait "$touch0" 2>/dev/null + # --- a zone's window hears the scale it is shown at ------------------------------------------ # wp_fractional_scale_v1 through the proxy: the compositor tells each window its output's scale, # in 120ths, so a browser draws text sharp at 1.5 instead of being scaled down from 2. This diff --git a/docs/user-guide.md b/docs/user-guide.md index 1ab04fe9..32171738 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -287,7 +287,10 @@ The menu shows the zone's glyph and label and the window's title, which the zone's proxy prefixes with the zone's name, as `[untrusted] ...`. A zone's window that opens while another window has the keyboard does not take it, nor does it get it when that window closes: Alt+j, Alt+k, a click or the -pointer moving onto it moves the keyboard there. Keys (Alt is the modifier): +pointer moving onto it moves the keyboard there. A touchscreen works as the +pointer, on the built-in screen when there is one: a tap is a click and a +drag drags with one finger, and zones are given pointer events only, not +touch. Keys (Alt is the modifier): | keys | what | | --- | --- | diff --git a/tools/desktop/dwl-zone-borders.py b/tools/desktop/dwl-zone-borders.py index c952592e..c64d97b2 100755 --- a/tools/desktop/dwl-zone-borders.py +++ b/tools/desktop/dwl-zone-borders.py @@ -1271,6 +1271,129 @@ \tif (lidshut && undocktimer) \t\twl_event_source_timer_update(undocktimer, 5000); } +"""), + # A touchscreen drives the pointer, where dwl 0.8 drops touch devices. A finger down moves + # the pointer there and presses its left button, moving drags, and lifting releases it: a tap + # clicks through dwl's own pointer path, its focus rules and the lock's. Zones are offered no + # touch, only those pointer events, so the proxy and its policy stay as they are. + ("""#include +""", + """#include +#include +"""), + ("""static struct wl_listener cursor_motion_absolute = {.notify = motionabsolute}; +""", + """static struct wl_listener cursor_motion_absolute = {.notify = motionabsolute}; +static void touchdown(struct wl_listener *listener, void *data); +static void touchmotion(struct wl_listener *listener, void *data); +static void touchup(struct wl_listener *listener, void *data); +static void touchcancel(struct wl_listener *listener, void *data); +static struct wl_listener cursor_touch_down = {.notify = touchdown}; +static struct wl_listener cursor_touch_motion = {.notify = touchmotion}; +static struct wl_listener cursor_touch_up = {.notify = touchup}; +static struct wl_listener cursor_touch_cancel = {.notify = touchcancel}; +static struct wl_listener cursor_touch_frame = {.notify = cursorframe}; +"""), + ("""\twl_signal_add(&cursor->events.frame, &cursor_frame); +""", + """\twl_signal_add(&cursor->events.frame, &cursor_frame); +\twl_signal_add(&cursor->events.touch_down, &cursor_touch_down); +\twl_signal_add(&cursor->events.touch_motion, &cursor_touch_motion); +\twl_signal_add(&cursor->events.touch_up, &cursor_touch_up); +\twl_signal_add(&cursor->events.touch_cancel, &cursor_touch_cancel); +\twl_signal_add(&cursor->events.touch_frame, &cursor_touch_frame); +"""), + ("""\tcase WLR_INPUT_DEVICE_SWITCH: +\t\tcreateswitch(wlr_switch_from_input_device(device)); +\t\tbreak; +""", + """\tcase WLR_INPUT_DEVICE_SWITCH: +\t\tcreateswitch(wlr_switch_from_input_device(device)); +\t\tbreak; +\tcase WLR_INPUT_DEVICE_TOUCH: +\t\twlr_cursor_attach_input_device(cursor, device); +\t\tbreak; +"""), + ("""void +cursorframe(struct wl_listener *listener, void *data) +{ +""", + """/* Kryptik: the finger that drives the pointer, -1 while none is down; a second is ignored. */ +static int32_t touchid = -1; + +/* Kryptik: the pointer to where a touch is: on the built-in panel when there is one, as a + * laptop's touchscreen is its own, and over every screen otherwise. */ +static void +touchpoint(struct wlr_input_device *device, uint32_t time, double x, double y) +{ +\tstruct wlr_box box = {0}; +\tdouble lx, ly; +\tMonitor *m; + +\twl_list_for_each(m, &mons, link) { +\t\tconst char *n = m->wlr_output->name; +\t\tif (m->wlr_output->enabled && (!strncmp(n, "eDP", 3) || !strncmp(n, "LVDS", 4) || !strncmp(n, "DSI", 3))) +\t\t\tbox = m->m; +\t} +\tif (!wlr_box_empty(&box)) { +\t\tlx = box.x + x * box.width; +\t\tly = box.y + y * box.height; +\t} else { +\t\twlr_cursor_absolute_to_layout_coords(cursor, device, x, y, &lx, &ly); +\t} +\tmotionnotify(time, device, lx - cursor->x, ly - cursor->y, lx - cursor->x, ly - cursor->y); +} + +/* Kryptik: the left button, as buttonpress takes it from a pointer. */ +static void +touchbutton(uint32_t time, enum wl_pointer_button_state state) +{ +\tstruct wlr_pointer_button_event event = {.time_msec = time, .button = BTN_LEFT, .state = state}; +\tbuttonpress(NULL, &event); +} + +void +touchdown(struct wl_listener *listener, void *data) +{ +\tstruct wlr_touch_down_event *event = data; +\tif (touchid != -1) +\t\treturn; +\ttouchid = event->touch_id; +\ttouchpoint(&event->touch->base, event->time_msec, event->x, event->y); +\ttouchbutton(event->time_msec, WL_POINTER_BUTTON_STATE_PRESSED); +} + +void +touchmotion(struct wl_listener *listener, void *data) +{ +\tstruct wlr_touch_motion_event *event = data; +\tif (event->touch_id == touchid) +\t\ttouchpoint(&event->touch->base, event->time_msec, event->x, event->y); +} + +void +touchup(struct wl_listener *listener, void *data) +{ +\tstruct wlr_touch_up_event *event = data; +\tif (event->touch_id != touchid) +\t\treturn; +\ttouchid = -1; +\ttouchbutton(event->time_msec, WL_POINTER_BUTTON_STATE_RELEASED); +} + +void +touchcancel(struct wl_listener *listener, void *data) +{ +\tstruct wlr_touch_cancel_event *event = data; +\tif (event->touch_id != touchid) +\t\treturn; +\ttouchid = -1; +\ttouchbutton(event->time_msec, WL_POINTER_BUTTON_STATE_RELEASED); +} + +void +cursorframe(struct wl_listener *listener, void *data) +{ """), # Locked, dwl heeds no key at all, so a laptop that starts to play aloud under the lock # is silenced only by unlocking. It heeds config.h's lockedkeys instead: the volume and diff --git a/tools/image/gui-test.sh b/tools/image/gui-test.sh index 12134e0b..9f25aa0a 100755 --- a/tools/image/gui-test.sh +++ b/tools/image/gui-test.sh @@ -70,7 +70,7 @@ install_disk gui-install "$USB" --vars clean && green "installed" || { red "inst # ----------------------------------------------------------------- step 2 -- step "step 2: the desktop, driven" rm -f "$SHOT" "$SHOT_FS" "$SHOT_OVER" "$SHOT_HEAD" "$SHOT_POPUP" "$SHOT_LOCKED" -start_vm gui-p2 --net user --gpu --second-head --mem 3072 +start_vm gui-p2 --net user --gpu --second-head --touch --mem 3072 DRIVE_TIMEOUT=600 drive --qmp "$QMP" \ "expect:KRYPTIK_SMOKE: END" "seen:kryptik-firstboot: created user '${TUSER}'" "login:${TUSER}:${TPASS}" \ "send:su - root -c 'bash /usr/lib/kryptik/guest-tests/gui-check.sh ${TUSER} ${TPASS} 2>&1 | tee /var/log/kryptik/gui-check.log; echo GCHECK-DONE'" \ @@ -116,6 +116,7 @@ DRIVE_TIMEOUT=600 drive --qmp "$QMP" \ "expect:GT CONSENT-WAIT 2" "key:y" "key:ret" \ "expect:GT POINTER-ZONE0" "pointer:-4000,-4000" "pointer:120,120" \ "expect:GT POINTER-UNTRUSTED" "pointer:-4000,-4000" "pointer:120,120" \ + "expect:GT TOUCH-UNTRUSTED" "touch:250,500" \ "expect:GT SCREENSHOT-POPUP" "sleep:2" "screendump:${SHOT_POPUP}" \ "expect:GT KEY-MENU-POPUP" "key:alt+p" \ "expect:GT KEY-FOCUS-POPUP" "key:alt+j" \ @@ -183,7 +184,7 @@ for name in session-socket compositor-running desktop-memory-bound chrome-focus- second-head-scale-chosen zone-sees-heads-unnamed \ vt-admin-login vt-desktop-back vt-desktop-takes-keys dwl-execs-nothing dwl-without-group seat-is-dwls dwl-confined-to-its-files \ volume-key lock-passes-volume-keys lock-holds-other-keys \ - zone0-cursor-set zone0-cursor-shown zone-cursor-asked zone-hears-of-outputs zone-cursor-not-shown zone-cursor-by-name zone-fractional-scale compositor-socket-unreached \ + zone0-cursor-set zone0-cursor-shown zone-cursor-asked zone-hears-of-outputs zone-cursor-not-shown zone-cursor-by-name touch-picks-window zone-fractional-scale compositor-socket-unreached \ zone0-frame-clock zone-window-animates \ status-network-line-user \ zone0-fullscreen-granted zone0-fullscreen-needs-focus fullscreen-by-key marks-fullscreen zone-child-mapped child-focused parent-focused parent-fullscreen fullscreen-keeps-focus keyboard-stays-on-fullscreen zoom-keeps-keyboard zoom-twice-keeps-keyboard zone0-over-fullscreen-gets-keyboard zone0-window-ends-fullscreen child-ends-fullscreen; do diff --git a/tools/image/run-ovmf.sh b/tools/image/run-ovmf.sh index 263a1500..c3d54bc1 100755 --- a/tools/image/run-ovmf.sh +++ b/tools/image/run-ovmf.sh @@ -6,7 +6,7 @@ # [--disk FILE]... [--testctl FILE] [--vars clean|enrolled|ms|FILE] # [--vars-file FILE] [--mode console|smoke|serve] [--timeout N] # [--log FILE] [--net none|user] [--mem MB] [--cpus N] [--gpu] -# [--second-head] [--s3] [--sound WAV] [--usb-port] [--allow-reboot] +# [--second-head] [--touch] [--s3] [--sound WAV] [--usb-port] [--allow-reboot] # [--until REGEX] [--name TAG] # # --usb IMG the medium as a USB mass-storage device (removable), @@ -38,6 +38,8 @@ # --second-head with --gpu in serve mode: a second output on the GPU, off # until a VNC client on the printed socket asks for a size # (vm-drive.py's head: step), which plugs a monitor in +# --touch with --gpu: a touchscreen over the first output, QEMU's +# virtio-multitouch (8.1 on), which vm-drive.py's touch: step taps # --s3 offer suspend to RAM, which QEMU otherwise hides: the guest # sleeps until vm-drive.py's wakeup step # --sound WAV two HD Audio cards: the first only plays, into the file WAV @@ -54,7 +56,7 @@ SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SELF}/../../build/lib/common.sh" USB=""; ISO=""; NOMEDIA=0; DISKS=(); TESTCTL=""; VARS="clean"; VARS_FILE="" -MODE="smoke"; TIMEOUT=300; LOG=""; NET="none"; MEM=2048; CPUS=2; GPU=0; HEAD2=0; S3=0; SOUND=""; ALLOW_REBOOT=0; NAME="vm" +MODE="smoke"; TIMEOUT=300; LOG=""; NET="none"; MEM=2048; CPUS=2; GPU=0; HEAD2=0; TOUCH=0; S3=0; SOUND=""; ALLOW_REBOOT=0; NAME="vm" DISK_RO=0; DISK_4K=0; BLKDEBUG=""; UNTIL=""; USB_PORT=0; USB_RO=on while [[ "$#" -gt 0 ]]; do case "$1" in @@ -77,13 +79,14 @@ while [[ "$#" -gt 0 ]]; do --cpus) CPUS="${2:?}"; shift 2 ;; --gpu) GPU=1; shift ;; --second-head) HEAD2=1; shift ;; + --touch) TOUCH=1; shift ;; --s3) S3=1; shift ;; --sound) SOUND="${2:?}"; shift 2 ;; --usb-port) USB_PORT=1; shift ;; --allow-reboot) ALLOW_REBOOT=1; shift ;; --until) UNTIL="${2:?}"; shift 2 ;; --name) NAME="${2:?}"; shift 2 ;; - -h|--help) sed -n '2,49p' "${BASH_SOURCE[0]}"; exit 0 ;; + -h|--help) sed -n '2,51p' "${BASH_SOURCE[0]}"; exit 0 ;; *) die "unknown argument: $1" ;; esac done @@ -93,6 +96,7 @@ done [[ "$NOMEDIA" -eq 1 && "${#DISKS[@]}" -eq 0 ]] && die "--no-media needs at least one --disk" [[ -n "$UNTIL" && "$MODE" != smoke ]] && die "--until is for --mode smoke" [[ "$HEAD2" -eq 1 && ( "$GPU" -ne 1 || "$MODE" != serve ) ]] && die "--second-head is for --gpu in --mode serve" +[[ "$TOUCH" -eq 1 && "$GPU" -ne 1 ]] && die "--touch is for --gpu: the touchscreen covers its first output" [[ "$HEAD2" -eq 1 && "$S3" -eq 1 ]] && die "--second-head needs virtio-gpu, which --s3 gives up for std VGA's framebuffer" # Files only. This hands paths to a process that writes to them. @@ -171,6 +175,13 @@ elif [[ "$GPU" -eq 1 ]]; then ARGS+=( -display none -vga none -device virtio-vga,id=gpu0 ) fi ARGS+=( -device virtio-keyboard-pci -device virtio-mouse-pci ) + if [[ "$TOUCH" -eq 1 ]]; then + "$QEMU" -device help 2>/dev/null | grep -q '"virtio-multitouch-pci"' \ + || die "--touch: this QEMU has no virtio-multitouch-pci (QEMU 8.1 or later has)" + # Bound to the GPU's first head, so an input-send-event naming that head reaches it + # before the mouse, which takes button events too. + ARGS+=( -device virtio-multitouch-pci,id=touch0,display=gpu0,head=0 ) + fi else ARGS+=( -display none -vga none ) fi diff --git a/tools/image/vm-drive.py b/tools/image/vm-drive.py index a199dea0..3820b2de 100755 --- a/tools/image/vm-drive.py +++ b/tools/image/vm-drive.py @@ -32,6 +32,9 @@ pointer:DX,DY move the guest's mouse by DX,DY through QMP; it is relative and accelerated, so a large move ends at the screen's edge + touch:X,Y tap the touchscreen (run-ovmf.sh --touch) at X,Y, + thousandths of the first output's width and height: + one finger down, then lifted type-from:REGEX wait for REGEX on a line that has ended (REGEX names no line end), then type what its first group matched (digits and letters) and Enter @@ -258,6 +261,15 @@ def vnc_size(s, w, h): if n: msg += struct.pack(">IHHHHI", 0, 0, 0, w, h, 0) s.sendall(msg) +def touch_events(x, y): + """A tap at X,Y (0..0x7fff) as two input-send-event batches: the finger down, then lifted.""" + def mtt(kind, tracking, axis="x", value=0): + return {"type": "mtt", "data": {"type": kind, "slot": 0, "tracking-id": tracking, "axis": axis, "value": value}} + down = [mtt("begin", 1), mtt("data", 1, "x", x), mtt("data", 1, "y", y), + {"type": "btn", "data": {"down": True, "button": "touch"}}] + up = [mtt("end", -1), {"type": "btn", "data": {"down": False, "button": "touch"}}] + return [down, up] + def qmp(path, cmd, args=None): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(path) @@ -352,6 +364,17 @@ def main(): r = qmp(qmpsock, "input-send-event", {"events": events}) if "error" in r: raise RuntimeError(f"input-send-event {rest}: {r['error']}") time.sleep(0.3) + elif kind == "touch": + if not qmpsock: raise RuntimeError("touch needs --qmp") + px, py = (int(v) for v in rest.split(",")) + if not (0 <= px <= 1000 and 0 <= py <= 1000): raise RuntimeError(f"touch:{rest}: not in 0..1000") + tap = touch_events(px * 0x7fff // 1000, py * 0x7fff // 1000) + for events in tap: + # The head binds the events to run-ovmf.sh's touchscreen, not the mouse. + r = qmp(qmpsock, "input-send-event", {"device": "gpu0", "head": 0, "events": events}) + if "error" in r: raise RuntimeError(f"input-send-event touch:{rest}: {r['error']}") + time.sleep(0.15) + time.sleep(0.3) elif kind == "type-from": if not qmpsock: raise RuntimeError("type-from needs --qmp") text = d.line_value(rest) diff --git a/tools/tests/vm-drive.py b/tools/tests/vm-drive.py index 110441f1..eccdce64 100755 --- a/tools/tests/vm-drive.py +++ b/tools/tests/vm-drive.py @@ -188,6 +188,30 @@ def run(self): ("device_add", {"driver": "usb-audio", "id": "usbsound", "audiodev": "snd2", "bus": "xhci.0"}), ("device_del", {"id": "usbsound"})]) +print("-- touch: taps the touchscreen on the GPU's first head: a finger down, then lifted") +sock7 = os.path.join(T, "serial7"); qmp7 = os.path.join(T, "qmp7") +Guest(sock7, {}).start() +q7 = Qmp(qmp7, busy=0); q7.start() +argv, sys.argv = sys.argv, ["vm-drive.py", "--serial", sock7, "--qmp", qmp7, "--timeout", "5", "touch:250,500"] +with contextlib.redirect_stdout(io.StringIO()): + rc = vm.main() +sys.argv = argv +check("touch: succeeds", rc, 0) +mt = lambda kind, tracking, axis="x", value=0: {"type": "mtt", "data": {"type": kind, "slot": 0, "tracking-id": tracking, "axis": axis, "value": value}} +check("the finger goes down at a quarter across and half down, then lifts, both bound to head 0", q7.asked, [ + ("input-send-event", {"device": "gpu0", "head": 0, "events": [ + mt("begin", 1), mt("data", 1, "x", 8191), mt("data", 1, "y", 16383), + {"type": "btn", "data": {"down": True, "button": "touch"}}]}), + ("input-send-event", {"device": "gpu0", "head": 0, "events": [ + mt("end", -1), {"type": "btn", "data": {"down": False, "button": "touch"}}]})]) +sock8 = os.path.join(T, "serial8") +Guest(sock8, {}).start() +argv, sys.argv = sys.argv, ["vm-drive.py", "--serial", sock8, "--qmp", qmp7, "--timeout", "5", "touch:1200,500"] +with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + rc = vm.main() +sys.argv = argv +check("a tap off the screen is refused before QEMU is asked", (rc != 0, len(q7.asked)), (True, 2)) + print() print(f"{PASS} passed, {FAIL} failed") sys.exit(0 if FAIL == 0 else 1) From 0724b5d5ac9a34c8420e91ef7f6fed4e7eeb8ebc Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 23 Oct 2026 05:00:55 -0700 Subject: [PATCH 2/3] A tap's press goes to the window tapped dwl's motionnotify picks the surface under the pointer before it moves the cursor. A mouse's small steps never show it, but a touch is one jump and the button at once: the press and release went to whatever the pointer had been resting on, while the keyboard went to the window tapped. A tap on one zone's window could click another's. touchpoint picks the surface again where the touch landed, as dwl's own focus-restoring call does, before the button goes out; a finger held down keeps its window as before. gui-check's touch round now rests the pointer on zone 0's window first, and holds the tap's press and release to untrusted's probe, with none for zone 0's. wlprobe's cursor mode says each pointer button it is sent. --- build/guest-tests/gui-check.sh | 18 ++++++++++++++++++ tools/desktop/dwl-zone-borders.py | 4 ++++ tools/desktop/wlprobe.c | 7 ++++++- tools/image/gui-test.sh | 3 ++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/build/guest-tests/gui-check.sh b/build/guest-tests/gui-check.sh index 239dea6c..38e952eb 100755 --- a/build/guest-tests/gui-check.sh +++ b/build/guest-tests/gui-check.sh @@ -21,6 +21,7 @@ # GT CONSENT-CODE 1 NN type NN and Enter (the question's code) # GT CONSENT-WAIT 2 type y and Enter (not the code: refused) # GT POINTER-ZONE0, GT POINTER-UNTRUSTED move the pointer onto the newest window +# GT TOUCH-HOVER-ZONE0 move the pointer right, onto zone 0's window # GT TOUCH-UNTRUSTED tap the screen's left half (the newest window) # GT SCREENSHOT-POPUP take a screenshot # GT KEY-MENU-POPUP press Alt+p (the chrome menu) @@ -1199,6 +1200,13 @@ mark touch untrusted launch_plain untrusted "/usr/libexec/kryptik/wlprobe cursor 30" > "$LOG/launch-touch.out" 2>&1 wait_for 20 probe_committed touch sleep 2 +# The pointer rests on zone 0's window, the right half, when the tap comes: the press must +# reach the window tapped, not the one under the pointer. zone 0's probe says each entry. +entered="$(grep -c 'set a ' "$LOG/touch-zone0.out")" +zone0_entered() { [[ "$(grep -c 'set a ' "$LOG/touch-zone0.out")" -gt "$entered" ]]; } +echo "GT TOUCH-HOVER-ZONE0" +wait_for 10 zone0_entered +hovered="$(grep -c 'set a ' "$LOG/touch-zone0.out")" touch_before="$(grep '^zone=' "$RT/kryptik/focus" 2>/dev/null)" echo "GT TOUCH-UNTRUSTED" if ! grep -q 'Name="QEMU Virtio MultiTouch"' /proc/bus/input/devices; then @@ -1210,6 +1218,16 @@ elif wait_for 10 grep -qx 'zone=untrusted' "$RT/kryptik/focus"; then else fail "touch-picks-window" "after the tap: $(tr '\n' ' ' < "$RT/kryptik/focus" 2>/dev/null); $(since_mark touch untrusted | tail -2 | tr '\n' ' ')" fi +touch_pressed() { since_mark touch untrusted | grep -q 'pointer button 272 released'; } +if [[ "$hovered" -le "$entered" ]]; then + fail "touch-presses-window" "the pointer never came onto zone 0's window before the tap, so nothing was shown: $(tail -3 "$LOG/touch-zone0.out" | tr '\n' ' ')" +elif ! wait_for 10 touch_pressed; then + fail "touch-presses-window" "untrusted's window was sent no press and release: $(since_mark touch untrusted | tail -3 | tr '\n' ' '); zone 0's: $(grep 'pointer button' "$LOG/touch-zone0.out" | tr '\n' ' ')" +elif grep -q 'pointer button' "$LOG/touch-zone0.out"; then + fail "touch-presses-window" "the tap's button also went to zone 0's window, where the pointer rested: $(grep 'pointer button' "$LOG/touch-zone0.out" | tr '\n' ' ')" +else + pass "touch-presses-window" "the tap's press and release went to untrusted's window, and none to zone 0's, under the pointer before" +fi pkill -u "$USER_NAME" -f 'wlprobe cursor 40' 2>/dev/null; wait "$touch0" 2>/dev/null # --- a zone's window hears the scale it is shown at ------------------------------------------ diff --git a/tools/desktop/dwl-zone-borders.py b/tools/desktop/dwl-zone-borders.py index c64d97b2..a44b71b1 100755 --- a/tools/desktop/dwl-zone-borders.py +++ b/tools/desktop/dwl-zone-borders.py @@ -1342,6 +1342,10 @@ \t\twlr_cursor_absolute_to_layout_coords(cursor, device, x, y, &lx, &ly); \t} \tmotionnotify(time, device, lx - cursor->x, ly - cursor->y, lx - cursor->x, ly - cursor->y); +\t/* motionnotify picks the surface before it moves the cursor, which a mouse's small steps +\t * never show. A touch jumps, so the surface is picked again where it landed, or the press +\t * would go to whatever the pointer rested on, another zone's window as likely as not. */ +\tmotionnotify(0, NULL, 0, 0, 0, 0); } /* Kryptik: the left button, as buttonpress takes it from a pointer. */ diff --git a/tools/desktop/wlprobe.c b/tools/desktop/wlprobe.c index faa0e9c2..c850c063 100644 --- a/tools/desktop/wlprobe.c +++ b/tools/desktop/wlprobe.c @@ -35,7 +35,8 @@ * its middle: drawn, it covers the whole screen. * Says when the window, and when the cursor image, * enters an output: the second only if the - * compositor took the image. With shape, also ask + * compositor took the image, and each pointer + * button it is sent. With shape, also ask * for the text cursor by name (wp_cursor_shape_v1) * wlprobe animate SECONDS as oversize 0, and once drawn commit again on * every frame callback for SECONDS; then say how @@ -616,6 +617,10 @@ static int handle_one(void) draw_popup(); } else if (cursor && object == pointer_id && opcode == 0) { set_cursor(get32(body)); /* wl_pointer.enter(serial, surface, x, y) */ + } else if (cursor && object == pointer_id && opcode == 3) { + /* wl_pointer.button(serial, time, button, state) */ + printf("pointer button %u %s\n", get32(body + 8), get32(body + 12) ? "pressed" : "released"); + fflush(stdout); } else if (cursor && opcode == 0 && (object == SURFACE || (cursor_surf && object == cursor_surf))) { /* wl_surface.enter(output) */ printf("%s entered an output\n", object == SURFACE ? "the window" : "the cursor image"); diff --git a/tools/image/gui-test.sh b/tools/image/gui-test.sh index 9f25aa0a..8e500ae0 100755 --- a/tools/image/gui-test.sh +++ b/tools/image/gui-test.sh @@ -116,6 +116,7 @@ DRIVE_TIMEOUT=600 drive --qmp "$QMP" \ "expect:GT CONSENT-WAIT 2" "key:y" "key:ret" \ "expect:GT POINTER-ZONE0" "pointer:-4000,-4000" "pointer:120,120" \ "expect:GT POINTER-UNTRUSTED" "pointer:-4000,-4000" "pointer:120,120" \ + "expect:GT TOUCH-HOVER-ZONE0" "pointer:4000,0" "pointer:-80,0" "pointer:-1,0" \ "expect:GT TOUCH-UNTRUSTED" "touch:250,500" \ "expect:GT SCREENSHOT-POPUP" "sleep:2" "screendump:${SHOT_POPUP}" \ "expect:GT KEY-MENU-POPUP" "key:alt+p" \ @@ -184,7 +185,7 @@ for name in session-socket compositor-running desktop-memory-bound chrome-focus- second-head-scale-chosen zone-sees-heads-unnamed \ vt-admin-login vt-desktop-back vt-desktop-takes-keys dwl-execs-nothing dwl-without-group seat-is-dwls dwl-confined-to-its-files \ volume-key lock-passes-volume-keys lock-holds-other-keys \ - zone0-cursor-set zone0-cursor-shown zone-cursor-asked zone-hears-of-outputs zone-cursor-not-shown zone-cursor-by-name touch-picks-window zone-fractional-scale compositor-socket-unreached \ + zone0-cursor-set zone0-cursor-shown zone-cursor-asked zone-hears-of-outputs zone-cursor-not-shown zone-cursor-by-name touch-picks-window touch-presses-window zone-fractional-scale compositor-socket-unreached \ zone0-frame-clock zone-window-animates \ status-network-line-user \ zone0-fullscreen-granted zone0-fullscreen-needs-focus fullscreen-by-key marks-fullscreen zone-child-mapped child-focused parent-focused parent-fullscreen fullscreen-keeps-focus keyboard-stays-on-fullscreen zoom-keeps-keyboard zoom-twice-keeps-keyboard zone0-over-fullscreen-gets-keyboard zone0-window-ends-fullscreen child-ends-fullscreen; do From e453db04cd67cd4316756523b9262f1ee756842b Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 23 Oct 2026 05:04:28 -0700 Subject: [PATCH 3/3] No touch acts through a shut lid or on a panel that is off Closed on a dock, the panel stays on, and a touch through the lid would click, and take the keyboard, on a screen no one sees, and hold off the idle lock. touchdown drops every touch while the lid is shut. touchpoint drops one, too, while a built-in panel is there but off, dark or disabled, rather than spread it over the other screens. A finger already down still ends through touchup or touchcancel. cleanuplisteners removes the five touch listeners with the cursor's own. One touchscreen is taken to be the laptop's, which the comment now says. --- tools/desktop/dwl-zone-borders.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/tools/desktop/dwl-zone-borders.py b/tools/desktop/dwl-zone-borders.py index a44b71b1..cff6881e 100755 --- a/tools/desktop/dwl-zone-borders.py +++ b/tools/desktop/dwl-zone-borders.py @@ -1302,6 +1302,15 @@ \twl_signal_add(&cursor->events.touch_up, &cursor_touch_up); \twl_signal_add(&cursor->events.touch_cancel, &cursor_touch_cancel); \twl_signal_add(&cursor->events.touch_frame, &cursor_touch_frame); +"""), + ("""\twl_list_remove(&cursor_motion_absolute.link); +""", + """\twl_list_remove(&cursor_motion_absolute.link); +\twl_list_remove(&cursor_touch_down.link); +\twl_list_remove(&cursor_touch_motion.link); +\twl_list_remove(&cursor_touch_up.link); +\twl_list_remove(&cursor_touch_cancel.link); +\twl_list_remove(&cursor_touch_frame.link); """), ("""\tcase WLR_INPUT_DEVICE_SWITCH: \t\tcreateswitch(wlr_switch_from_input_device(device)); @@ -1322,19 +1331,27 @@ static int32_t touchid = -1; /* Kryptik: the pointer to where a touch is: on the built-in panel when there is one, as a - * laptop's touchscreen is its own, and over every screen otherwise. */ -static void + * laptop's touchscreen is its own, and over every screen otherwise. One touchscreen is taken + * to be the laptop's, so a touch monitor on a desk drives the panel too. 0, the touch dropped, + * while the panel is there but off: dark, or shut away, a touch on it lands where no one sees. */ +static int touchpoint(struct wlr_input_device *device, uint32_t time, double x, double y) { \tstruct wlr_box box = {0}; \tdouble lx, ly; +\tint panel = 0; \tMonitor *m; \twl_list_for_each(m, &mons, link) { \t\tconst char *n = m->wlr_output->name; -\t\tif (m->wlr_output->enabled && (!strncmp(n, "eDP", 3) || !strncmp(n, "LVDS", 4) || !strncmp(n, "DSI", 3))) +\t\tif (strncmp(n, "eDP", 3) && strncmp(n, "LVDS", 4) && strncmp(n, "DSI", 3)) +\t\t\tcontinue; +\t\tpanel = 1; +\t\tif (m->wlr_output->enabled) \t\t\tbox = m->m; \t} +\tif (panel && wlr_box_empty(&box)) +\t\treturn 0; \tif (!wlr_box_empty(&box)) { \t\tlx = box.x + x * box.width; \t\tly = box.y + y * box.height; @@ -1346,6 +1363,7 @@ \t * never show. A touch jumps, so the surface is picked again where it landed, or the press \t * would go to whatever the pointer rested on, another zone's window as likely as not. */ \tmotionnotify(0, NULL, 0, 0, 0, 0); +\treturn 1; } /* Kryptik: the left button, as buttonpress takes it from a pointer. */ @@ -1360,10 +1378,13 @@ touchdown(struct wl_listener *listener, void *data) { \tstruct wlr_touch_down_event *event = data; -\tif (touchid != -1) +\t/* Through a lid shut on a dock, a touch is no one's: it would click, and take the keyboard, +\t * on a screen no one sees, and hold off the idle lock. */ +\tif (touchid != -1 || lidshut) +\t\treturn; +\tif (!touchpoint(&event->touch->base, event->time_msec, event->x, event->y)) \t\treturn; \ttouchid = event->touch_id; -\ttouchpoint(&event->touch->base, event->time_msec, event->x, event->y); \ttouchbutton(event->time_msec, WL_POINTER_BUTTON_STATE_PRESSED); }