From 276a70a84fed5be229c12546df1ac393158a4ec9 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 16:23:07 +0300 Subject: Implement relative pointer mode on Wayland yasl_surface_set_pointer_mode(YASL_POINTER_RELATIVE) now takes a persistent zwp_locked_pointer_v1 on the surface and hides its cursor. While the compositor keeps the lock engaged, zwp_relative_pointer_v1 motion is pushed as POINTER_MOTION with pos frozen and delta in device pixels. When the lock disengages, as on focus loss, motion is absolute again with a delta derived by the input queue. The lock follows the pointer's seat: it moves to another seat when its seat loses the pointer, and is released on NORMAL and on surface destruction. Compositors without both protocols return false. Co-Authored-By: Claude Opus 5.5 --- tests/meson.build | 5 + tests/wayland/test_relative_pointer.c | 170 ++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 tests/wayland/test_relative_pointer.c (limited to 'tests') diff --git a/tests/meson.build b/tests/meson.build index 951b30e..980f5db 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -189,6 +189,11 @@ if has_wayland include_directories: inc, dependencies: yasl_dep, ) test('cursor-wayland', cursor_wayland_test) + relative_pointer_test = executable('yasl-relative-pointer-wayland-test', + 'wayland/test_relative_pointer.c', + include_directories: inc, dependencies: yasl_dep, + ) + test('relative-pointer-wayland', relative_pointer_test) endif benchmark_exe = executable( diff --git a/tests/wayland/test_relative_pointer.c b/tests/wayland/test_relative_pointer.c new file mode 100644 index 0000000..bcc41e3 --- /dev/null +++ b/tests/wayland/test_relative_pointer.c @@ -0,0 +1,170 @@ +#include "platform/wayland/internal.h" +#include + +/* Record lock, relative-pointer and cursor requests without a compositor. */ +static char lock_objects[4]; +static int locks, unlocks, pointer_hides, pointer_shapes; +static struct wl_surface *locked_surface; +static struct wl_pointer *locked_wl_pointer; +static uint32_t lock_lifetime; + +static struct zwp_locked_pointer_v1 *lock_pointer(struct zwp_pointer_constraints_v1 *mgr, + struct wl_surface *surface, struct wl_pointer *pointer, + struct wl_region *region, uint32_t lifetime) +{ + assert(mgr && surface && pointer && !region); + locked_surface = surface; + locked_wl_pointer = pointer; + lock_lifetime = lifetime; + return (struct zwp_locked_pointer_v1 *)&lock_objects[locks++ % 4]; +} + +static int lock_add_listener(struct zwp_locked_pointer_v1 *lock, + const struct zwp_locked_pointer_v1_listener *listener, void *data) +{ + assert(lock && listener && data); + return 0; +} + +static void lock_destroy(struct zwp_locked_pointer_v1 *lock) +{ + assert(lock); + unlocks++; +} + +static void pointer_cursor(struct wl_pointer *pointer, uint32_t serial, + struct wl_surface *surface, int32_t x, int32_t y) +{ + assert(pointer && serial && !surface && !x && !y); + pointer_hides++; +} + +static void cursor_shape(struct wp_cursor_shape_device_v1 *device, + uint32_t serial, uint32_t shape) +{ + assert(device && serial && shape == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT); + pointer_shapes++; +} + +static uint32_t pointer_version(struct wl_pointer *pointer) +{ + assert(pointer); + return WL_POINTER_FRAME_SINCE_VERSION; +} + +static void pointer_release(struct wl_pointer *pointer) { assert(pointer); } +static void shape_device_destroy(struct wp_cursor_shape_device_v1 *device) { assert(device); } + +#define zwp_pointer_constraints_v1_lock_pointer lock_pointer +#define zwp_locked_pointer_v1_add_listener lock_add_listener +#define zwp_locked_pointer_v1_destroy lock_destroy +#define wl_pointer_set_cursor pointer_cursor +#define wp_cursor_shape_device_v1_set_shape cursor_shape +#define wp_cursor_shape_device_v1_destroy shape_device_destroy +#define wl_pointer_get_version pointer_version +#define wl_pointer_release pointer_release +#include "../../src/platform/wayland/pointer.c" + +static struct yasl_event next_event(struct wayland_surface *w) +{ + struct yasl_event event; + assert(yasl_surface_poll_event(&w->base, &event)); + return event; +} + +static void move_to(struct seat *s, int x, int y) +{ + pointer_motion(s, s->pointer, 0, wl_fixed_from_int(x), wl_fixed_from_int(y)); + pointer_frame(s, s->pointer); +} + +static void relative_move(struct seat *s, double dx, double dy) +{ + relative_motion(s, NULL, 0, 0, wl_fixed_from_double(dx), wl_fixed_from_double(dy), 0, 0); +} + +int main(void) +{ + struct wayland_display display = {.base.ops = &yasl_wayland_backend}; + struct wayland_surface surface = {.base.display = &display.base, .conn = &display, + .scale_factor = 2, .cursor = YASL_CURSOR_TEXT}; + surface.surface = (struct wl_surface *)&surface; + wl_surface_set_user_data(surface.surface, &surface); + display.surfaces = &surface; + struct seat seat = {.conn = &display, .cursor_shape = -1}; + seat.pointer = (struct wl_pointer *)&seat; + seat.cursor_shape_dev = (struct wp_cursor_shape_device_v1 *)&seat; + display.seats = &seat; + struct yasl_event event; + + /* Without both protocols the surface stays in absolute mode. */ + assert(!yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_RELATIVE)); + assert(!surface.relative && !locks); + display.relative_pointer_mgr = (struct zwp_relative_pointer_manager_v1 *)&display; + display.pointer_constraints = (struct zwp_pointer_constraints_v1 *)&display; + + pointer_enter(&seat, seat.pointer, 10, surface.surface, wl_fixed_from_int(5), wl_fixed_from_int(5)); + pointer_frame(&seat, seat.pointer); + assert(pointer_shapes == 1); + event = next_event(&surface); + assert(event.type == YASL_EVENT_POINTER_MOTION && event.pointer.pos.x == 10); + + /* Relative mode locks persistently on the pointer's seat and hides the cursor. */ + assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_RELATIVE)); + assert(locks == 1 && locked_surface == surface.surface && locked_wl_pointer == seat.pointer); + assert(lock_lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + assert(pointer_hides == 1 && surface.lock_seat == &seat); + + /* Until the compositor activates the lock, motion is absolute with a queue delta. */ + relative_move(&seat, 3, 3); + move_to(&seat, 7, 5); + event = next_event(&surface); + assert(event.pointer.pos.x == 14 && event.pointer.pos.y == 10); + assert(event.pointer.delta.x == 4 && event.pointer.delta.y == 0); + assert(!yasl_surface_poll_event(&surface.base, &event)); + + /* Locked: relative samples in device pixels, position frozen at the lock. */ + pointer_locked(&surface, surface.pointer_lock); + relative_move(&seat, 3.5, -2); + relative_move(&seat, 1000, 0); + event = next_event(&surface); + assert(event.type == YASL_EVENT_POINTER_MOTION); + assert(event.pointer.pos.x == 14 && event.pointer.pos.y == 10); + assert(event.pointer.delta.x == 7 && event.pointer.delta.y == -4); + event = next_event(&surface); + assert(event.pointer.delta.x == 2000 && event.pointer.pos.x == 14); + + /* Deactivation (e.g. focus loss) returns to absolute motion without unlocking. */ + pointer_unlocked(&surface, surface.pointer_lock); + relative_move(&seat, 5, 5); + assert(!yasl_surface_poll_event(&surface.base, &event)); + move_to(&seat, 8, 5); + event = next_event(&surface); + assert(event.pointer.delta.x == 2 && event.pointer.delta.y == 0); + assert(unlocks == 0 && surface.pointer_lock); + pointer_locked(&surface, surface.pointer_lock); + + /* Returning to normal destroys the lock and restores the chosen cursor. */ + assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_NORMAL)); + assert(unlocks == 1 && !surface.pointer_lock && !surface.input.relative); + assert(pointer_shapes == 2); + assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_NORMAL)); + assert(unlocks == 1); + assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_RELATIVE)); + assert(locks == 2 && pointer_hides == 2); + pointer_locked(&surface, surface.pointer_lock); + + /* Losing the seat's pointer hands the lock to another seat with a pointer. */ + struct seat other = {.conn = &display, .cursor_shape = -1}; + other.pointer = (struct wl_pointer *)&other; + seat.next = &other; + seat_drop_pointer(&seat); + assert(unlocks == 2 && locks == 3 && locked_wl_pointer == other.pointer); + assert(surface.lock_seat == &other && !surface.input.relative); + + /* Surface destruction releases the lock through surface_unlock_pointer(). */ + surface_unlock_pointer(&surface); + assert(unlocks == 3 && !surface.lock_seat && !surface.pointer_lock); + input_queue_clear(&surface.input); + return 0; +} -- cgit v1.2.3