SDL: wayland: Add mouse pointer warp support

From 3a6d9c59f45a48d8d5a07e6f9428d45aa2069387 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 1 May 2024 12:57:39 -0400
Subject: [PATCH] wayland: Add mouse pointer warp support

The pointer confinement protocol does allow attempted warping the pointer via a hint, provided that the pointer is locked at the time of the request, and the requested coordinates fall within the bounds of the window.

Toggle the pointer locked state and request the pointer warp when the required protocol is available. This is similar to what XWayland does internally.
---
 docs/README-wayland.md                  |  5 +-
 include/SDL3/SDL_hints.h                | 20 ++++----
 src/video/wayland/SDL_waylandevents.c   | 63 ++++++++++++++-----------
 src/video/wayland/SDL_waylandevents_c.h |  7 ++-
 src/video/wayland/SDL_waylandmouse.c    | 63 +++++++++++++++++++++----
 src/video/wayland/SDL_waylandwindow.c   |  2 +-
 6 files changed, 108 insertions(+), 52 deletions(-)

diff --git a/docs/README-wayland.md b/docs/README-wayland.md
index 6b4aa6ce2c80a..6dac845ca57e5 100644
--- a/docs/README-wayland.md
+++ b/docs/README-wayland.md
@@ -39,9 +39,10 @@ encounter limitations or behavior that is different from other windowing systems
   unknown. In most cases, applications don't actually need the global cursor position and should use the window-relative
   coordinates as provided by the mouse movement event or from ```SDL_GetMouseState()``` instead.
 
-### Warping the global mouse cursor position via ```SDL_WarpMouseGlobal()``` doesn't work
+### Warping the mouse cursor to or from a point outside the window doesn't work
 
-- For security reasons, Wayland does not allow warping the global mouse cursor position.
+- The cursor can be warped only within the window with mouse focus, provided that the `zwp_pointer_confinement_v1`
+  protocol is supported by the compositor.
 
 ### The application icon can't be set via ```SDL_SetWindowIcon()```
 
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 60ba7f3ca2656..c8f12ce036b4d 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -2995,19 +2995,21 @@ extern "C" {
 #define SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR "SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR"
 
 /**
- * Enable or disable mouse pointer warp emulation, needed by some older games.
+ * Enable or disable hidden mouse pointer warp emulation, needed by some older games.
  *
- * Wayland does not directly support warping the mouse. When this hint is set,
- * any SDL will emulate mouse warps using relative mouse mode. This is
- * required for some older games (such as Source engine games), which warp the
- * mouse to the centre of the screen rather than using relative mouse motion.
- * Note that relative mouse mode may have different mouse acceleration
- * behaviour than pointer warps.
+ * Wayland requires the pointer confinement protocol to warp the mouse, but
+ * that is just a hint that the compositor is free to ignore, and warping the
+ * the pointer to or from regions outside of the focused window is prohibited.
+ * When this hint is set and the pointer is hidden, SDL will emulate mouse warps
+ * using relative mouse mode. This is required for some older games (such as Source
+ * engine games), which warp the mouse to the centre of the screen rather than using
+ * relative mouse motion. Note that relative mouse mode may have different mouse
+ * acceleration behaviour than pointer warps.
  *
  * The variable can be set to the following values:
  *
- * - "0": All mouse warps fail, as mouse warping is not available under
- *   wayland.
+ * - "0": Attempts to warp the mouse will be made, if the appropriate protocol
+ *        is available.
  * - "1": Some mouse warps will be emulated by forcing relative mouse mode.
  *
  * If not set, this is automatically enabled unless an application uses
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index b8f23ead40b1c..6e1be4c4415c9 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -3197,32 +3197,47 @@ static const struct zwp_locked_pointer_v1_listener locked_pointer_listener = {
     locked_pointer_unlocked,
 };
 
-static void lock_pointer_to_window(SDL_Window *window,
-                                   struct SDL_WaylandInput *input)
+int Wayland_input_lock_pointer(struct SDL_WaylandInput *input, SDL_Window *window)
 {
     SDL_WindowData *w = window->driverdata;
     SDL_VideoData *d = input->display;
-    struct zwp_locked_pointer_v1 *locked_pointer;
 
     if (!d->pointer_constraints || !input->pointer) {
-        return;
+        return -1;
     }
 
+    if (!w->locked_pointer) {
+        if (w->confined_pointer) {
+            /* If the pointer is already confined to the surface, the lock will fail with a protocol error. */
+            Wayland_input_unconfine_pointer(input, window);
+        }
+
+        w->locked_pointer = zwp_pointer_constraints_v1_lock_pointer(d->pointer_constraints,
+                                                                    w->surface,
+                                                                    input->pointer,
+                                                                    NULL,
+                                                                    ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
+        zwp_locked_pointer_v1_add_listener(w->locked_pointer,
+                                           &locked_pointer_listener,
+                                           window);
+    }
+
+    return 0;
+}
+
+int Wayland_input_unlock_pointer(struct SDL_WaylandInput *input, SDL_Window *window)
+{
+    SDL_WindowData *w = window->driverdata;
+
     if (w->locked_pointer) {
-        return;
+        zwp_locked_pointer_v1_destroy(w->locked_pointer);
+        w->locked_pointer = NULL;
     }
 
-    locked_pointer =
-        zwp_pointer_constraints_v1_lock_pointer(d->pointer_constraints,
-                                                w->surface,
-                                                input->pointer,
-                                                NULL,
-                                                ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
-    zwp_locked_pointer_v1_add_listener(locked_pointer,
-                                       &locked_pointer_listener,
-                                       window);
+    /* Restore existing pointer confinement. */
+    Wayland_input_confine_pointer(input, window);
 
-    w->locked_pointer = locked_pointer;
+    return 0;
 }
 
 static void pointer_confine_destroy(SDL_Window *window)
@@ -3234,7 +3249,7 @@ static void pointer_confine_destroy(SDL_Window *window)
     }
 }
 
-int Wayland_input_lock_pointer(struct SDL_WaylandInput *input)
+int Wayland_input_enable_relative_pointer(struct SDL_WaylandInput *input)
 {
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
     SDL_VideoData *d = input->display;
@@ -3261,10 +3276,7 @@ int Wayland_input_lock_pointer(struct SDL_WaylandInput *input)
     }
 
     if (!input->relative_pointer) {
-        relative_pointer =
-            zwp_relative_pointer_manager_v1_get_relative_pointer(
-                d->relative_pointer_manager,
-                input->pointer);
+        relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(d->relative_pointer_manager, input->pointer);
         zwp_relative_pointer_v1_add_listener(relative_pointer,
                                              &relative_pointer_listener,
                                              input);
@@ -3272,7 +3284,7 @@ int Wayland_input_lock_pointer(struct SDL_WaylandInput *input)
     }
 
     for (window = vd->windows; window; window = window->next) {
-        lock_pointer_to_window(window, input);
+        Wayland_input_lock_pointer(input, window);
     }
 
     d->relative_mouse_mode = 1;
@@ -3280,19 +3292,14 @@ int Wayland_input_lock_pointer(struct SDL_WaylandInput *input)
     return 0;
 }
 
-int Wayland_input_unlock_pointer(struct SDL_WaylandInput *input)
+int Wayland_input_disable_relative_pointer(struct SDL_WaylandInput *input)
 {
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
     SDL_VideoData *d = input->display;
     SDL_Window *window;
-    SDL_WindowData *w;
 
     for (window = vd->windows; window; window = window->next) {
-        w = window->driverdata;
-        if (w->locked_pointer) {
-            zwp_locked_pointer_v1_destroy(w->locked_pointer);
-        }
-        w->locked_pointer = NULL;
+        Wayland_input_unlock_pointer(input, window);
     }
 
     if (input->relative_pointer) {
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 2103209f00563..5559901c4e4ad 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -200,8 +200,11 @@ extern void Wayland_create_text_input(SDL_VideoData *d);
 extern void Wayland_input_initialize_seat(SDL_VideoData *d);
 extern void Wayland_display_destroy_input(SDL_VideoData *d);
 
-extern int Wayland_input_lock_pointer(struct SDL_WaylandInput *input);
-extern int Wayland_input_unlock_pointer(struct SDL_WaylandInput *input);
+extern int Wayland_input_enable_relative_pointer(struct SDL_WaylandInput *input);
+extern int Wayland_input_disable_relative_pointer(struct SDL_WaylandInput *input);
+
+extern int Wayland_input_lock_pointer(struct SDL_WaylandInput *input, SDL_Window *window);
+extern int Wayland_input_unlock_pointer(struct SDL_WaylandInput *input, SDL_Window *window);
 
 extern int Wayland_input_confine_pointer(struct SDL_WaylandInput *input, SDL_Window *window);
 extern int Wayland_input_unconfine_pointer(struct SDL_WaylandInput *input, SDL_Window *window);
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index a95afae6e6ba0..c5e1625289eff 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -36,6 +36,7 @@
 #include "SDL_waylandshmbuffer.h"
 
 #include "cursor-shape-v1-client-protocol.h"
+#include "pointer-constraints-unstable-v1-client-protocol.h"
 
 #include "../../SDL_hints_c.h"
 
@@ -556,7 +557,7 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
         input->cursor_visible = SDL_TRUE;
 
         if (input->relative_mode_override) {
-            Wayland_input_unlock_pointer(input);
+            Wayland_input_disable_relative_pointer(input);
             input->relative_mode_override = SDL_FALSE;
         }
 
@@ -572,21 +573,62 @@ static int Wayland_WarpMouse(SDL_Window *window, float x, float y)
 {
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
     SDL_VideoData *d = vd->driverdata;
+    SDL_WindowData *wind = window->driverdata;
     struct SDL_WaylandInput *input = d->input;
 
-    if (input->cursor_visible == SDL_TRUE) {
-        return SDL_Unsupported();
+    if (input->cursor_visible || (input->warp_emulation_prohibited && !d->relative_mouse_mode)) {
+        if (d->pointer_constraints) {
+            const SDL_bool toggle_lock = !wind->locked_pointer;
+
+            /* The pointer confinement protocol allows setting a hint to warp the pointer,
+             * but only when the pointer is locked.
+             *
+             * Lock the pointer, set the position hint, unlock, and hope for the best.
+             */
+            if (toggle_lock) {
+                Wayland_input_lock_pointer(input, window);
+            }
+            if (wind->locked_pointer) {
+                zwp_locked_pointer_v1_set_cursor_position_hint(wind->locked_pointer, wl_fixed_from_double(x), wl_fixed_from_double(y));
+                wl_surface_commit(wind->surface);
+            }
+            if (toggle_lock) {
+                Wayland_input_unlock_pointer(input, window);
+            }
+
+            /* NOTE: There is a pending warp event under discussion that should replace this when available.
+             * https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/340
+             */
+            SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, SDL_FALSE, x, y);
+        } else {
+            return SDL_SetError("wayland: mouse warp failed; compositor lacks support for the required zwp_pointer_confinement_v1 protocol");
+        }
     } else if (input->warp_emulation_prohibited) {
         return SDL_Unsupported();
-    } else {
-        if (!d->relative_mouse_mode) {
-            Wayland_input_lock_pointer(input);
-            input->relative_mode_override = SDL_TRUE;
-        }
+    } else if (!d->relative_mouse_mode) {
+        Wayland_input_lock_pointer(input, window);
+        input->relative_mode_override = SDL_TRUE;
     }
+
     return 0;
 }
 
+static int Wayland_WarpMouseGlobal(float x, float y)
+{
+    SDL_VideoDevice *vd = SDL_GetVideoDevice();
+    SDL_VideoData *d = vd->driverdata;
+    struct SDL_WaylandInput *input = d->input;
+    SDL_WindowData *wind = input->pointer_focus;
+
+    /* If the client wants the coordinates warped to within the focused window, just convert the coordinates to relative. */
+    if (wind) {
+        SDL_Window *window = wind->sdlwindow;
+        return Wayland_WarpMouse(window, x - (float)window->x, y - (float)window->y);
+    }
+
+    return SDL_SetError("wayland: can't warp the mouse when a window does not have focus");
+}
+
 static int Wayland_SetRelativeMouseMode(SDL_bool enabled)
 {
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
@@ -603,9 +645,9 @@ static int Wayland_SetRelativeMouseMode(SDL_bool enabled)
          * mouse warp emulation by default.
          */
         data->input->warp_emulation_prohibited = SDL_TRUE;
-        return Wayland_input_lock_pointer(data->input);
+        return Wayland_input_enable_relative_pointer(data->input);
     } else {
-        return Wayland_input_unlock_pointer(data->input);
+        return Wayland_input_disable_relative_pointer(data->input);
     }
 }
 
@@ -713,6 +755,7 @@ void Wayland_InitMouse(void)
     mouse->ShowCursor = Wayland_ShowCursor;
     mouse->FreeCursor = Wayland_FreeCursor;
     mouse->WarpMouse = Wayland_WarpMouse;
+    mouse->WarpMouseGlobal = Wayland_WarpMouseGlobal;
     mouse->SetRelativeMouseMode = Wayland_SetRelativeMouseMode;
     mouse->GetGlobalMouseState = Wayland_GetGlobalMouseState;
 
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 736d09c1a4c49..bfbde9e926b49 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -2350,7 +2350,7 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
 #endif
 
     if (c->relative_mouse_mode) {
-        Wayland_input_lock_pointer(c->input);
+        Wayland_input_enable_relative_pointer(c->input);
     }
 
     /* We may need to create an idle inhibitor for this new window */