diff --git a/build/guest-tests/gui-check.sh b/build/guest-tests/gui-check.sh index 77b566fc..38e952eb 100755 --- a/build/guest-tests/gui-check.sh +++ b/build/guest-tests/gui-check.sh @@ -21,6 +21,8 @@ # 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) # GT KEY-FOCUS-POPUP press Alt+j @@ -1184,6 +1186,50 @@ 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 +# 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 + 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 +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 ------------------------------------------ # 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..cff6881e 100755 --- a/tools/desktop/dwl-zone-borders.py +++ b/tools/desktop/dwl-zone-borders.py @@ -1271,6 +1271,154 @@ \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); +"""), + ("""\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)); +\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. 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 (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; +\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); +\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); +\treturn 1; +} + +/* 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; +\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; +\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/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 12134e0b..8e500ae0 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,8 @@ 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" \ "expect:GT KEY-FOCUS-POPUP" "key:alt+j" \ @@ -183,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 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 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)