SDL: wayland: Handle captured pointer movements over a subsurface

From 5f8eb445419c5e9c4388ac414722d7baee2a9655 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Fri, 22 May 2026 11:42:53 -0400
Subject: [PATCH] wayland: Handle captured pointer movements over a subsurface

Some compositors will send pointer enter/leave event while moving between surfaces that are part of the same window while mouse capture is active. Maintain window focus in this case, and adjust the coordinates relative to the content surface by the subsurface offset, if necessary.
---
 src/video/wayland/SDL_waylandevents.c   | 34 ++++++++++++++++++++-----
 src/video/wayland/SDL_waylandevents_c.h |  1 +
 2 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index b38ef441db6ed..e828dc9f6930d 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -685,8 +685,16 @@ static void pointer_dispatch_absolute_motion(SDL_WaylandSeat *seat)
     SDL_Window *window = window_data ? window_data->sdlwindow : NULL;
 
     if (window_data) {
-        const float sx = (float)(wl_fixed_to_double(seat->pointer.pending_frame.absolute.sx) * window_data->pointer_scale.x);
-        const float sy = (float)(wl_fixed_to_double(seat->pointer.pending_frame.absolute.sy) * window_data->pointer_scale.y);
+        double sx = wl_fixed_to_double(seat->pointer.pending_frame.absolute.sx);
+        double sy = wl_fixed_to_double(seat->pointer.pending_frame.absolute.sy);
+
+        if (seat->pointer.focus_surface == window_data->mask.surface) {
+            sx += (double)window_data->mask.offset_x;
+            sy += (double)window_data->mask.offset_y;
+        }
+
+        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, sx, sy);
 
         seat->pointer.last_motion.x = (int)SDL_floorf(sx);
@@ -816,6 +824,7 @@ static void pointer_dispatch_enter(SDL_WaylandSeat *seat)
     }
 
     seat->pointer.focus = window;
+    seat->pointer.focus_surface = seat->pointer.pending_frame.enter_surface;
     ++window->pointer_focus_count;
     SDL_SetMouseFocus(window->sdlwindow);
 
@@ -874,6 +883,7 @@ static void pointer_dispatch_leave(SDL_WaylandSeat *seat, bool update_pointer)
                 window->sdlwindow->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
 
                 seat->pointer.focus = NULL;
+                seat->pointer.focus_surface = NULL;
                 for (Uint8 i = 1; seat->pointer.buttons_pressed; ++i) {
                     if (seat->pointer.buttons_pressed & SDL_BUTTON_MASK(i)) {
                         SDL_SendMouseButton(0, window->sdlwindow, seat->pointer.sdl_id, i, false);
@@ -1283,12 +1293,24 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer)
 
     if (seat->pointer.pending_frame.enter_surface) {
         if (seat->pointer.pending_frame.leave_surface) {
-            // Leaving the previous surface before entering a new surface.
-            pointer_dispatch_leave(seat, false);
-            seat->pointer.pending_frame.leave_surface = NULL;
+            SDL_WindowData *window_data = seat->pointer.focus;
+            SDL_WindowData *new_focus = Wayland_GetWindowDataForOwnedSurface(seat->pointer.pending_frame.enter_surface);
+
+            if (window_data && (window_data->sdlwindow->flags & SDL_WINDOW_MOUSE_CAPTURE) && window_data == new_focus) {
+                // The mouse is captured and moving between owned window surfaces. Just change the focused surface.
+                seat->pointer.focus_surface = seat->pointer.pending_frame.enter_surface;
+                seat->pointer.pending_frame.enter_surface = NULL;
+                seat->pointer.pending_frame.leave_surface = NULL;
+            } else {
+                // Leaving the previous surface before entering a new surface.
+                pointer_dispatch_leave(seat, false);
+                seat->pointer.pending_frame.leave_surface = NULL;
+            }
         }
 
-        pointer_dispatch_enter(seat);
+        if (seat->pointer.pending_frame.enter_surface) {
+            pointer_dispatch_enter(seat);
+        }
     }
 
     if (seat->pointer.pending_frame.have_absolute) {
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 1a597a35f8b10..9e3d573b6c00c 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -189,6 +189,7 @@ typedef struct SDL_WaylandSeat
         struct zwp_pointer_gesture_pinch_v1 *gesture_pinch;
 
         SDL_WindowData *focus;
+        struct wl_surface *focus_surface;
 
         // According to the spec, a seat can only have one active gesture of any type at a time.
         SDL_WindowData *gesture_focus;