protocols: forward pointer-gestures swipe/pinch/hold

Advertise zwp_pointer_gestures_v1 (v3, with hold) and fan libinput
touchpad gestures out to the client holding seat pointer focus.
The stdin test feed gains swipe/pinch/hold messages driving the same
forwarders in any mode (nested has no host gesture source).

wltest gains --gesture-log plus swipe/pinch/hold feed helpers; smoke
tests cover the global and an exact-value round trip of every gesture
and cancel path.
This commit is contained in:
Andrew Hamilton 2026-09-04 10:13:13 +10:00
parent b189318124
commit 8a71a062ce
5 changed files with 538 additions and 11 deletions

View file

@ -91,6 +91,22 @@ CTL_CMD_MODS = 2
CTL_CMD_WARP = 3
CTL_CMD_PTR_MOTION = 4
CTL_CMD_TOUCH = 6
CTL_CMD_SWIPE = 7
CTL_CMD_PINCH = 8
CTL_CMD_HOLD = 9
# 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
GESTURE_UPDATE = 1
GESTURE_END = 2
GESTURE_CANCEL = 3
HOLD_BEGIN = 0
HOLD_END = 1
HOLD_CANCEL = 2
GESTURE_BEGIN = 0
GESTURE_UPDATE = 1
GESTURE_END = 2
GESTURE_CANCEL = 3
TOUCH_UP = 0
TOUCH_DOWN = 1
TOUCH_MOTION = 2
@ -233,6 +249,27 @@ class Session:
self.proc.stdin.write(record)
self.proc.stdin.flush()
def swipe(self, phase, fingers, dx=0, dy=0):
"""Touchpad swipe event through the shared gesture forwarders."""
record = struct.pack("<BIIii", CTL_CMD_SWIPE, phase, fingers,
dx, dy)
self.proc.stdin.write(record)
self.proc.stdin.flush()
def pinch(self, phase, fingers, dx=0, dy=0, scale=1.0, rotation=0.0):
"""Touchpad pinch event; scale/rotation ride as thousandths."""
record = struct.pack("<BIIiiii", CTL_CMD_PINCH, phase, fingers,
dx, dy, round(scale * 1000),
round(rotation * 1000))
self.proc.stdin.write(record)
self.proc.stdin.flush()
def hold(self, phase, fingers):
"""Touchpad hold event through the shared gesture forwarders."""
record = struct.pack("<BII", CTL_CMD_HOLD, phase, fingers)
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.
@ -1411,6 +1448,99 @@ def test_standalone_headless_modal_tab_groups_off(tmp_path):
s.close()
def test_standalone_headless_gestures_globals(tmp_path):
"""The standalone compositor advertises zwp_pointer_gestures_v1 so
touchpad clients can bind swipe/pinch/hold."""
out = tmp_path / "globals.txt"
s = _spawn_standalone({}, "%s --globals-out '%s' '#112233'" % (WL, out))
try:
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if out.exists():
break
time.sleep(0.1)
assert out.exists(), "wltest --globals-out never wrote its file"
names = {line.split()[0] for line in out.read_text().splitlines()}
assert "zwp_pointer_gestures_v1" in names, \
f"missing gestures global; advertised: {sorted(names)}"
finally:
s.close()
def _gestures_text(log_path):
return log_path.read_text() if log_path.exists() else ""
def test_standalone_headless_gestures_roundtrip(tmp_path):
"""Swipe/pinch/hold injected through the test feed reach a gesture
client holding pointer focus, with exact fingers/deltas/scales."""
log = tmp_path / "gestures.log"
s = _spawn_standalone(
{}, "%s --gesture-log '%s' '#ff0000'" % (WL, log))
try:
s.wait_any_active()
s.wait_active_frame()
assert s.proc.poll() is None
# Warp until the client logs a full swipe: proves pointer-focus
# routing (gestures only fan out to the focused client) and
# delivery in one loop; focus then persists for pinch/hold.
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
s.warp(400, 400)
time.sleep(0.05)
s.warp(600, 400)
s.swipe(GESTURE_BEGIN, 3)
s.swipe(GESTURE_UPDATE, 3, 10, -5)
s.swipe(GESTURE_END, 3)
time.sleep(0.2)
text = _gestures_text(log)
if "swipe begin fingers=3" in text \
and "swipe end cancelled=0" in text:
break
text = _gestures_text(log)
assert "swipe begin fingers=3" in text, \
f"swipe never delivered:\n{text!r}"
assert "swipe update dx=10 dy=-5" in text, \
f"swipe deltas wrong:\n{text!r}"
assert "swipe end cancelled=0" in text, \
f"swipe end missing:\n{text!r}"
# Pinch, hold and every cancel path now that focus is held.
s.pinch(GESTURE_BEGIN, 2)
s.pinch(GESTURE_UPDATE, 2, 1, 2, 1.5, 90.0)
s.pinch(GESTURE_END, 2)
s.hold(HOLD_BEGIN, 2)
s.hold(HOLD_END, 2)
s.swipe(GESTURE_BEGIN, 3)
s.swipe(GESTURE_CANCEL, 3)
s.pinch(GESTURE_BEGIN, 2)
s.pinch(GESTURE_CANCEL, 2)
s.hold(HOLD_BEGIN, 1)
s.hold(HOLD_CANCEL, 1)
deadline = time.monotonic() + 10.0
while time.monotonic() < deadline:
text = _gestures_text(log)
if text.count("cancelled=1") >= 3 \
and "hold end cancelled=0" in text:
break
time.sleep(0.1)
text = _gestures_text(log)
assert "pinch begin fingers=2" in text, \
f"pinch never delivered:\n{text!r}"
assert "pinch update dx=1 dy=2 scale=1.5 rotation=90" in text, \
f"pinch values wrong:\n{text!r}"
assert "pinch end cancelled=0" in text, \
f"pinch end missing:\n{text!r}"
assert "hold begin fingers=2" in text, \
f"hold never delivered:\n{text!r}"
assert "hold end cancelled=0" in text, \
f"hold end missing:\n{text!r}"
assert text.count("cancelled=1") >= 3, \
f"cancel paths missing:\n{text!r}"
assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_nested_primary_selection_bridged(tmp_path):
"""Nested mode: a tab's primary selection is fanned out to a second tab
exactly as standalone, and the host-side bridge publishes it over

View file

@ -1,6 +1,7 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <poll.h>
@ -38,6 +39,7 @@
#include "viewporter-client.h"
#include "wlr-gamma-control-unstable-v1-client.h"
#include "xdg-dialog-v1-client.h"
#include "pointer-gestures-unstable-v1-client.h"
/* Minimal deterministic xdg toplevel test client: shows a solid-color shm
buffer and runs until killed. Used by test.sh to give the smoke test two
@ -145,6 +147,7 @@ static struct zwp_virtual_keyboard_manager_v1 *virtual_kb_mgr;
static struct zwlr_virtual_pointer_manager_v1 *virtual_ptr_mgr;
static struct xdg_activation_v1 *activation_mgr;
static struct xdg_wm_dialog_v1 *dialog_mgr;
static struct zwp_pointer_gestures_v1 *gestures_mgr;
static struct zwp_text_input_manager_v3 *text_input_mgr;
static struct zwp_input_method_manager_v2 *input_method_mgr;
static struct wp_content_type_manager_v1 *content_type_mgr;
@ -819,6 +822,12 @@ static void registry_global(void *data, struct wl_registry *reg, uint32_t name,
} else if (strcmp(interface, xdg_wm_dialog_v1_interface.name) == 0) {
dialog_mgr = wl_registry_bind(reg, name, &xdg_wm_dialog_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwp_pointer_gestures_v1_interface.name) == 0) {
/* v3 carries hold gestures alongside swipe/pinch. */
gestures_mgr = wl_registry_bind(reg, name,
&zwp_pointer_gestures_v1_interface,
version < 3 ? version : 3);
} else if (strcmp(interface,
zwp_text_input_manager_v3_interface.name) == 0) {
text_input_mgr = wl_registry_bind(reg, name,
@ -1552,6 +1561,125 @@ static void inhibit_run(void) {
}
}
/* --- Pointer gestures (touchpad swipe/pinch/hold) ---
--gesture-log <file>: bind the gestures manager for the seat's pointer
and append every received gesture event ("swipe begin fingers=3" ...).
The compositor only routes gestures to the client holding seat pointer
focus, so the driver must warp the pointer onto this window first. */
static const char *gesture_log_path;
static FILE *gesture_log;
static void gesture_note(const char *fmt, ...) {
if (!gesture_log)
return;
va_list ap;
va_start(ap, fmt);
vfprintf(gesture_log, fmt, ap);
va_end(ap);
fputc('\n', gesture_log);
fflush(gesture_log);
}
static void gesture_swipe_begin(void *data,
struct zwp_pointer_gesture_swipe_v1 *sw, uint32_t serial,
uint32_t time, struct wl_surface *srf, uint32_t fingers) {
(void)data; (void)sw; (void)serial; (void)time; (void)srf;
gesture_note("swipe begin fingers=%u", fingers);
}
static void gesture_swipe_update(void *data,
struct zwp_pointer_gesture_swipe_v1 *sw, uint32_t time,
wl_fixed_t dx, wl_fixed_t dy) {
(void)data; (void)sw; (void)time;
gesture_note("swipe update dx=%g dy=%g",
wl_fixed_to_double(dx), wl_fixed_to_double(dy));
}
static void gesture_swipe_end(void *data,
struct zwp_pointer_gesture_swipe_v1 *sw, uint32_t serial,
uint32_t time, int32_t cancelled) {
(void)data; (void)sw; (void)serial; (void)time;
gesture_note("swipe end cancelled=%u", cancelled);
}
static const struct zwp_pointer_gesture_swipe_v1_listener swipe_listener = {
.begin = gesture_swipe_begin,
.update = gesture_swipe_update,
.end = gesture_swipe_end,
};
static void gesture_pinch_begin(void *data,
struct zwp_pointer_gesture_pinch_v1 *pi, uint32_t serial,
uint32_t time, struct wl_surface *srf, uint32_t fingers) {
(void)data; (void)pi; (void)serial; (void)time; (void)srf;
gesture_note("pinch begin fingers=%u", fingers);
}
static void gesture_pinch_update(void *data,
struct zwp_pointer_gesture_pinch_v1 *pi, uint32_t time,
wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale,
wl_fixed_t rotation) {
(void)data; (void)pi; (void)time;
gesture_note("pinch update dx=%g dy=%g scale=%g rotation=%g",
wl_fixed_to_double(dx), wl_fixed_to_double(dy),
wl_fixed_to_double(scale), wl_fixed_to_double(rotation));
}
static void gesture_pinch_end(void *data,
struct zwp_pointer_gesture_pinch_v1 *pi, uint32_t serial,
uint32_t time, int32_t cancelled) {
(void)data; (void)pi; (void)serial; (void)time;
gesture_note("pinch end cancelled=%u", cancelled);
}
static const struct zwp_pointer_gesture_pinch_v1_listener pinch_listener = {
.begin = gesture_pinch_begin,
.update = gesture_pinch_update,
.end = gesture_pinch_end,
};
static void gesture_hold_begin(void *data,
struct zwp_pointer_gesture_hold_v1 *ho, uint32_t serial,
uint32_t time, struct wl_surface *srf, uint32_t fingers) {
(void)data; (void)ho; (void)serial; (void)time; (void)srf;
gesture_note("hold begin fingers=%u", fingers);
}
static void gesture_hold_end(void *data,
struct zwp_pointer_gesture_hold_v1 *ho, uint32_t serial,
uint32_t time, int32_t cancelled) {
(void)data; (void)ho; (void)serial; (void)time;
gesture_note("hold end cancelled=%u", cancelled);
}
static const struct zwp_pointer_gesture_hold_v1_listener hold_listener = {
.begin = gesture_hold_begin,
.end = gesture_hold_end,
};
/* Bind all three gesture kinds for the seat pointer once it exists. */
static void gesture_bind_all(void) {
if (!gestures_mgr || !pointer || !gesture_log_path)
return;
if (!gesture_log) {
gesture_log = fopen(gesture_log_path, "w");
if (!gesture_log) {
fprintf(stderr, "wltest: cannot write gesture log %s\n",
gesture_log_path);
exit(1);
}
}
struct zwp_pointer_gesture_swipe_v1 *sw =
zwp_pointer_gestures_v1_get_swipe_gesture(gestures_mgr, pointer);
zwp_pointer_gesture_swipe_v1_add_listener(sw, &swipe_listener, NULL);
struct zwp_pointer_gesture_pinch_v1 *pi =
zwp_pointer_gestures_v1_get_pinch_gesture(gestures_mgr, pointer);
zwp_pointer_gesture_pinch_v1_add_listener(pi, &pinch_listener, NULL);
struct zwp_pointer_gesture_hold_v1 *ho =
zwp_pointer_gestures_v1_get_hold_gesture(gestures_mgr, pointer);
zwp_pointer_gesture_hold_v1_add_listener(ho, &hold_listener, NULL);
}
/* --- Gamma control (wlr-gamma-control-unstable-v1) ---
--gamma --gamma-out <file>: ask for the output's gamma control. The
compositor answers gamma_size (a LUT exists, e.g. DRM) or failed (no
@ -2236,8 +2364,8 @@ static void print_usage(const char *argv0) {
fprintf(stderr, "usage: %s [--app-id <id>] [--extra #RRGGBB] "
"[--cursor-shape name] [--globals-out <file>] "
"[--constrain locked|confined] [--confine-rect X Y W H] "
"[--primary-set <text>] [--primary-demand <file>] "
"[--touch-log <file>] "
"[--primary-set <text>] [--primary-demand <file>] "
"[--touch-log <file>] [--gesture-log <file>] "
"[--layer <bg|bottom|top|overlay>] [--layer-anchor <top|bottom|left|right>] "
"[--exclusive-zone <int>] [--layer-size <W> <H>] "
"[--foreign [--foreign-out <file>] [--foreign-close <app_id>]] "
@ -2404,6 +2532,14 @@ int main(int argc, char **argv) {
}
pointer_log_path = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--gesture-log") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"wltest: --gesture-log needs a file argument\n");
return 1;
}
gesture_log_path = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--primary-demand-expect") == 0) {
if (i + 2 >= argc) {
fprintf(stderr, "wltest: --primary-demand-expect needs a "
@ -2834,6 +2970,9 @@ int main(int argc, char **argv) {
wl_keyboard_add_listener(keyboard, &keyboard_listener, NULL);
pointer = wl_seat_get_pointer(seat);
wl_pointer_add_listener(pointer, &pointer_listener, NULL);
/* --gesture-log: bind swipe/pinch/hold for this pointer so tests
can prove gesture delivery end-to-end. */
gesture_bind_all();
/* --pointer-out: record every pointer enter/motion/button so tests
can prove virtual-pointer delivery end-to-end. */
if (pointer_log_path && !pointer_log) {