SDL: wayland: Add support for high-DPI cursors

From 8e54698aa6e3c0cf02d0b628403f047f28ea7fd0 Mon Sep 17 00:00:00 2001
From: Ethan Lee <[EMAIL REDACTED]>
Date: Wed, 22 Sep 2021 13:26:44 -0400
Subject: [PATCH] wayland: Add support for high-DPI cursors

---
 src/video/wayland/SDL_waylandmouse.c | 230 ++++++++++++++++-----------
 src/video/wayland/SDL_waylandmouse.h |   2 +-
 src/video/wayland/SDL_waylandvideo.c |   6 +-
 src/video/wayland/SDL_waylandvideo.h |   8 +-
 4 files changed, 147 insertions(+), 99 deletions(-)

diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index a0df26bdf0..23663ebe17 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -47,11 +47,110 @@ typedef struct {
     int                hot_x, hot_y;
     int                w, h;
 
-    /* Either a preloaded cursor, or one we created ourselves */
-    struct wl_cursor   *cursor;
+    /* shm_data is non-NULL for custom cursors.
+     * When shm_data is NULL, system_cursor must be valid
+     */
+    SDL_SystemCursor    system_cursor;
     void               *shm_data;
 } Wayland_CursorData;
 
+static SDL_bool
+wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorData *cdata, float *scale)
+{
+    struct wl_cursor_theme *theme = NULL;
+    struct wl_cursor *cursor;
+
+    SDL_Window *focus;
+    SDL_WindowData *focusdata;
+    int i;
+
+    /* FIXME: We need to be able to query the cursor size from the desktop at
+     * some point! For a while this was 32, but when testing on real desktops it
+     * seems like most of them default to 24. We'll need a protocol to get this
+     * for real, but for now this is a pretty safe bet.
+     * -flibit
+     */
+    int size = 24;
+
+    /* First, find the appropriate theme based on the current scale... */
+    focus = SDL_GetMouse()->focus;
+    if (focus == NULL) {
+        /* Nothing to see here, bail. */
+        return SDL_FALSE;
+    }
+    focusdata = focus->driverdata;
+    *scale = focusdata->scale_factor;
+    size *= focusdata->scale_factor;
+    for (i = 0; i < vdata->num_cursor_themes; i += 1) {
+        if (vdata->cursor_themes[i].size == size) {
+            theme = vdata->cursor_themes[i].theme;
+            break;
+        }
+    }
+    if (theme == NULL) {
+        vdata->cursor_themes = SDL_realloc(vdata->cursor_themes,
+                                           sizeof(SDL_WaylandCursorTheme) * (vdata->num_cursor_themes + 1));
+        if (vdata->cursor_themes == NULL) {
+            SDL_OutOfMemory();
+            return SDL_FALSE;
+        }
+        theme = WAYLAND_wl_cursor_theme_load(NULL, size, vdata->shm);
+        vdata->cursor_themes[vdata->num_cursor_themes++].theme = theme;
+    }
+
+    /* Next, find the cursor from the theme... */
+    switch(cdata->system_cursor)
+    {
+    case SDL_SYSTEM_CURSOR_ARROW:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "left_ptr");
+        break;
+    case SDL_SYSTEM_CURSOR_IBEAM:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "xterm");
+        break;
+    case SDL_SYSTEM_CURSOR_WAIT:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "watch");
+        break;
+    case SDL_SYSTEM_CURSOR_CROSSHAIR:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    case SDL_SYSTEM_CURSOR_WAITARROW:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "watch");
+        break;
+    case SDL_SYSTEM_CURSOR_SIZENWSE:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    case SDL_SYSTEM_CURSOR_SIZENESW:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    case SDL_SYSTEM_CURSOR_SIZEWE:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    case SDL_SYSTEM_CURSOR_SIZENS:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    case SDL_SYSTEM_CURSOR_SIZEALL:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    case SDL_SYSTEM_CURSOR_NO:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "xterm");
+        break;
+    case SDL_SYSTEM_CURSOR_HAND:
+        cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "hand1");
+        break;
+    default:
+        SDL_assert(0);
+        return SDL_FALSE;
+    }
+
+    /* ... Set the cursor data, finally. */
+    cdata->buffer = WAYLAND_wl_cursor_image_get_buffer(cursor->images[0]);
+    cdata->hot_x = cursor->images[0]->hotspot_x;
+    cdata->hot_y = cursor->images[0]->hotspot_y;
+    cdata->w = cursor->images[0]->width;
+    cdata->h = cursor->images[0]->height;
+    return SDL_TRUE;
+}
+
 static int
 wayland_create_tmp_file(off_t size)
 {
@@ -192,30 +291,30 @@ Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
 }
 
 static SDL_Cursor *
-CreateCursorFromWlCursor(SDL_VideoData *d, struct wl_cursor *wlcursor)
+Wayland_CreateSystemCursor(SDL_SystemCursor id)
 {
+    SDL_VideoData *data = SDL_GetVideoDevice()->driverdata;
     SDL_Cursor *cursor;
 
     cursor = SDL_calloc(1, sizeof (*cursor));
     if (cursor) {
-        Wayland_CursorData *data = SDL_calloc (1, sizeof (Wayland_CursorData));
-        if (!data) {
+        Wayland_CursorData *cdata = SDL_calloc (1, sizeof (Wayland_CursorData));
+        if (!cdata) {
             SDL_OutOfMemory();
             SDL_free(cursor);
             return NULL;
         }
-        cursor->driverdata = (void *) data;
+        cursor->driverdata = (void *) cdata;
 
-        data->buffer = WAYLAND_wl_cursor_image_get_buffer(wlcursor->images[0]);
-        data->surface = wl_compositor_create_surface(d->compositor);
-        wl_surface_set_user_data(data->surface, NULL);
-        data->hot_x = wlcursor->images[0]->hotspot_x;
-        data->hot_y = wlcursor->images[0]->hotspot_y;
-        data->w = wlcursor->images[0]->width;
-        data->h = wlcursor->images[0]->height;
-        data->cursor= wlcursor;
+        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!
+         */
+        cdata->system_cursor = id;
     } else {
-        SDL_OutOfMemory ();
+        SDL_OutOfMemory();
     }
 
     return cursor;
@@ -224,66 +323,7 @@ CreateCursorFromWlCursor(SDL_VideoData *d, struct wl_cursor *wlcursor)
 static SDL_Cursor *
 Wayland_CreateDefaultCursor()
 {
-    SDL_VideoDevice *device = SDL_GetVideoDevice();
-    SDL_VideoData *data = device->driverdata;
-
-    return CreateCursorFromWlCursor (data,
-                                     WAYLAND_wl_cursor_theme_get_cursor(data->cursor_theme,
-                                                                "left_ptr"));
-}
-
-static SDL_Cursor *
-Wayland_CreateSystemCursor(SDL_SystemCursor id)
-{
-    SDL_VideoDevice *vd = SDL_GetVideoDevice();
-    SDL_VideoData *d = vd->driverdata;
-
-    struct wl_cursor *cursor = NULL;
-
-    switch(id)
-    {
-    default:
-        SDL_assert(0);
-        return NULL;
-    case SDL_SYSTEM_CURSOR_ARROW:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "left_ptr");
-        break;
-    case SDL_SYSTEM_CURSOR_IBEAM:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "xterm");
-        break;
-    case SDL_SYSTEM_CURSOR_WAIT:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "watch");
-        break;
-    case SDL_SYSTEM_CURSOR_CROSSHAIR:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    case SDL_SYSTEM_CURSOR_WAITARROW:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "watch");
-        break;
-    case SDL_SYSTEM_CURSOR_SIZENWSE:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    case SDL_SYSTEM_CURSOR_SIZENESW:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    case SDL_SYSTEM_CURSOR_SIZEWE:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    case SDL_SYSTEM_CURSOR_SIZENS:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    case SDL_SYSTEM_CURSOR_SIZEALL:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    case SDL_SYSTEM_CURSOR_NO:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "xterm");
-        break;
-    case SDL_SYSTEM_CURSOR_HAND:
-        cursor = WAYLAND_wl_cursor_theme_get_cursor(d->cursor_theme, "hand1");
-        break;
-    }
-
-    return CreateCursorFromWlCursor(d, cursor);
+    return Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
 }
 
 static void
@@ -300,14 +340,14 @@ Wayland_FreeCursor(SDL_Cursor *cursor)
     if (!d)
         return;
 
-    if (d->buffer && !d->cursor)
+    if (d->buffer && d->shm_data)
         wl_buffer_destroy(d->buffer);
 
     if (d->surface)
         wl_surface_destroy(d->surface);
 
     /* Not sure what's meant to happen to shm_data */
-    SDL_free (cursor->driverdata);
+    SDL_free(cursor->driverdata);
     SDL_free(cursor);
 }
 
@@ -317,8 +357,8 @@ Wayland_ShowCursor(SDL_Cursor *cursor)
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
     SDL_VideoData *d = vd->driverdata;
     struct SDL_WaylandInput *input = d->input;
-
     struct wl_pointer *pointer = d->pointer;
+    float scale = 1.0f;
 
     if (!pointer)
         return -1;
@@ -327,22 +367,26 @@ Wayland_ShowCursor(SDL_Cursor *cursor)
     {
         Wayland_CursorData *data = cursor->driverdata;
 
-        wl_pointer_set_cursor (pointer,
-                               input->pointer_enter_serial,
-                               data->surface,
-                               data->hot_x,
-                               data->hot_y);
+        /* TODO: High-DPI custom cursors? -flibit */
+        if (data->shm_data == NULL) {
+            if (!wayland_get_system_cursor(d, data, &scale)) {
+                return -1;
+            }
+        }
+
+        wl_surface_set_buffer_scale(data->surface, scale);
+        wl_pointer_set_cursor(pointer,
+                              input->pointer_enter_serial,
+                              data->surface,
+                              data->hot_x / scale,
+                              data->hot_y / scale);
         wl_surface_attach(data->surface, data->buffer, 0, 0);
         wl_surface_damage(data->surface, 0, 0, data->w, data->h);
         wl_surface_commit(data->surface);
     }
     else
     {
-        wl_pointer_set_cursor (pointer,
-                               input->pointer_enter_serial,
-                               NULL,
-                               0,
-                               0);
+        wl_pointer_set_cursor(pointer, input->pointer_enter_serial, NULL, 0, 0);
     }
     
     return 0;
@@ -389,10 +433,12 @@ Wayland_InitMouse(void)
 }
 
 void
-Wayland_FiniMouse(void)
+Wayland_FiniMouse(SDL_VideoData *data)
 {
-    /* This effectively assumes that nobody else
-     * touches SDL_Mouse which is effectively
-     * a singleton */
+    int i;
+    for (i = 0; i < data->num_cursor_themes; i += 1) {
+        WAYLAND_wl_cursor_theme_destroy(data->cursor_themes[i].theme);
+    }
+    SDL_free(data->cursor_themes);
 }
 #endif  /* SDL_VIDEO_DRIVER_WAYLAND */
diff --git a/src/video/wayland/SDL_waylandmouse.h b/src/video/wayland/SDL_waylandmouse.h
index 51bfaf5d00..79efaf7e80 100644
--- a/src/video/wayland/SDL_waylandmouse.h
+++ b/src/video/wayland/SDL_waylandmouse.h
@@ -26,6 +26,6 @@
 #if SDL_VIDEO_DRIVER_WAYLAND
 
 extern void Wayland_InitMouse(void);
-extern void Wayland_FiniMouse(void);
+extern void Wayland_FiniMouse(SDL_VideoData *data);
 
 #endif
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 1cd67aaf44..03464d1938 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -495,7 +495,6 @@ display_handle_global(void *data, struct wl_registry *registry, uint32_t id,
         xdg_wm_base_add_listener(d->shell.xdg, &shell_listener_xdg, NULL);
     } else if (SDL_strcmp(interface, "wl_shm") == 0) {
         d->shm = wl_registry_bind(registry, id, &wl_shm_interface, 1);
-        d->cursor_theme = WAYLAND_wl_cursor_theme_load(NULL, 32, d->shm);
     } else if (SDL_strcmp(interface, "zwp_relative_pointer_manager_v1") == 0) {
         Wayland_display_add_relative_pointer_manager(d, id);
     } else if (SDL_strcmp(interface, "zwp_pointer_constraints_v1") == 0) {
@@ -618,7 +617,7 @@ Wayland_VideoQuit(_THIS)
     SDL_VideoData *data = _this->driverdata;
     int i, j;
 
-    Wayland_FiniMouse ();
+    Wayland_FiniMouse(data);
 
     for (i = 0; i < _this->num_displays; ++i) {
         SDL_VideoDisplay *display = &_this->displays[i];
@@ -671,9 +670,6 @@ Wayland_VideoQuit(_THIS)
     if (data->shm)
         wl_shm_destroy(data->shm);
 
-    if (data->cursor_theme)
-        WAYLAND_wl_cursor_theme_destroy(data->cursor_theme);
-
     if (data->shell.xdg)
         xdg_wm_base_destroy(data->shell.xdg);
 
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index 60336b6ccf..2d8b6323a1 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -41,13 +41,19 @@ struct qt_surface_extension;
 struct qt_windowmanager;
 #endif /* SDL_VIDEO_DRIVER_WAYLAND_QT_TOUCH */
 
+typedef struct {
+    struct wl_cursor_theme *theme;
+    int size;
+} SDL_WaylandCursorTheme;
+
 typedef struct {
     struct wl_display *display;
     int display_disconnected;
     struct wl_registry *registry;
     struct wl_compositor *compositor;
     struct wl_shm *shm;
-    struct wl_cursor_theme *cursor_theme;
+    SDL_WaylandCursorTheme *cursor_themes;
+    int num_cursor_themes;
     struct wl_pointer *pointer;
     struct {
         struct xdg_wm_base *xdg;