SDL: wayland: Add cursor-shape-v1 protocol support (525c3)

From 525c317ed958558abdedc899df6beedb8020b87c Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Thu, 14 Mar 2024 09:49:40 -0400
Subject: [PATCH] wayland: Add cursor-shape-v1 protocol support

This is the preferred method of handling the cursor shape on KDE 6 and likely other window managers going forward.

Manual backport of c2e9693
---
 src/video/wayland/SDL_waylandevents.c   |  21 ++++
 src/video/wayland/SDL_waylandevents_c.h |   3 +
 src/video/wayland/SDL_waylandmouse.c    |  62 +++++++++-
 src/video/wayland/SDL_waylandvideo.c    |  11 ++
 src/video/wayland/SDL_waylandvideo.h    |   1 +
 wayland-protocols/cursor-shape-v1.xml   | 147 ++++++++++++++++++++++++
 6 files changed, 242 insertions(+), 3 deletions(-)
 create mode 100644 wayland-protocols/cursor-shape-v1.xml

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index a5105a2fc6271..6bbf8cf3e8470 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -64,6 +64,7 @@
 #include <xkbcommon/xkbcommon-compose.h>
 #include "../../events/imKStoUCS.h"
 #include "../../events/SDL_keysym_to_scancode_c.h"
+#include "cursor-shape-v1-client-protocol.h"
 
 /* Clamp the wl_seat version on older versions of libwayland. */
 #if SDL_WAYLAND_CHECK_VERSION(1, 21, 0)
@@ -178,6 +179,17 @@ static SDL_bool Wayland_SurfaceHasActiveTouches(struct wl_surface *surface)
     return SDL_FALSE;
 }
 
+void Wayland_CreateCursorShapeDevice(struct SDL_WaylandInput *input)
+{
+    SDL_VideoData *viddata = input->display;
+
+    if (viddata->cursor_shape_manager) {
+        if (input->pointer && !input->cursor_shape) {
+            input->cursor_shape = wp_cursor_shape_manager_v1_get_pointer(viddata->cursor_shape_manager, input->pointer);
+        }
+    }
+}
+
 /* Returns SDL_TRUE if a key repeat event was due */
 static SDL_bool keyboard_repeat_handle(SDL_WaylandKeyboardRepeat *repeat_info, uint32_t elapsed)
 {
@@ -1394,10 +1406,15 @@ static void seat_handle_capabilities(void *data, struct wl_seat *seat,
         input->pointer = wl_seat_get_pointer(seat);
         SDL_memset(&input->pointer_curr_axis_info, 0, sizeof(input->pointer_curr_axis_info));
         input->display->pointer = input->pointer;
+        Wayland_CreateCursorShapeDevice(input);
         wl_pointer_set_user_data(input->pointer, input);
         wl_pointer_add_listener(input->pointer, &pointer_listener,
                                 input);
     } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && input->pointer) {
+        if (input->cursor_shape) {
+            wp_cursor_shape_device_v1_destroy(input->cursor_shape);
+            input->cursor_shape = NULL;
+        }
         wl_pointer_destroy(input->pointer);
         input->pointer = NULL;
         input->display->pointer = NULL;
@@ -2532,6 +2549,10 @@ void Wayland_display_destroy_input(SDL_VideoData *d)
         wl_keyboard_destroy(input->keyboard);
     }
 
+    if (input->cursor_shape) {
+        wp_cursor_shape_device_v1_destroy(input->cursor_shape);
+    }
+
     if (input->pointer) {
         wl_pointer_destroy(input->pointer);
     }
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index d69888c8227b6..f5a5e400427ce 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -92,6 +92,7 @@ struct SDL_WaylandInput
     SDL_WaylandDataDevice *data_device;
     SDL_WaylandPrimarySelectionDevice *primary_selection_device;
     SDL_WaylandTextInput *text_input;
+    struct wp_cursor_shape_device_v1 *cursor_shape;
     struct zwp_relative_pointer_v1 *relative_pointer;
     SDL_WindowData *pointer_focus;
     SDL_WindowData *keyboard_focus;
@@ -175,6 +176,8 @@ extern int Wayland_input_ungrab_keyboard(SDL_Window *window);
 extern void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_WaylandTabletManager *tablet_manager);
 extern void Wayland_input_destroy_tablet(struct SDL_WaylandInput *input);
 
+extern void Wayland_CreateCursorShapeDevice(struct SDL_WaylandInput *input);
+
 #endif /* SDL_waylandevents_h_ */
 
 /* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index 4b7264588dd0b..4913ac8ce80f4 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -41,6 +41,8 @@
 #include "wayland-cursor.h"
 #include "SDL_waylandmouse.h"
 
+#include "cursor-shape-v1-client-protocol.h"
+
 #include "SDL_hints.h"
 #include "../../SDL_hints_c.h"
 
@@ -481,8 +483,10 @@ static SDL_Cursor *Wayland_CreateSystemCursor(SDL_SystemCursor id)
         }
         cursor->driverdata = (void *)cdata;
 
-        cdata->surface = wl_compositor_create_surface(data->compositor);
-        wl_surface_set_user_data(cdata->surface, NULL);
+        if (!data->cursor_shape_manager) {
+            cdata->surface = wl_compositor_create_surface(data->compositor);
+            wl_surface_set_user_data(cdata->surface, NULL);
+        }
 
         /* Note that we can't actually set any other cursor properties, as this
          * is output-specific. See wayland_get_system_cursor for the rest!
@@ -533,6 +537,55 @@ static void Wayland_FreeCursor(SDL_Cursor *cursor)
     SDL_free(cursor);
 }
 
+static void Wayland_SetSystemCursorShape(struct SDL_WaylandInput *input, SDL_SystemCursor id)
+{
+    Uint32 shape;
+
+    switch (id) {
+    case SDL_SYSTEM_CURSOR_ARROW:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
+        break;
+    case SDL_SYSTEM_CURSOR_IBEAM:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT;
+        break;
+    case SDL_SYSTEM_CURSOR_WAIT:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT;
+        break;
+    case SDL_SYSTEM_CURSOR_CROSSHAIR:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
+        break;
+    case SDL_SYSTEM_CURSOR_WAITARROW:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS;
+        break;
+    case SDL_SYSTEM_CURSOR_SIZENWSE:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE;
+        break;
+    case SDL_SYSTEM_CURSOR_SIZENESW:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE;
+        break;
+    case SDL_SYSTEM_CURSOR_SIZEWE:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE;
+        break;
+    case SDL_SYSTEM_CURSOR_SIZENS:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE;
+        break;
+    case SDL_SYSTEM_CURSOR_SIZEALL:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL;
+        break;
+    case SDL_SYSTEM_CURSOR_NO:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED;
+        break;
+    case SDL_SYSTEM_CURSOR_HAND:
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER;
+        break;
+    default:
+        SDL_assert(0); /* Should never be here... */
+        shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
+    }
+
+    wp_cursor_shape_device_v1_set_shape(input->cursor_shape, input->pointer_enter_serial, shape);
+}
+
 static int Wayland_ShowCursor(SDL_Cursor *cursor)
 {
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
@@ -550,7 +603,10 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
 
         /* TODO: High-DPI custom cursors? -flibit */
         if (!data->shm_data) {
-            if (!wayland_get_system_cursor(d, data, &scale)) {
+            if (input->cursor_shape) {
+                Wayland_SetSystemCursorShape(input, data->system_cursor);
+                return 0;
+            } else if (!wayland_get_system_cursor(d, data, &scale)) {
                 return -1;
             }
         }
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index e999c7ba12e1f..1c4155b620471 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -58,6 +58,7 @@
 #include "viewporter-client-protocol.h"
 #include "primary-selection-unstable-v1-client-protocol.h"
 #include "fractional-scale-v1-client-protocol.h"
+#include "cursor-shape-v1-client-protocol.h"
 
 #ifdef HAVE_LIBDECOR_H
 #include <libdecor.h>
@@ -875,6 +876,11 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint
         d->viewporter = wl_registry_bind(d->registry, id, &wp_viewporter_interface, 1);
     } else if (SDL_strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
         d->fractional_scale_manager = wl_registry_bind(d->registry, id, &wp_fractional_scale_manager_v1_interface, 1);
+    } else if (SDL_strcmp(interface, "wp_cursor_shape_manager_v1") == 0) {
+        d->cursor_shape_manager = wl_registry_bind(d->registry, id, &wp_cursor_shape_manager_v1_interface, 1);
+        if (d->input) {
+            Wayland_CreateCursorShapeDevice(d->input);
+        }
 #ifdef SDL_VIDEO_DRIVER_WAYLAND_QT_TOUCH
     } else if (SDL_strcmp(interface, "qt_touch_extension") == 0) {
         Wayland_touch_create(d, id);
@@ -1121,6 +1127,11 @@ static void Wayland_VideoCleanup(_THIS)
         data->fractional_scale_manager = NULL;
     }
 
+    if (data->cursor_shape_manager) {
+        wp_cursor_shape_manager_v1_destroy(data->cursor_shape_manager);
+        data->cursor_shape_manager = NULL;
+    }
+
     if (data->compositor) {
         wl_compositor_destroy(data->compositor);
         data->compositor = NULL;
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index b7b3348c6d4d9..f4ec5922ad2b9 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -70,6 +70,7 @@ typedef struct
     } shell;
     struct zwp_relative_pointer_manager_v1 *relative_pointer_manager;
     struct zwp_pointer_constraints_v1 *pointer_constraints;
+    struct wp_cursor_shape_manager_v1 *cursor_shape_manager;
     struct wl_data_device_manager *data_device_manager;
     struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager;
     struct zxdg_decoration_manager_v1 *decoration_manager;
diff --git a/wayland-protocols/cursor-shape-v1.xml b/wayland-protocols/cursor-shape-v1.xml
new file mode 100644
index 0000000000000..56f6a1a65bf9f
--- /dev/null
+++ b/wayland-protocols/cursor-shape-v1.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="cursor_shape_v1">
+  <copyright>
+    Copyright 2018 The Chromium Authors
+    Copyright 2023 Simon Ser
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+    The above copyright notice and this permission notice (including the next
+    paragraph) shall be included in all copies or substantial portions of the
+    Software.
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+  </copyright>
+
+  <interface name="wp_cursor_shape_manager_v1" version="1">
+    <description summary="cursor shape manager">
+      This global offers an alternative, optional way to set cursor images. This
+      new way uses enumerated cursors instead of a wl_surface like
+      wl_pointer.set_cursor does.
+
+      Warning! The protocol described in this file is currently in the testing
+      phase. Backward compatible changes may be added together with the
+      corresponding interface version bump. Backward incompatible changes can
+      only be done by creating a new major version of the extension.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the manager">
+        Destroy the cursor shape manager.
+      </description>
+    </request>
+
+    <request name="get_pointer">
+      <description summary="manage the cursor shape of a pointer device">
+        Obtain a wp_cursor_shape_device_v1 for a wl_pointer object.
+      </description>
+      <arg name="cursor_shape_device" type="new_id" interface="wp_cursor_shape_device_v1"/>
+      <arg name="pointer" type="object" interface="wl_pointer"/>
+    </request>
+
+    <request name="get_tablet_tool_v2">
+      <description summary="manage the cursor shape of a tablet tool device">
+        Obtain a wp_cursor_shape_device_v1 for a zwp_tablet_tool_v2 object.
+      </description>
+      <arg name="cursor_shape_device" type="new_id" interface="wp_cursor_shape_device_v1"/>
+      <arg name="tablet_tool" type="object" interface="zwp_tablet_tool_v2"/>
+    </request>
+  </interface>
+
+  <interface name="wp_cursor_shape_device_v1" version="1">
+    <description summary="cursor shape for a device">
+      This interface advertises the list of supported cursor shapes for a
+      device, and allows clients to set the cursor shape.
+    </description>
+
+    <enum name="shape">
+      <description summary="cursor shapes">
+        This enum describes cursor shapes.
+
+        The names are taken from the CSS W3C specification:
+        https://w3c.github.io/csswg-drafts/css-ui/#cursor
+      </description>
+      <entry name="default" value="1" summary="default cursor"/>
+      <entry name="context_menu" value="2" summary="a context menu is available for the object under the cursor"/>
+      <entry name="help" value="3" summary="help is available for the object under the cursor"/>
+      <entry name="pointer" value="4" summary="pointer that indicates a link or another interactive element"/>
+      <entry name="progress" value="5" summary="progress indicator"/>
+      <entry name="wait" value="6" summary="program is busy, user should wait"/>
+      <entry name="cell" value="7" summary="a cell or set of cells may be selected"/>
+      <entry name="crosshair" value="8" summary="simple crosshair"/>
+      <entry name="text" value="9" summary="text may be selected"/>
+      <entry name="vertical_text" value="10" summary="vertical text may be selected"/>
+      <entry name="alias" value="11" summary="drag-and-drop: alias of/shortcut to something is to be created"/>
+      <entry name="copy" value="12" summary="drag-and-drop: something is to be copied"/>
+      <entry name="move" value="13" summary="drag-and-drop: something is to be moved"/>
+      <entry name="no_drop" value="14" summary="drag-and-drop: the dragged item cannot be dropped at the current cursor location"/>
+      <entry name="not_allowed" value="15" summary="drag-and-drop: the requested action will not be carried out"/>
+      <entry name="grab" value="16" summary="drag-and-drop: something can be grabbed"/>
+      <entry name="grabbing" value="17" summary="drag-and-drop: something is being grabbed"/>
+      <entry name="e_resize" value="18" summary="resizing: the east border is to be moved"/>
+      <entry name="n_resize" value="19" summary="resizing: the north border is to be moved"/>
+      <entry name="ne_resize" value="20" summary="resizing: the north-east corner is to be moved"/>
+      <entry name="nw_resize" value="21" summary="resizing: the north-west corner is to be moved"/>
+      <entry name="s_resize" value="22" summary="resizing: the south border is to be moved"/>
+      <entry name="se_resize" value="23" summary="resizing: the south-east corner is to be moved"/>
+      <entry name="sw_resize" value="24" summary="resizing: the south-west corner is to be moved"/>
+      <entry name="w_resize" value="25" summary="resizing: the west border is to be moved"/>
+      <entry name="ew_resize" value="26" summary="resizing: the east and west borders are to be moved"/>
+      <entry name="ns_resize" value="27" summary="resizing: the north and south borders are to be moved"/>
+      <entry name="nesw_resize" value="28" summary="resizing: the north-east and south-west corners are to be moved"/>
+      <entry name="nwse_resize" value="29" summary="resizing: the north-west and south-east corners are to be moved"/>
+      <entry name="col_resize" value="30" summary="resizing: that the item/column can be resized horizontally"/>
+      <entry name="row_resize" value="31" summary="resizing: that the item/row can be resized vertically"/>
+      <entry name="all_scroll" value="32" summary="something can be scrolled in any direction"/>
+      <entry name="zoom_in" value="33" summary="something can be zoomed in"/>
+      <entry name="zoom_out" value="34" summary="something can be zoomed out"/>
+    </enum>
+
+    <enum name="error">
+      <entry name="invalid_shape" value="1"
+        summary="the specified shape value is invalid"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the cursor shape device">
+        Destroy the cursor shape device.
+
+        The device cursor shape remains unchanged.
+      </description>
+    </request>
+
+    <request name="set_shape">
+      <description summary="set device cursor to the shape">
+        Sets the device cursor to the specified shape. The compositor will
+        change the cursor image based on the specified shape.
+
+        The cursor actually changes only if the input device focus is one of
+        the requesting client's surfaces. If any, the previous cursor image
+        (surface or shape) is replaced.
+
+        The "shape" argument must be a valid enum entry, otherwise the
+        invalid_shape protocol error is raised.
+
+        This is similar to the wl_pointer.set_cursor and
+        zwp_tablet_tool_v2.set_cursor requests, but this request accepts a
+        shape instead of contents in the form of a surface. Clients can mix
+        set_cursor and set_shape requests.
+
+        The serial parameter must match the latest wl_pointer.enter or
+        zwp_tablet_tool_v2.proximity_in serial number sent to the client.
+        Otherwise the request will be ignored.
+      </description>
+      <arg name="serial" type="uint" summary="serial number of the enter event"/>
+      <arg name="shape" type="uint" enum="shape"/>
+    </request>
+  </interface>
+</protocol>