test: synth feed drives real host pointer/touch listeners

This commit is contained in:
Andrew Hamilton 2026-09-10 10:58:22 +10:00
parent 1aa7665a82
commit 3a194104a3
2 changed files with 200 additions and 6 deletions

125
kick.c
View file

@ -8433,6 +8433,39 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
uint32_t mods_locked, uint32_t group);
static void host_ptr_motion(uint32_t time, double sx, double sy);
/* Host pointer/touch listeners (defined with the host seat handlers). The
CTL_CMD_HOST_PTR/HOST_TOUCH feed drives them with NULL objects -- every
listener voids its Wayland object -- the same way CTL_CMD_KEY already
drives host_kbd_key with NULLs. */
static void host_ptr_enter(void *data, struct wl_pointer *p, uint32_t serial,
struct wl_surface *surface, wl_fixed_t sx,
wl_fixed_t sy);
static void host_ptr_leave(void *data, struct wl_pointer *p, uint32_t serial,
struct wl_surface *surface);
static void host_ptr_motion_event(void *data, struct wl_pointer *p,
uint32_t time, wl_fixed_t sx, wl_fixed_t sy);
static void host_ptr_button(void *data, struct wl_pointer *p, uint32_t serial,
uint32_t time, uint32_t button, uint32_t state);
static void host_ptr_axis(void *data, struct wl_pointer *p, uint32_t time,
uint32_t axis, wl_fixed_t value);
static void host_ptr_axis_source(void *data, struct wl_pointer *p,
uint32_t source);
static void host_ptr_axis_discrete(void *data, struct wl_pointer *p,
uint32_t axis, int32_t discrete);
static void host_ptr_axis_stop(void *data, struct wl_pointer *p,
uint32_t time, uint32_t axis);
static void host_ptr_frame(void *data, struct wl_pointer *p);
static void host_touch_down(void *data, struct wl_touch *touch,
uint32_t serial, uint32_t time,
struct wl_surface *surface, int32_t id,
wl_fixed_t fx, wl_fixed_t fy);
static void host_touch_motion(void *data, struct wl_touch *touch,
uint32_t time, int32_t id,
wl_fixed_t fx, wl_fixed_t fy);
static void host_touch_up(void *data, struct wl_touch *touch,
uint32_t serial, uint32_t time, int32_t id);
static void host_touch_cancel(void *data, struct wl_touch *touch);
static void host_touch_frame(void *data, struct wl_touch *touch);
/* Touch delivery shared by native touchscreens, the nested host forwarder
and the ctl feed. `action` is a KICK_TOUCH_* value; (ox,oy) are
@ -10035,6 +10068,13 @@ static void handle_new_input(struct wl_listener *listener, void *data) {
#define CTL_CMD_SWIPE 7
#define CTL_CMD_PINCH 8
#define CTL_CMD_HOLD 9
/* HOST_PTR/HOST_TOUCH drive the real host wl_pointer/wl_touch listeners with
synthesized events (a 17-byte record: sub-command + three u32 args). The
listeners void their Wayland objects, the same way CTL_CMD_KEY already
drives host_kbd_key with NULLs, so the test feed covers the nested input
path instead of bypassing it. */
#define CTL_CMD_HOST_PTR 10
#define CTL_CMD_HOST_TOUCH 11
#define CTL_MAX_MSG 32 /* pinch carries six u32s after padding */
#define CTL_BUF_SIZE 512
@ -10156,6 +10196,78 @@ static void ctl_dispatch(const uint8_t *msg, size_t len) {
case 2: gesture_hold_end(ctl_time, true); break;
}
break;
case CTL_CMD_HOST_PTR:
/* Synthesized host pointer event through the real wl_pointer
listener (sub, a, b, c). Coordinates are output pixels. */
if (len < 17) return;
memcpy(u32, msg + 1, 16);
ctl_time += 1;
switch (u32[0]) {
case 0: /* enter: serial, x, y */
host_ptr_enter(NULL, NULL, u32[1], NULL,
wl_fixed_from_int((int)u32[2]),
wl_fixed_from_int((int)u32[3]));
break;
case 1: /* leave: serial */
host_ptr_leave(NULL, NULL, u32[1], NULL);
break;
case 2: /* motion: x, y */
host_ptr_motion_event(NULL, NULL, ctl_time,
wl_fixed_from_int((int)u32[1]),
wl_fixed_from_int((int)u32[2]));
break;
case 3: /* button: button, state */
host_ptr_button(NULL, NULL, ++ctl_serial, ctl_time,
u32[1], u32[2]);
break;
case 4: /* axis: axis, value (pixels) */
host_ptr_axis(NULL, NULL, ctl_time, u32[1],
wl_fixed_from_int((int)u32[2]));
break;
case 5: /* axis_discrete: axis, discrete */
host_ptr_axis_discrete(NULL, NULL, u32[1], (int32_t)u32[2]);
break;
case 6: /* axis_source: source */
host_ptr_axis_source(NULL, NULL, u32[1]);
break;
case 7: /* axis_stop: axis */
host_ptr_axis_stop(NULL, NULL, ctl_time, u32[1]);
break;
case 8: /* frame */
host_ptr_frame(NULL, NULL);
break;
}
break;
case CTL_CMD_HOST_TOUCH:
/* Synthesized host touch event through the real wl_touch listener
(sub, a, b, c). Coordinates are output pixels. */
if (len < 17) return;
memcpy(u32, msg + 1, 16);
ctl_time += 1;
switch (u32[0]) {
case 0: /* down: slot, x, y */
host_touch_down(NULL, NULL, ++ctl_serial, ctl_time, NULL,
(int32_t)u32[1],
wl_fixed_from_int((int)u32[2]),
wl_fixed_from_int((int)u32[3]));
break;
case 1: /* motion: slot, x, y */
host_touch_motion(NULL, NULL, ctl_time, (int32_t)u32[1],
wl_fixed_from_int((int)u32[2]),
wl_fixed_from_int((int)u32[3]));
break;
case 2: /* up: slot */
host_touch_up(NULL, NULL, ++ctl_serial, ctl_time,
(int32_t)u32[1]);
break;
case 3: /* cancel */
host_touch_cancel(NULL, NULL);
break;
case 4: /* frame */
host_touch_frame(NULL, NULL);
break;
}
break;
}
}
@ -10183,13 +10295,14 @@ static int ctl_cb(int fd, uint32_t mask, void *data) {
uint8_t msg[CTL_MAX_MSG];
while (have >= 5) {
/* MODS/SWIPE carry four u32s, TOUCH three, PINCH six after the
command byte; HOLD is a single u32 pair like the rest. */
/* MODS/SWIPE/TOUCH/HOST_PTR/HOST_TOUCH carry four u32s, PINCH six
after the command byte; HOLD is a single u32 pair like the rest. */
size_t need = (buf[0] == CTL_CMD_MODS || buf[0] == CTL_CMD_TOUCH ||
buf[0] == CTL_CMD_SWIPE)
? 17
: buf[0] == CTL_CMD_PINCH ? 25
: 9;
buf[0] == CTL_CMD_SWIPE || buf[0] == CTL_CMD_HOST_PTR ||
buf[0] == CTL_CMD_HOST_TOUCH)
? 17
: buf[0] == CTL_CMD_PINCH ? 25
: 9;
if (have < need) break;
memcpy(msg, buf, need);
ctl_dispatch(msg, need);

View file

@ -126,6 +126,26 @@ KICK_REPEAT_DEFAULT = "600"
CTL_CMD_SWIPE = 7
CTL_CMD_PINCH = 8
CTL_CMD_HOLD = 9
CTL_CMD_HOST_PTR = 10
CTL_CMD_HOST_TOUCH = 11
# Host-pointer sub-commands (sub, a, b, c), mirroring wl_pointer events.
HP_ENTER = 0
HP_LEAVE = 1
HP_MOTION = 2
HP_BUTTON = 3
HP_AXIS = 4
HP_DISCRETE = 5
HP_SOURCE = 6
HP_STOP = 7
HP_FRAME = 8
# Host-touch sub-commands, mirroring wl_touch events.
HT_DOWN = 0
HT_MOTION = 1
HT_UP = 2
HT_CANCEL = 3
HT_FRAME = 4
# Evdev button code for BTN_LEFT, as the compositor expects.
BTN_LEFT = 0x110
# Gesture phases: 0 begin / 1 update / 2 end (3 cancels swipe/pinch;
# hold has no update phase, so its phases are renamed below).
GESTURE_BEGIN = 0
@ -370,6 +390,21 @@ class Session:
self.proc.stdin.write(record)
self.proc.stdin.flush()
def host_ptr(self, sub, a=0, b=0, c=0):
"""Synthesized host wl_pointer event through the real host listener
(enter/motion/button/axis/...), the same object a live host seat
would deliver."""
record = struct.pack("<BIIII", CTL_CMD_HOST_PTR, sub, a, b, c)
self.proc.stdin.write(record)
self.proc.stdin.flush()
def host_touch(self, sub, a=0, b=0, c=0):
"""Synthesized host wl_touch event through the real host listener
(down/motion/up/cancel/frame)."""
record = struct.pack("<BIIII", CTL_CMD_HOST_TOUCH, sub, a, b, c)
self.proc.stdin.write(record)
self.proc.stdin.flush()
def pointer_enter_until(self, pattern, timeout=20.0):
"""Keep re-entering the surface until `pattern` matches the log.
@ -4323,6 +4358,52 @@ def test_touch_delivery_to_client():
assert "motion" in content
def test_synth_feed_drives_host_listeners(tmp_path):
"""Synthesized host pointer/touch events go through the real host
wl_pointer/wl_touch listeners instead of bypassing them for the shared
inners: enter/motion/button/axis/discrete/source/stop/frame/leave plus
touch down/motion/up/cancel/frame all execute, touch still delivers to
the client, and the session survives the burst. Guards the nested input
path the stdin feed would otherwise never cover."""
logf = tmp_path / "touch.log"
with _run_standalone(
{}, f"{WL} --touch-log {logf} '#306090'",
wait="app0_mapped") as s:
s.host_ptr(HP_ENTER, 1, 300, 300)
s.host_ptr(HP_MOTION, 320, 320)
s.host_ptr(HP_BUTTON, BTN_LEFT, 1)
s.host_ptr(HP_BUTTON, BTN_LEFT, 0)
s.host_ptr(HP_DISCRETE, 0, 1)
s.host_ptr(HP_SOURCE, 0)
s.host_ptr(HP_AXIS, 0, 15)
s.host_ptr(HP_STOP, 0)
s.host_ptr(HP_FRAME)
s.host_ptr(HP_LEAVE, 1)
_assert_alive(s, "compositor died on host pointer burst")
s.host_touch(HT_DOWN, 7, 300, 300)
s.host_touch(HT_MOTION, 7, 340, 340)
s.host_touch(HT_UP, 7)
deadline = time.monotonic() + 5
content = ""
while time.monotonic() < deadline:
if os.path.exists(logf):
content = open(logf).read()
if "down" in content and "up" in content:
break
time.sleep(0.2)
assert "down" in content and "up" in content, \
f"host-path touch never delivered: {content!r}"
dline = [l for l in content.splitlines()
if l.startswith("down")][0]
assert " 7 " in f" {dline} ", f"wrong slot id: {dline}"
# A second sequence ended by cancel (not up) still ends cleanly.
s.host_touch(HT_DOWN, 8, 300, 300)
s.host_touch(HT_CANCEL)
s.host_touch(HT_FRAME)
time.sleep(0.5)
_assert_alive(s, "compositor died on host touch cancel")
def test_alt_shift_move_no_member_is_noop():
"""Shift+Left/Right with the prefix held on a tab whose focused member
is the main surface still gets consumed (never leaks Shift+arrow to