SDL: mouse: Handle explicit warp events

From ee1c942a92f236380cc92a417475614c1f6d5b7c Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sun, 12 Apr 2026 12:04:58 -0400
Subject: [PATCH] mouse: Handle explicit warp events

The warp event in Wayland can occur without explicitly warping the mouse, such as if a window changes position under the cursor while the cursor is stationary. Introduce the internal SDL_SendMouseWarp() function to handle updating the absolute cursor position without generating relative motion for these cases.
---
 src/events/SDL_mouse.c                  | 66 ++++++++++++++++++-------
 src/events/SDL_mouse_c.h                |  4 ++
 src/video/wayland/SDL_waylandevents.c   | 15 ++++--
 src/video/wayland/SDL_waylandevents_c.h |  1 +
 src/video/wayland/SDL_waylandmouse.c    |  4 +-
 5 files changed, 66 insertions(+), 24 deletions(-)

diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c
index 62cfb362d6776..e00e67c64ef8d 100644
--- a/src/events/SDL_mouse.c
+++ b/src/events/SDL_mouse.c
@@ -662,6 +662,31 @@ void SDL_SendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouse
     SDL_PrivateSendMouseMotion(timestamp, window, mouseID, relative, x, y);
 }
 
+void SDL_SendMouseWarp(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, float x, float y)
+{
+    SDL_Mouse *mouse = SDL_GetMouse();
+
+    // Ignore the previous position when we warp, as warps don't generate relative motion.
+    mouse->last_x = x;
+    mouse->last_y = y;
+    mouse->has_position = false;
+
+    if (mouse->relative_mode) {
+        /* Sending motion events when warping while relative mode is active can confuse
+         * clients that don't expect it, so just update the absolute position and don't
+         * generate a motion event unless SDL_HINT_MOUSE_RELATIVE_WARP_MOTION is set.
+         */
+        if (!mouse->relative_mode_warp_motion) {
+            mouse->x = x;
+            mouse->y = y;
+            mouse->has_position = true;
+            return;
+        }
+    }
+
+    SDL_SendMouseMotion(timestamp, window, mouseID, false, x, y);
+}
+
 static void ConstrainMousePosition(SDL_Mouse *mouse, SDL_Window *window, float *x, float *y)
 {
     /* make sure that the pointers find themselves inside the windows,
@@ -1263,24 +1288,29 @@ void SDL_PerformWarpMouseInWindow(SDL_Window *window, float x, float y, bool ign
         return;
     }
 
-    // Ignore the previous position when we warp
-    mouse->last_x = x;
-    mouse->last_y = y;
-    mouse->has_position = false;
-
-    if (mouse->relative_mode && !ignore_relative_mode) {
-        /* 2.0.22 made warping in relative mode actually functional, which
-         * surprised many applications that weren't expecting the additional
-         * mouse motion.
-         *
-         * So for now, warping in relative mode adjusts the absolution position
-         * but doesn't generate motion events, unless SDL_HINT_MOUSE_RELATIVE_WARP_MOTION is set.
-         */
-        if (!mouse->relative_mode_warp_motion) {
-            mouse->x = x;
-            mouse->y = y;
-            mouse->has_position = true;
-            return;
+    /* If the backend sends explicit warp events, this will be taken care of if/when the pointer actually warps,
+     * Warps when in relative save the position to be applied when leaving relative mode.
+     */
+    if (!mouse->have_explicit_warp_event || mouse->relative_mode) {
+        // Ignore the previous position when we warp, as warps don't generate relative motion.
+        mouse->last_x = x;
+        mouse->last_y = y;
+        mouse->has_position = false;
+
+        if (mouse->relative_mode && !ignore_relative_mode) {
+            /* 2.0.22 made warping in relative mode actually functional, which
+             * surprised many applications that weren't expecting the additional
+             * mouse motion.
+             *
+             * So for now, warping in relative mode adjusts the absolute position, but
+             * doesn't generate motion events, unless SDL_HINT_MOUSE_RELATIVE_WARP_MOTION is set.
+             */
+            if (!mouse->relative_mode_warp_motion) {
+                mouse->x = x;
+                mouse->y = y;
+                mouse->has_position = true;
+                return;
+            }
         }
     }
 
diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h
index 36c458df4335d..26a9d0751aa3f 100644
--- a/src/events/SDL_mouse_c.h
+++ b/src/events/SDL_mouse_c.h
@@ -136,6 +136,7 @@ typedef struct
     bool warp_emulation_hint;
     bool warp_emulation_active;
     bool warp_emulation_prohibited;
+    bool have_explicit_warp_event;
     Uint64 last_center_warp_time_ns;
     bool enable_normal_speed_scale;
     float normal_speed_scale;
@@ -210,6 +211,9 @@ extern bool SDL_UpdateMouseCapture(bool force_release);
 // Send a mouse motion event
 extern void SDL_SendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, bool relative, float x, float y);
 
+// Send a mouse motion event resulting from a pointer warp
+void SDL_SendMouseWarp(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, float x, float y);
+
 // Send a mouse button event
 extern void SDL_SendMouseButton(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, Uint8 button, bool down);
 
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 2d31def0c7408..2d2e05e7d5978 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -640,7 +640,7 @@ void Wayland_PumpEvents(SDL_VideoDevice *_this)
     }
 }
 
-static void pointer_dispatch_absolute_motion(SDL_WaylandSeat *seat)
+static void pointer_dispatch_absolute_motion(SDL_WaylandSeat *seat, bool warp)
 {
     SDL_WindowData *window_data = seat->pointer.focus;
     SDL_Window *window = window_data ? window_data->sdlwindow : NULL;
@@ -656,7 +656,11 @@ static void pointer_dispatch_absolute_motion(SDL_WaylandSeat *seat)
 
         sx *= window_data->pointer_scale.x;
         sy *= window_data->pointer_scale.y;
-        SDL_SendMouseMotion(seat->pointer.pending_frame.timestamp_ns, window_data->sdlwindow, seat->pointer.sdl_id, false, (float)sx, (float)sy);
+        if (!warp) {
+            SDL_SendMouseMotion(seat->pointer.pending_frame.timestamp_ns, window_data->sdlwindow, seat->pointer.sdl_id, false, (float)sx, (float)sy);
+        } else {
+            SDL_SendMouseWarp(seat->pointer.pending_frame.timestamp_ns, window_data->sdlwindow, seat->pointer.sdl_id, (float)sx, (float)sy);
+        }
 
         seat->pointer.last_motion.x = (int)SDL_floor(sx);
         seat->pointer.last_motion.y = (int)SDL_floor(sy);
@@ -763,7 +767,7 @@ static void pointer_handle_motion(void *data, struct wl_pointer *pointer,
         }
     } else {
         seat->pointer.pending_frame.timestamp_ns = timestamp;
-        pointer_dispatch_absolute_motion(seat);
+        pointer_dispatch_absolute_motion(seat, false);
     }
 }
 
@@ -790,7 +794,7 @@ static void pointer_dispatch_enter(SDL_WaylandSeat *seat)
     SDL_SetMouseFocus(window->sdlwindow);
 
     // Send the initial position.
-    pointer_dispatch_absolute_motion(seat);
+    pointer_dispatch_absolute_motion(seat, false);
 
     // Update the pointer grab state.
     Wayland_SeatUpdatePointerGrab(seat);
@@ -1280,7 +1284,7 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer)
     }
 
     if (seat->pointer.pending_frame.have_absolute) {
-        pointer_dispatch_absolute_motion(seat);
+        pointer_dispatch_absolute_motion(seat, seat->pointer.pending_frame.have_warp);
     }
 
     if (seat->pointer.pending_frame.have_relative) {
@@ -1341,6 +1345,7 @@ static void pointer_handle_warp(void *data, struct wl_pointer *wl_pointer, wl_fi
     SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
 
     seat->pointer.pending_frame.have_absolute = true;
+    seat->pointer.pending_frame.have_warp = true;
     seat->pointer.pending_frame.absolute.sx = surface_x;
     seat->pointer.pending_frame.absolute.sy = surface_y;
 }
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 42faffd7fab7f..49bae2d8e671b 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -205,6 +205,7 @@ typedef struct SDL_WaylandSeat
         {
             bool have_absolute;
             bool have_relative;
+            bool have_warp;
             bool have_axis;
 
             Uint32 buttons_pressed;
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index dea9d3ae05ca0..c1eaa9b8f3b47 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -1199,7 +1199,7 @@ void Wayland_SeatWarpMouse(SDL_WaylandSeat *seat, SDL_WindowData *window, float
         }
 
         if (wl_pointer_get_version(seat->pointer.wl_pointer) < WL_POINTER_WARP_SINCE_VERSION) {
-            SDL_SendMouseMotion(0, window->sdlwindow, seat->pointer.sdl_id, false, x, y);
+            SDL_SendMouseWarp(0, window->sdlwindow, seat->pointer.sdl_id, x, y);
         }
     }
 }
@@ -1383,6 +1383,8 @@ void Wayland_InitMouse(SDL_VideoData *data)
     mouse->SetRelativeMouseMode = Wayland_SetRelativeMouseMode;
     mouse->GetGlobalMouseState = Wayland_GetGlobalMouseState;
 
+    mouse->have_explicit_warp_event = true;
+
     SDL_HitTestResult r = SDL_HITTEST_NORMAL;
     while (r <= SDL_HITTEST_RESIZE_LEFT) {
         switch (r) {