From 6f81c70f67a74f5899ef486f2d709e45e96656a0 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 13 Oct 2025 10:48:17 -0400
Subject: [PATCH] wayland: Clean up gesture support
The gesture capability is tied to the pointer capability, not touch, and may not always be exposed by the compositor.
---
src/video/wayland/SDL_waylandevents.c | 128 +++++++++++++++---------
src/video/wayland/SDL_waylandevents_c.h | 6 +-
src/video/wayland/SDL_waylandvideo.c | 9 +-
3 files changed, 91 insertions(+), 52 deletions(-)
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 2b89953f4b934..48ef0dea79061 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -46,6 +46,8 @@
#include "primary-selection-unstable-v1-client-protocol.h"
#include "input-timestamps-unstable-v1-client-protocol.h"
#include "pointer-gestures-unstable-v1-client-protocol.h"
+#include "cursor-shape-v1-client-protocol.h"
+#include "viewporter-client-protocol.h"
#ifdef HAVE_LIBDECOR_H
#include <libdecor.h>
@@ -67,8 +69,6 @@
#include <unistd.h>
#include <xkbcommon/xkbcommon-compose.h>
#include <xkbcommon/xkbcommon.h>
-#include "cursor-shape-v1-client-protocol.h"
-#include "viewporter-client-protocol.h"
// Weston uses a ratio of 10 units per scroll tick
#define WAYLAND_WHEEL_AXIS_UNIT 10
@@ -289,6 +289,71 @@ void Wayland_DisplayInitInputTimestampManager(SDL_VideoData *display)
}
}
+static void handle_pinch_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers)
+{
+ if (!surface) {
+ return;
+ }
+
+ SDL_WindowData *wind = Wayland_GetWindowDataForOwnedSurface(surface);
+ if (wind) {
+ SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
+ seat->pointer.gesture_focus = wind;
+
+ const Uint64 timestamp = Wayland_GetPointerTimestamp(seat, time);
+ SDL_SendPinch(SDL_EVENT_PINCH_BEGIN, timestamp, wind->sdlwindow, 0.0f);
+ }
+}
+
+static void handle_pinch_update(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t time,
+ wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation)
+{
+ SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
+
+ if (seat->pointer.gesture_focus) {
+ const Uint64 timestamp = Wayland_GetPointerTimestamp(seat, time);
+ const float s = (float)wl_fixed_to_double(scale);
+ SDL_SendPinch(SDL_EVENT_PINCH_UPDATE, timestamp, seat->pointer.gesture_focus->sdlwindow, s);
+ }
+}
+
+static void handle_pinch_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled)
+{
+ SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
+
+ if (seat->pointer.gesture_focus) {
+ const Uint64 timestamp = Wayland_GetPointerTimestamp(seat, time);
+ SDL_SendPinch(SDL_EVENT_PINCH_END, timestamp, seat->pointer.gesture_focus->sdlwindow, 0.0f);
+
+ seat->pointer.gesture_focus = NULL;
+ }
+}
+
+static const struct zwp_pointer_gesture_pinch_v1_listener gesture_pinch_listener = {
+ handle_pinch_begin,
+ handle_pinch_update,
+ handle_pinch_end
+};
+
+static void Wayland_SeatCreatePointerGestures(SDL_WaylandSeat *seat)
+{
+ if (seat->display->zwp_pointer_gestures) {
+ if (seat->pointer.wl_pointer && !seat->pointer.gesture_pinch) {
+ seat->pointer.gesture_pinch = zwp_pointer_gestures_v1_get_pinch_gesture(seat->display->zwp_pointer_gestures, seat->pointer.wl_pointer);
+ zwp_pointer_gesture_pinch_v1_set_user_data(seat->pointer.gesture_pinch, seat);
+ zwp_pointer_gesture_pinch_v1_add_listener(seat->pointer.gesture_pinch, &gesture_pinch_listener, seat);
+ }
+ }
+}
+
+void Wayland_DisplayInitPointerGestureManager(SDL_VideoData *display)
+{
+ SDL_WaylandSeat *seat;
+ wl_list_for_each (seat, &display->seat_list, link) {
+ Wayland_SeatCreatePointerGestures(seat);
+ }
+}
+
static void Wayland_SeatCreateCursorShape(SDL_WaylandSeat *seat)
{
if (seat->display->cursor_shape_manager) {
@@ -1417,43 +1482,6 @@ static const struct wl_touch_listener touch_listener = {
touch_handler_orientation // Version 6
};
-void pinch_begin(void *data,
- struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1,
- uint32_t serial,
- uint32_t time,
- struct wl_surface *surface,
- uint32_t fingers)
-{
- SDL_SendPinch(SDL_EVENT_PINCH_BEGIN, 0, NULL, 0);
-}
-void pinch_update(void *data,
- struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1,
- uint32_t time,
- wl_fixed_t dx,
- wl_fixed_t dy,
- wl_fixed_t scale,
- wl_fixed_t rotation)
-{
-
- float s = (float)(wl_fixed_to_double(scale));
- SDL_SendPinch(SDL_EVENT_PINCH_UPDATE, 0, NULL, s);
-}
-
-void pinch_end(void *data,
- struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1,
- uint32_t serial,
- uint32_t time,
- int32_t cancelled)
-{
- SDL_SendPinch(SDL_EVENT_PINCH_END, 0, NULL, 0);
-}
-
-static const struct zwp_pointer_gesture_pinch_v1_listener gesture_pinch_listener = {
- pinch_begin,
- pinch_update,
- pinch_end
-};
-
// Fallback for xkb_keymap_key_get_mods_for_level(), which is only available from 1.0.0, while the SDL minimum is 0.5.0.
#if !SDL_XKBCOMMON_CHECK_VERSION(1, 0, 0)
static size_t xkb_legacy_get_mods_for_level(SDL_WaylandSeat *seat, xkb_keycode_t key, xkb_layout_index_t layout, xkb_level_index_t level, xkb_mod_mask_t *masks_out, size_t masks_size)
@@ -2296,6 +2324,11 @@ static const struct wl_keyboard_listener keyboard_listener = {
static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat, bool send_event)
{
+ // End any active gestures.
+ if (seat->pointer.gesture_focus) {
+ SDL_SendPinch(SDL_EVENT_PINCH_END, 0, seat->pointer.gesture_focus->sdlwindow, 0.0f);
+ }
+
// Make sure focus is removed from a surface before the pointer is destroyed.
if (seat->pointer.focus) {
pointer_handle_leave(seat, seat->pointer.wl_pointer, 0, seat->pointer.focus->surface);
@@ -2319,6 +2352,10 @@ static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat, bool send_event)
zwp_input_timestamps_v1_destroy(seat->pointer.timestamps);
}
+ if (seat->pointer.gesture_pinch) {
+ zwp_pointer_gesture_pinch_v1_destroy(seat->pointer.gesture_pinch);
+ }
+
if (seat->pointer.cursor_state.frame_callback) {
wl_callback_destroy(seat->pointer.cursor_state.frame_callback);
}
@@ -2425,10 +2462,6 @@ static void Wayland_SeatDestroyTouch(SDL_WaylandSeat *seat)
}
}
- if (seat->touch.gesture_pinch) {
- zwp_pointer_gesture_pinch_v1_destroy(seat->touch.gesture_pinch);
- }
-
SDL_zero(seat->touch);
WAYLAND_wl_list_init(&seat->touch.points);
}
@@ -2447,6 +2480,9 @@ static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum w
wl_pointer_set_user_data(seat->pointer.wl_pointer, seat);
wl_pointer_add_listener(seat->pointer.wl_pointer, &pointer_listener, seat);
+ // Pointer gestures
+ Wayland_SeatCreatePointerGestures(seat);
+
seat->pointer.sdl_id = SDL_GetNextObjectID();
if (seat->name) {
@@ -2472,12 +2508,6 @@ static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum w
}
SDL_AddTouch((SDL_TouchID)(uintptr_t)seat->touch.wl_touch, SDL_TOUCH_DEVICE_DIRECT, name_fmt);
-
- /* Pinch gesture */
- seat->touch.gesture_pinch = zwp_pointer_gestures_v1_get_pinch_gesture(seat->display->zwp_pointer_gestures, seat->pointer.wl_pointer);
- zwp_pointer_gesture_pinch_v1_set_user_data(seat->touch.gesture_pinch, seat);
- zwp_pointer_gesture_pinch_v1_add_listener(seat->touch.gesture_pinch, &gesture_pinch_listener, seat);
-
} else if (!(capabilities & WL_SEAT_CAPABILITY_TOUCH) && seat->touch.wl_touch) {
Wayland_SeatDestroyTouch(seat);
}
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 70da05763ace5..03c44375f6369 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -123,10 +123,14 @@ typedef struct SDL_WaylandSeat
struct wp_cursor_shape_device_v1 *cursor_shape;
struct zwp_locked_pointer_v1 *locked_pointer;
struct zwp_confined_pointer_v1 *confined_pointer;
+ struct zwp_pointer_gesture_pinch_v1 *gesture_pinch;
SDL_WindowData *focus;
SDL_CursorData *current_cursor;
+ // According to the spec, a seat can only have one active gesture of any type at a time.
+ SDL_WindowData *gesture_focus;
+
Uint64 highres_timestamp_ns;
Uint32 enter_serial;
SDL_MouseButtonFlags buttons_pressed;
@@ -192,7 +196,6 @@ typedef struct SDL_WaylandSeat
struct zwp_input_timestamps_v1 *timestamps;
Uint64 highres_timestamp_ns;
struct wl_list points;
- struct zwp_pointer_gesture_pinch_v1 *gesture_pinch;
} touch;
struct
@@ -220,6 +223,7 @@ extern int Wayland_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS);
extern void Wayland_DisplayInitInputTimestampManager(SDL_VideoData *display);
extern void Wayland_DisplayInitCursorShapeManager(SDL_VideoData *display);
+extern void Wayland_DisplayInitPointerGestureManager(SDL_VideoData *display);
extern void Wayland_DisplayInitTabletManager(SDL_VideoData *display);
extern void Wayland_DisplayInitDataDeviceManager(SDL_VideoData *display);
extern void Wayland_DisplayInitPrimarySelectionDeviceManager(SDL_VideoData *display);
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index cdfe4227b9e91..d4f97f6547f46 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -1324,7 +1324,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin
} else if (SDL_strcmp(interface, "wp_pointer_warp_v1") == 0) {
d->wp_pointer_warp_v1 = wl_registry_bind(d->registry, id, &wp_pointer_warp_v1_interface, 1);
} else if (SDL_strcmp(interface, "zwp_pointer_gestures_v1") == 0) {
- d->zwp_pointer_gestures = wl_registry_bind(d->registry, id, &zwp_pointer_gestures_v1_interface, 1);
+ d->zwp_pointer_gestures = wl_registry_bind(d->registry, id, &zwp_pointer_gestures_v1_interface, SDL_min(version, 3));
+ Wayland_DisplayInitPointerGestureManager(d);
}
#ifdef SDL_WL_FIXES_VERSION
else if (SDL_strcmp(interface, "wl_fixes") == 0) {
@@ -1649,7 +1650,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
}
if (data->zwp_pointer_gestures) {
- zwp_pointer_gestures_v1_destroy(data->zwp_pointer_gestures);
+ if (zwp_pointer_gestures_v1_get_version(data->zwp_pointer_gestures) >= ZWP_POINTER_GESTURES_V1_RELEASE_SINCE_VERSION) {
+ zwp_pointer_gestures_v1_release(data->zwp_pointer_gestures);
+ } else {
+ zwp_pointer_gestures_v1_destroy(data->zwp_pointer_gestures);
+ }
data->zwp_pointer_gestures = NULL;
}