SDL: wayland: Set tablet cursors separately from pointer cursors

From 6a510d6174e853f47e0dfee1d7a258aaa777968e Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 29 Oct 2025 12:34:55 -0400
Subject: [PATCH] wayland: Set tablet cursors separately from pointer cursors

Some compositors don't implicitly use the pointer cursor when the tablet cursor is not set, and the presence of a tablet doesn't necessarily guarantee pointer capability. Set the cursor for tablet tools independently of pointer cursors.

This required refactoring of cursor state handling, as well as some tablet related structures.
---
 src/video/wayland/SDL_waylandevents.c   | 210 ++++++-----
 src/video/wayland/SDL_waylandevents_c.h |  78 +++-
 src/video/wayland/SDL_waylandmouse.c    | 465 +++++++++++++-----------
 src/video/wayland/SDL_waylandmouse.h    |   8 +-
 4 files changed, 425 insertions(+), 336 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 46c8877d8c78b..b69116a07e603 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -373,8 +373,15 @@ void Wayland_DisplayInitPointerGestureManager(SDL_VideoData *display)
 static void Wayland_SeatCreateCursorShape(SDL_WaylandSeat *seat)
 {
     if (seat->display->cursor_shape_manager) {
-        if (seat->pointer.wl_pointer && !seat->pointer.cursor_shape) {
-            seat->pointer.cursor_shape = wp_cursor_shape_manager_v1_get_pointer(seat->display->cursor_shape_manager, seat->pointer.wl_pointer);
+        if (seat->pointer.wl_pointer && !seat->pointer.cursor_state.cursor_shape) {
+            seat->pointer.cursor_state.cursor_shape = wp_cursor_shape_manager_v1_get_pointer(seat->display->cursor_shape_manager, seat->pointer.wl_pointer);
+        }
+
+        SDL_WaylandPenTool *tool;
+        wl_list_for_each(tool, &seat->tablet.tool_list, link) {
+            if (!tool->cursor_state.cursor_shape) {
+                tool->cursor_state.cursor_shape = wp_cursor_shape_manager_v1_get_tablet_tool_v2(seat->display->cursor_shape_manager, tool->wltool);
+            }
         }
     }
 }
@@ -770,7 +777,7 @@ static void pointer_dispatch_absolute_motion(SDL_WaylandSeat *seat)
 
             if (rc != window_data->hit_test_result) {
                 window_data->hit_test_result = rc;
-                Wayland_SeatUpdateCursor(seat);
+                Wayland_SeatUpdatePointerCursor(seat);
             }
         }
     }
@@ -852,7 +859,7 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
          * window that already has focus, as the focus change sequence
          * won't be run.
          */
-        Wayland_SeatUpdateCursor(seat);
+        Wayland_SeatUpdatePointerCursor(seat);
     }
 }
 
@@ -892,7 +899,7 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
     }
 
     Wayland_SeatUpdatePointerGrab(seat);
-    Wayland_SeatUpdateCursor(seat);
+    Wayland_SeatUpdatePointerCursor(seat);
 }
 
 static bool Wayland_ProcessHitTest(SDL_WaylandSeat *seat, Uint32 serial)
@@ -1269,7 +1276,7 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer)
              * window that already has focus, as the focus change sequence
              * won't be run.
              */
-            Wayland_SeatUpdateCursor(seat);
+            Wayland_SeatUpdatePointerCursor(seat);
         }
     }
 
@@ -2354,7 +2361,7 @@ static const struct wl_keyboard_listener keyboard_listener = {
 
 static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat, bool send_event)
 {
-    Wayland_SeatDestroyCursorFrameCallback(seat);
+    Wayland_CursorStateRelease(&seat->pointer.cursor_state);
 
     // End any active gestures.
     if (seat->pointer.gesture_focus) {
@@ -2388,18 +2395,6 @@ static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat, bool send_event)
         zwp_pointer_gesture_pinch_v1_destroy(seat->pointer.gesture_pinch);
     }
 
-    if (seat->pointer.cursor_state.surface) {
-        wl_surface_destroy(seat->pointer.cursor_state.surface);
-    }
-
-    if (seat->pointer.cursor_state.viewport) {
-        wp_viewport_destroy(seat->pointer.cursor_state.viewport);
-    }
-
-    if (seat->pointer.cursor_shape) {
-        wp_cursor_shape_device_v1_destroy(seat->pointer.cursor_shape);
-    }
-
     if (seat->pointer.wl_pointer) {
         if (wl_pointer_get_version(seat->pointer.wl_pointer) >= WL_POINTER_RELEASE_SINCE_VERSION) {
             wl_pointer_release(seat->pointer.wl_pointer);
@@ -3274,23 +3269,6 @@ void Wayland_DisplayCreateTextInputManager(SDL_VideoData *d, uint32_t id)
 }
 
 // Pen/Tablet support...
-
-typedef struct SDL_WaylandPenTool  // a stylus, etc, on a tablet.
-{
-    SDL_PenID instance_id;
-    SDL_PenInfo info;
-    SDL_Window *tool_focus;
-    struct zwp_tablet_tool_v2 *wltool;
-    float x;
-    float y;
-    bool frame_motion_set;
-    float frame_axes[SDL_PEN_AXIS_COUNT];
-    Uint32 frame_axes_set;
-    int frame_pen_down;
-    int frame_buttons[3];
-    struct wl_list link;
-} SDL_WaylandPenTool;
-
 static void tablet_tool_handle_type(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t type)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
@@ -3342,7 +3320,9 @@ static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *to
     if (sdltool->instance_id) {
         SDL_RemovePenDevice(0, sdltool->instance_id);
     }
-    zwp_tablet_tool_v2_destroy(tool);
+
+    Wayland_CursorStateRelease(&sdltool->cursor_state);
+    zwp_tablet_tool_v2_destroy(sdltool->wltool);
     WAYLAND_wl_list_remove(&sdltool->link);
     SDL_free(sdltool);
 }
@@ -3351,79 +3331,70 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
     SDL_WindowData *windowdata = surface ? Wayland_GetWindowDataForOwnedSurface(surface) : NULL;
-    sdltool->tool_focus = windowdata ? windowdata->sdlwindow : NULL;
+    sdltool->focus = windowdata;
+    sdltool->proximity_serial = serial;
+    sdltool->frame.have_proximity_in = true;
 
-    SDL_assert(sdltool->instance_id == 0);  // shouldn't be added at this point.
-    if (sdltool->info.subtype != SDL_PEN_TYPE_UNKNOWN) {   // don't tell SDL about it if we don't know its role.
-        sdltool->instance_id = SDL_AddPenDevice(0, NULL, &sdltool->info, sdltool);
-    }
-
-    // According to the docs, this should be followed by a motion event, where we'll send our SDL events.
+    // According to the docs, this should be followed by a frame event, where we'll send our SDL events.
 }
 
 static void tablet_tool_handle_proximity_out(void *data, struct zwp_tablet_tool_v2 *tool)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->tool_focus = NULL;
-
-    if (sdltool->instance_id) {
-        SDL_RemovePenDevice(0, sdltool->instance_id);
-        sdltool->instance_id = 0;
-    }
+    sdltool->frame.have_proximity_out = true;
 }
 
 static void tablet_tool_handle_down(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->frame_pen_down = 1;
+    sdltool->frame.tool_state = WAYLAND_TABLET_TOOL_STATE_DOWN;
 }
 
 static void tablet_tool_handle_up(void *data, struct zwp_tablet_tool_v2 *tool)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->frame_pen_down = 0;
+    sdltool->frame.tool_state = WAYLAND_TABLET_TOOL_STATE_UP;
 }
 
 static void tablet_tool_handle_motion(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t sx_w, wl_fixed_t sy_w)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    SDL_Window *window = sdltool->tool_focus;
-    if (window) {
-        const SDL_WindowData *windowdata = window->internal;
-        sdltool->x = (float)(wl_fixed_to_double(sx_w) * windowdata->pointer_scale.x);
-        sdltool->y = (float)(wl_fixed_to_double(sy_w) * windowdata->pointer_scale.y);
-        sdltool->frame_motion_set = true;
+    SDL_WindowData *windowdata = sdltool->focus;
+    if (windowdata) {
+        sdltool->frame.x = (float)(wl_fixed_to_double(sx_w) * windowdata->pointer_scale.x);
+        sdltool->frame.y = (float)(wl_fixed_to_double(sy_w) * windowdata->pointer_scale.y);
+        sdltool->frame.have_motion = true;
     }
 }
 
 static void tablet_tool_handle_pressure(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t pressure)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->frame_axes[SDL_PEN_AXIS_PRESSURE] = ((float) pressure) / 65535.0f;
-    sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_PRESSURE);
+    sdltool->frame.axes[SDL_PEN_AXIS_PRESSURE] = ((float) pressure) / 65535.0f;
+    sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_PRESSURE);
     if (pressure) {
-        sdltool->frame_axes[SDL_PEN_AXIS_DISTANCE] = 0.0f;
-        sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_DISTANCE);
+        sdltool->frame.axes[SDL_PEN_AXIS_DISTANCE] = 0.0f;
+        sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_DISTANCE);
     }
 }
 
 static void tablet_tool_handle_distance(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t distance)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->frame_axes[SDL_PEN_AXIS_DISTANCE] = ((float) distance) / 65535.0f;
-    sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_DISTANCE);
+    sdltool->frame.axes[SDL_PEN_AXIS_DISTANCE] = ((float) distance) / 65535.0f;
+    sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_DISTANCE);
     if (distance) {
-        sdltool->frame_axes[SDL_PEN_AXIS_PRESSURE] = 0.0f;
-        sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_PRESSURE);
+        sdltool->frame.axes[SDL_PEN_AXIS_PRESSURE] = 0.0f;
+        sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_PRESSURE);
     }
 }
 
 static void tablet_tool_handle_tilt(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t xtilt, wl_fixed_t ytilt)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->frame_axes[SDL_PEN_AXIS_XTILT] = (float)(wl_fixed_to_double(xtilt));
-    sdltool->frame_axes[SDL_PEN_AXIS_YTILT] = (float)(wl_fixed_to_double(ytilt));
-    sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_XTILT) | (1u << SDL_PEN_AXIS_YTILT);
+    sdltool->frame.axes[SDL_PEN_AXIS_XTILT] = (float)(wl_fixed_to_double(xtilt));
+    sdltool->frame.axes[SDL_PEN_AXIS_YTILT] = (float)(wl_fixed_to_double(ytilt));
+    sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_XTILT) | (1u << SDL_PEN_AXIS_YTILT);
 }
 
 static void tablet_tool_handle_button(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial, uint32_t button, uint32_t state)
@@ -3446,23 +3417,23 @@ static void tablet_tool_handle_button(void *data, struct zwp_tablet_tool_v2 *too
         return;  // don't care about this button, I guess.
     }
 
-    SDL_assert((sdlbutton >= 1) && (sdlbutton <= SDL_arraysize(sdltool->frame_buttons)));
-    sdltool->frame_buttons[sdlbutton-1] = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED) ? 1 : 0;
+    SDL_assert((sdlbutton >= 1) && (sdlbutton <= SDL_arraysize(sdltool->frame.buttons)));
+    sdltool->frame.buttons[sdlbutton-1] = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED) ? 1 : 0;
 }
 
 static void tablet_tool_handle_rotation(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t degrees)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
     const float rotation = (float)(wl_fixed_to_double(degrees));
-    sdltool->frame_axes[SDL_PEN_AXIS_ROTATION] = (rotation > 180.0f) ? (rotation - 360.0f) : rotation;  // map to -180.0f ... 179.0f range
-    sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_ROTATION);
+    sdltool->frame.axes[SDL_PEN_AXIS_ROTATION] = (rotation > 180.0f) ? (rotation - 360.0f) : rotation;  // map to -180.0f ... 179.0f range
+    sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_ROTATION);
 }
 
 static void tablet_tool_handle_slider(void *data, struct zwp_tablet_tool_v2 *tool, int32_t position)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
-    sdltool->frame_axes[SDL_PEN_AXIS_SLIDER] = position / 65535.f;
-    sdltool->frame_axes_set |= (1u << SDL_PEN_AXIS_SLIDER);
+    sdltool->frame.axes[SDL_PEN_AXIS_SLIDER] = position / 65535.f;
+    sdltool->frame.axes_set |= (1u << SDL_PEN_AXIS_SLIDER);
 }
 
 static void tablet_tool_handle_wheel(void *data, struct zwp_tablet_tool_v2 *tool, int32_t degrees, int32_t clicks)
@@ -3474,51 +3445,66 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
 
-    if (!sdltool->instance_id) {
-        return;  // Not a pen we report on.
+    const Uint64 timestamp = Wayland_AdjustEventTimestampBase(Wayland_EventTimestampMSToNS(time));
+    SDL_Window *window = sdltool->focus ? sdltool->focus->sdlwindow : NULL;
+
+    if (sdltool->frame.have_proximity_in) {
+        SDL_assert(sdltool->instance_id == 0);  // shouldn't be added at this point.
+        if (sdltool->info.subtype != SDL_PEN_TYPE_UNKNOWN) {   // don't tell SDL about it if we don't know its role.
+            sdltool->instance_id = SDL_AddPenDevice(timestamp, NULL, &sdltool->info, sdltool);
+            Wayland_TabletToolUpdateCursor(sdltool);
+        }
     }
 
-    const Uint64 timestamp = Wayland_AdjustEventTimestampBase(Wayland_EventTimestampMSToNS(time));
     const SDL_PenID instance_id = sdltool->instance_id;
-    SDL_Window *window = sdltool->tool_focus;
+
+    if (!instance_id) {
+        return;  // Not a pen we report on.
+    }
+
+    // !!! FIXME: Should hit testing be done if pens generate pointer motion?
 
     // I don't know if this is necessary (or makes sense), but send motion before pen downs, but after pen ups, so you don't get unexpected lines drawn.
-    if (sdltool->frame_motion_set && (sdltool->frame_pen_down != -1)) {
-        if (sdltool->frame_pen_down) {
-            SDL_SendPenMotion(timestamp, instance_id, window, sdltool->x, sdltool->y);
+    if (sdltool->frame.have_motion && sdltool->frame.tool_state) {
+        if (sdltool->frame.tool_state == WAYLAND_TABLET_TOOL_STATE_UP) {
+            SDL_SendPenMotion(timestamp, instance_id, window, sdltool->frame.x, sdltool->frame.y);
             SDL_SendPenTouch(timestamp, instance_id, window, false, true);  // !!! FIXME: how do we know what tip is in use?
         } else {
             SDL_SendPenTouch(timestamp, instance_id, window, false, false); // !!! FIXME: how do we know what tip is in use?
-            SDL_SendPenMotion(timestamp, instance_id, window, sdltool->x, sdltool->y);
+            SDL_SendPenMotion(timestamp, instance_id, window, sdltool->frame.x, sdltool->frame.y);
         }
     } else {
-        if (sdltool->frame_pen_down != -1) {
-            SDL_SendPenTouch(timestamp, instance_id, window, false, (sdltool->frame_pen_down != 0));  // !!! FIXME: how do we know what tip is in use?
+        if (sdltool->frame.tool_state) {
+            SDL_SendPenTouch(timestamp, instance_id, window, false, sdltool->frame.tool_state == WAYLAND_TABLET_TOOL_STATE_DOWN);  // !!! FIXME: how do we know what tip is in use?
         }
 
-        if (sdltool->frame_motion_set) {
-            SDL_SendPenMotion(timestamp, instance_id, window, sdltool->x, sdltool->y);
+        if (sdltool->frame.have_motion) {
+            SDL_SendPenMotion(timestamp, instance_id, window, sdltool->frame.x, sdltool->frame.y);
         }
     }
 
     for (SDL_PenAxis i = 0; i < SDL_PEN_AXIS_COUNT; i++) {
-        if (sdltool->frame_axes_set & (1u << i)) {
-            SDL_SendPenAxis(timestamp, instance_id, window, i, sdltool->frame_axes[i]);
+        if (sdltool->frame.axes_set & (1u << i)) {
+            SDL_SendPenAxis(timestamp, instance_id, window, i, sdltool->frame.axes[i]);
         }
     }
 
-    for (int i = 0; i < SDL_arraysize(sdltool->frame_buttons); i++) {
-        const int state = sdltool->frame_buttons[i];
-        if (state != -1) {
-            SDL_SendPenButton(timestamp, instance_id, window, (Uint8)(i + 1), (state != 0));
-            sdltool->frame_buttons[i] = -1;
+    for (int i = 0; i < SDL_arraysize(sdltool->frame.buttons); i++) {
+        const int state = sdltool->frame.buttons[i];
+        if (state) {
+            SDL_SendPenButton(timestamp, instance_id, window, (Uint8)(i + 1), state == WAYLAND_TABLET_TOOL_BUTTON_DOWN);
         }
     }
 
-    // reset for next frame.
-    sdltool->frame_pen_down = -1;
-    sdltool->frame_motion_set = false;
-    sdltool->frame_axes_set = 0;
+    if (sdltool->frame.have_proximity_out) {
+        sdltool->focus = NULL;
+        Wayland_TabletToolUpdateCursor(sdltool);
+        SDL_RemovePenDevice(timestamp, sdltool->instance_id);
+        sdltool->instance_id = 0;
+    }
+
+    // Reset for the next frame.
+    SDL_zero(sdltool->frame);
 }
 
 static const struct zwp_tablet_tool_v2_listener tablet_tool_listener = {
@@ -3558,10 +3544,11 @@ static void tablet_seat_handle_tool_added(void *data, struct zwp_tablet_seat_v2
         sdltool->wltool = tool;
         sdltool->info.max_tilt = -1.0f;
         sdltool->info.num_buttons = -1;
-        sdltool->frame_pen_down = -1;
-        for (int i = 0; i < SDL_arraysize(sdltool->frame_buttons); i++) {
-            sdltool->frame_buttons[i] = -1;
+
+        if (seat->display->cursor_shape_manager) {
+            sdltool->cursor_state.cursor_shape = wp_cursor_shape_manager_v1_get_tablet_tool_v2(seat->display->cursor_shape_manager, tool);
         }
+
         WAYLAND_wl_list_insert(&seat->tablet.tool_list, &sdltool->link);
 
         // this will send a bunch of zwp_tablet_tool_v2 events right up front to tell
@@ -3598,6 +3585,8 @@ void Wayland_DisplayInitTabletManager(SDL_VideoData *display)
 static void Wayland_remove_all_pens_callback(SDL_PenID instance_id, void *handle, void *userdata)
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) handle;
+
+    Wayland_CursorStateRelease(&sdltool->cursor_state);
     zwp_tablet_tool_v2_destroy(sdltool->wltool);
     SDL_free(sdltool);
 }
@@ -3605,10 +3594,10 @@ static void Wayland_remove_all_pens_callback(SDL_PenID instance_id, void *handle
 static void Wayland_SeatDestroyTablet(SDL_WaylandSeat *seat, bool send_events)
 {
     if (send_events) {
-        SDL_WaylandPenTool *pen, *temp;
-        wl_list_for_each_safe (pen, temp, &seat->tablet.tool_list, link) {
+        SDL_WaylandPenTool *tool, *temp;
+        wl_list_for_each_safe (tool, temp, &seat->tablet.tool_list, link) {
             // Remove all tools for this seat, sending PROXIMITY_OUT events.
-            tablet_tool_handle_removed(pen, pen->wltool);
+            tablet_tool_handle_removed(tool, tool->wltool);
         }
     } else {
         // Shutting down, just delete everything.
@@ -3675,8 +3664,13 @@ void Wayland_DisplayRemoveWindowReferencesFromSeats(SDL_VideoData *display, SDL_
 
         SDL_WaylandPenTool *tool;
         wl_list_for_each (tool, &seat->tablet.tool_list, link) {
-            if (tool->tool_focus == window->sdlwindow) {
-                tablet_tool_handle_proximity_out(tool, tool->wltool);
+            if (tool->focus == window) {
+                tool->focus = NULL;
+                Wayland_TabletToolUpdateCursor(tool);
+                if (tool->instance_id) {
+                    SDL_RemovePenDevice(0, tool->instance_id);
+                    tool->instance_id = 0;
+                }
             }
         }
     }
@@ -3811,7 +3805,7 @@ void Wayland_SeatUpdatePointerGrab(SDL_WaylandSeat *seat)
             seat->pointer.locked_pointer = NULL;
 
             // Update the cursor after destroying a relative move lock.
-            Wayland_SeatUpdateCursor(seat);
+            Wayland_SeatUpdatePointerCursor(seat);
         }
 
         if (seat->pointer.wl_pointer) {
@@ -3831,7 +3825,7 @@ void Wayland_SeatUpdatePointerGrab(SDL_WaylandSeat *seat)
                     zwp_locked_pointer_v1_add_listener(seat->pointer.locked_pointer, &locked_pointer_listener, seat);
 
                     // Ensure that the relative pointer is hidden, if required.
-                    Wayland_SeatUpdateCursor(seat);
+                    Wayland_SeatUpdatePointerCursor(seat);
                 }
 
                 // Locked the cursor for relative mode, nothing more to do.
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index a178d129f4dff..57d58368f22ff 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -56,6 +56,66 @@ typedef struct
     char text[8];
 } SDL_WaylandKeyboardRepeat;
 
+typedef struct SDL_WaylandCursorState
+{
+    SDL_CursorData *current_cursor;
+    struct wp_cursor_shape_device_v1 *cursor_shape;
+    struct wl_surface *surface;
+    struct wp_viewport *viewport;
+
+    double scale;
+
+    // Pointer to the internal data for system cursors.
+    void *system_cursor_handle;
+
+    // The cursor animation thread lock must be held when modifying this.
+    struct wl_callback *frame_callback;
+
+    Uint64 last_frame_callback_time_ms;
+    Uint32 current_frame_time_ms;
+    int current_frame;
+    SDL_HitTestResult hit_test_result;
+} SDL_WaylandCursorState;
+
+typedef struct SDL_WaylandPenTool  // a stylus, etc, on a tablet.
+{
+    SDL_PenID instance_id;
+    SDL_PenInfo info;
+    SDL_WindowData *focus;
+    struct zwp_tablet_tool_v2 *wltool;
+    Uint32 proximity_serial;
+
+    struct
+    {
+        float x;
+        float y;
+
+        float axes[SDL_PEN_AXIS_COUNT];
+        Uint32 axes_set;
+
+        enum
+        {
+            WAYLAND_TABLET_TOOL_BUTTON_NONE = 0,
+            WAYLAND_TABLET_TOOL_BUTTON_DOWN,
+            WAYLAND_TABLET_TOOL_BUTTON_UP
+        } buttons[3];
+
+        enum
+        {
+            WAYLAND_TABLET_TOOL_STATE_NONE = 0,
+            WAYLAND_TABLET_TOOL_STATE_DOWN,
+            WAYLAND_TABLET_TOOL_STATE_UP
+        } tool_state;
+
+        bool have_motion;
+        bool have_proximity_in;
+        bool have_proximity_out;
+    } frame;
+
+    SDL_WaylandCursorState cursor_state;
+    struct wl_list link;
+} SDL_WaylandPenTool;
+
 typedef struct SDL_WaylandSeat
 {
     SDL_VideoData *display;
@@ -120,7 +180,6 @@ typedef struct SDL_WaylandSeat
         struct wl_pointer *wl_pointer;
         struct zwp_relative_pointer_v1 *relative_pointer;
         struct zwp_input_timestamps_v1 *timestamps;
-        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;
@@ -176,22 +235,7 @@ typedef struct SDL_WaylandSeat
             Uint64 timestamp_ns;
         } pending_frame;
 
-        // Cursor state
-        struct
-        {
-            struct wl_surface *surface;
-            struct wp_viewport *viewport;
-
-            // Animation state for cursors
-            void *cursor_handle;
-
-            // The cursor animation thread lock must be held when modifying this.
-            struct wl_callback *frame_callback;
-
-            Uint64 last_frame_callback_time_ms;
-            Uint32 current_frame_time_ms;
-            int current_frame;
-        } cursor_state;
+        SDL_WaylandCursorState cursor_state;
     } pointer;
 
     struct
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index bae09f9ff6035..348e92295cc6e 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -42,6 +42,7 @@
 #include "pointer-constraints-unstable-v1-client-protocol.h"
 #include "viewporter-client-protocol.h"
 #include "pointer-warp-v1-client-protocol.h"
+#include "tablet-v2-client-protocol.h"
 
 #include "../../SDL_hints_c.h"
 
@@ -296,48 +297,40 @@ static void Wayland_DBusFinishCursorProperties(void)
 
 #endif
 
-static CustomCursorImage *Wayland_GetScaledCustomCursorImage(SDL_CursorData *data, int frame_index, double scale)
+static struct wl_buffer *Wayland_CursorStateGetFrame(SDL_WaylandCursorState *state, int frame_index)
 {
-    const int offset = data->cursor_data.custom.images_per_frame * frame_index;
-
-    /* Find the closest image. Images that are larger than the
-     * desired size are preferred over images that are smaller.
-     */
-    CustomCursorImage *closest = NULL;
-    int desired_w = (int)SDL_round(data->cursor_data.custom.width * scale);
-    int desired_h = (int)SDL_round(data->cursor_data.custom.height * scale);
-    int desired_size = desired_w * desired_h;
-    int closest_distance = -1;
-    int closest_size = -1;
-    for (int i = 0; i < data->cursor_data.custom.images_per_frame && closest_distance && data->cursor_data.custom.images[offset + i].buffer; ++i) {
-        CustomCursorImage *candidate = &data->cursor_data.custom.images[offset + i];
-        int size = candidate->width * candidate->height;
-        int delta_w = candidate->width - desired_w;
-        int delta_h = candidate->height - desired_h;
-        int distance = (delta_w * delta_w) + (delta_h * delta_h);
-        if (closest_distance < 0 || distance < closest_distance ||
-            (size > desired_size && closest_size < desired_size)) {
-            closest = candidate;
-            closest_distance = distance;
-            closest_size = size;
-            }
-    }
-
-    return closest;
-}
-
-static struct wl_buffer *Wayland_SeatGetCursorFrame(SDL_WaylandSeat *seat, int frame_index)
-{
-    SDL_CursorData *data = seat->pointer.current_cursor;
+    SDL_CursorData *data = state->current_cursor;
 
     if (data) {
         if (!data->is_system_cursor) {
-            const double scale = seat->pointer.focus ? seat->pointer.focus->scale_factor : 1.0;
-            CustomCursorImage *image = Wayland_GetScaledCustomCursorImage(data, frame_index, scale);
+            const int offset = data->cursor_data.custom.images_per_frame * frame_index;
+
+            /* Find the closest image. Images that are larger than the
+             * desired size are preferred over images that are smaller.
+             */
+            CustomCursorImage *closest = NULL;
+            int desired_w = (int)SDL_round(data->cursor_data.custom.width * state->scale);
+            int desired_h = (int)SDL_round(data->cursor_data.custom.height * state->scale);
+            int desired_size = desired_w * desired_h;
+            int closest_distance = -1;
+            int closest_size = -1;
+            for (int i = 0; i < data->cursor_data.custom.images_per_frame && closest_distance && data->cursor_data.custom.images[offset + i].buffer; ++i) {
+                CustomCursorImage *candidate = &data->cursor_data.custom.images[offset + i];
+                int size = candidate->width * candidate->height;
+                int delta_w = candidate->width - desired_w;
+                int delta_h = candidate->height - desired_h;
+                int distance = (delta_w * delta_w) + (delta_h * delta_h);
+                if (closest_distance < 0 || distance < closest_distance ||
+                    (size > desired_size && closest_size < desired_size)) {
+                    closest = candidate;
+                    closest_distance = distance;
+                    closest_size = size;
+                }
+            }
 
-            return image ? image->buffer : NULL;
+            return closest ? closest->buffer : NULL;
         } else {
-            return ((Wayland_CachedSystemCursor *)(seat->pointer.cursor_state.cursor_handle))->buffers[frame_index];
+            return ((Wayland_CachedSystemCursor *)(state->system_cursor_handle))->buffers[frame_index];
         }
     }
 
@@ -509,23 +502,23 @@ static const struct wl_callback_listener cursor_frame_listener = {
 
 static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
 {
-    SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
-    if (!seat->pointer.current_cursor) {
+    SDL_WaylandCursorState *state = (SDL_WaylandCursorState *)data;
+    if (!state->current_cursor) {
         return;
     }
 
-    Uint32 *frames = seat->pointer.current_cursor->frame_durations_ms;
-    SDL_CursorData *c = seat->pointer.current_cursor;
+    Uint32 *frames = state->current_cursor->frame_durations_ms;
+    SDL_CursorData *c = state->current_cursor;
 
     const Uint64 now = SDL_GetTicks();
-    const Uint32 elapsed = (now - seat->pointer.cursor_state.last_frame_callback_time_ms) % c->total_duration_ms;
+    const Uint32 elapsed = (now - state->last_frame_callback_time_ms) % c->total_duration_ms;
     Uint32 advance = 0;
-    int next = seat->pointer.cursor_state.current_frame;
+    int next = state->current_frame;
 
-    seat->pointer.cursor_state.current_frame_time_ms += elapsed;
+    state->current_frame_time_ms += elapsed;
 
     // Calculate the next frame based on the elapsed duration.
-    for (Uint32 t = frames[next]; t <= seat->pointer.cursor_state.current_frame_time_ms; t += frames[next]) {
+    for (Uint32 t = frames[next]; t <= state->current_frame_time_ms; t += frames[next]) {
         next = (next + 1) % c->num_frames;
         advance = t;
 
@@ -536,52 +529,52 @@ static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
     }
 
     wl_callback_destroy(cb);
-    seat->pointer.cursor_state.frame_callback = NULL;
+    state->frame_callback = NULL;
 
     // Don't queue another callback if this frame time is infinite.
     if (frames[next]) {
-        seat->pointer.cursor_state.frame_callback = wl_surface_frame(seat->pointer.cursor_state.surface);
-        wl_callback_add_listener(seat->pointer.cursor_state.frame_callback, &cursor_frame_listener, data);
+        state->frame_callback = wl_surface_frame(state->surface);
+        wl_callback_add_listener(state->frame_callback, &cursor_frame_listener, data);
     }
 
-    seat->pointer.cursor_state.current_frame_time_ms -= advance;
-    seat->pointer.cursor_state.last_frame_callback_time_ms = now;
-    seat->pointer.cursor_state.current_frame = next;
+    state->current_frame_time_ms -= advance;
+    state->last_frame_callback_time_ms = now;
+    state->current_frame = next;
 
-    struct wl_buffer *buffer = Wayland_SeatGetCursorFrame(seat, next);
-    wl_surface_attach(seat->pointer.cursor_state.surface, buffer, 0, 0);
+    struct wl_buffer *buffer = Wayland_CursorStateGetFrame(state, next);
+    wl_surface_attach(state->surface, buffer, 0, 0);
 
-    if (wl_surface_get_version(seat->pointer.cursor_state.surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
-        wl_surface_damage_buffer(seat->pointer.cursor_state.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
+    if (wl_surface_get_version(state->surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
+        wl_surface_damage_buffer(state->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
     } else {
-        wl_surface_damage(seat->pointer.cursor_state.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
+        wl_surface_damage(state->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
     }
-    wl_surface_commit(seat->pointer.cursor_state.surface);
+    wl_surface_commit(state->surface);
 }
 
-void Wayland_SeatSetCursorFrameCallback(SDL_WaylandSeat *seat)
+void Wayland_CursorStateSetFrameCallback(SDL_WaylandCursorState *state, void *userdata)
 {
     if (cursor_thread_context.lock) {
         SDL_LockMutex(cursor_thread_context.lock);
     }
 
-    seat->pointer.cursor_state.frame_callback = wl_surface_frame(seat->pointer.cursor_state.surface);
-    wl_callback_add_listener(seat->pointer.cursor_state.frame_callback, &cursor_frame_listener, seat);
+    state->frame_callback = wl_surface_frame(state->surface);
+    wl_callback_add_listener(state->frame_callback, &cursor_frame_listener, userdata);
 
  

(Patch may be truncated, please check the link at the top of this post.)