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:
parent
b189318124
commit
8a71a062ce
5 changed files with 538 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
143
tests/wltest.c
143
tests/wltest.c
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue