SDL: wayland: Clean up gesture support

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;
     }