SDL: wayland: Implement animated system cursors when not using the cursor shape protocol

From ef5d56de5137c3231d3a84d1ae50001177726436 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 27 May 2024 13:12:43 -0400
Subject: [PATCH] wayland: Implement animated system cursors when not using the
 cursor shape protocol

If a system cursor has more than one frame, create a frame callback to run the animation and attach new buffers as necessary to animate the cursor.
---
 src/video/wayland/SDL_waylandevents_c.h |   1 +
 src/video/wayland/SDL_waylandmouse.c    | 154 ++++++++++++++++++++----
 2 files changed, 130 insertions(+), 25 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 5559901c4e4ad..f1eff1fd82d33 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -110,6 +110,7 @@ struct SDL_WaylandInput
     struct zwp_input_timestamps_v1 *touch_timestamps;
     SDL_WindowData *pointer_focus;
     SDL_WindowData *keyboard_focus;
+    struct Wayland_CursorData *current_cursor;
     Uint32 keyboard_id;
     Uint32 pointer_id;
     uint32_t pointer_enter_serial;
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index 2108ea8ce9662..5f8379409d50d 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -47,16 +47,39 @@ static int Wayland_SetRelativeMouseMode(SDL_bool enabled);
 typedef struct
 {
     struct Wayland_SHMBuffer shmBuffer;
+} Wayland_CustomCursor;
+
+typedef struct
+{
+    struct wl_buffer *wl_buffer;
+    Uint32 duration;
+} Wayland_SystemCursorFrame;
+
+typedef struct
+{
+    Wayland_SystemCursorFrame *frames;
+    struct wl_callback *frame_callback;
+    Uint64 last_frame_time_ms;
+    Uint32 total_duration;
+    int num_frames;
+    int current_frame;
+    SDL_SystemCursor id;
+} Wayland_SystemCursor;
+
+struct Wayland_CursorData
+{
+    union
+    {
+        struct Wayland_SHMBuffer custom;
+        Wayland_SystemCursor system;
+    } cursor_data;
     struct wl_surface *surface;
 
     int hot_x, hot_y;
     int w, h;
 
-    /* shmBuffer.shm_data is non-NULL for custom cursors.
-     * When shm_data is NULL, system_cursor must be valid
-     */
-    SDL_SystemCursor system_cursor;
-} Wayland_CursorData;
+    SDL_bool is_system_cursor;
+};
 
 static int dbus_cursor_size;
 static char *dbus_cursor_theme;
@@ -257,7 +280,45 @@ static void Wayland_DBusFinishCursorProperties()
 
 #endif
 
-static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorData *cdata, float *scale)
+static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time);
+struct wl_callback_listener cursor_frame_listener = {
+    cursor_frame_done
+};
+
+static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
+{
+    struct Wayland_CursorData *c = (struct Wayland_CursorData *)data;
+
+    const Uint64 now = SDL_GetTicks();
+    const Uint64 elapsed = (now - c->cursor_data.system.last_frame_time_ms) % c->cursor_data.system.total_duration;
+    int next = c->cursor_data.system.current_frame;
+
+    wl_callback_destroy(cb);
+    c->cursor_data.system.frame_callback = wl_surface_frame(c->surface);
+    wl_callback_add_listener(c->cursor_data.system.frame_callback, &cursor_frame_listener, data);
+
+    /* Calculate the next frame based on the elapsed duration. */
+    for (Uint64 t = c->cursor_data.system.frames[next].duration; t <= elapsed; t += c->cursor_data.system.frames[next].duration) {
+        next = (next + 1) % c->cursor_data.system.num_frames;
+
+        /* Make sure we don't end up in an infinite loop if a cursor has frame durations of 0. */
+        if (!c->cursor_data.system.frames[next].duration) {
+            break;
+        }
+    }
+
+    c->cursor_data.system.last_frame_time_ms = now;
+    c->cursor_data.system.current_frame = next;
+    wl_surface_attach(c->surface, c->cursor_data.system.frames[next].wl_buffer, 0, 0);
+    if (wl_surface_get_version(c->surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
+        wl_surface_damage_buffer(c->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
+    } else {
+        wl_surface_damage(c->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
+    }
+    wl_surface_commit(c->surface);
+}
+
+static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, struct Wayland_CursorData *cdata, float *scale)
 {
     struct wl_cursor_theme *theme = NULL;
     struct wl_cursor *cursor;
@@ -268,7 +329,6 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
 
     SDL_Window *focus;
     SDL_WindowData *focusdata;
-    int i;
 
     /* Fallback envvar if the DBus properties don't exist */
     if (size <= 0) {
@@ -291,7 +351,7 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
     /* Cursors use integer scaling. */
     *scale = SDL_ceilf(focusdata->windowed_scale_factor);
     size *= *scale;
-    for (i = 0; i < vdata->num_cursor_themes; i += 1) {
+    for (int i = 0; i < vdata->num_cursor_themes; i += 1) {
         if (vdata->cursor_themes[i].size == size) {
             theme = vdata->cursor_themes[i].theme;
             break;
@@ -317,7 +377,7 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
         vdata->cursor_themes[vdata->num_cursor_themes++].theme = theme;
     }
 
-    css_name = SDL_GetCSSCursorName(cdata->system_cursor, &fallback_name);
+    css_name = SDL_GetCSSCursorName(cdata->cursor_data.system.id, &fallback_name);
     cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, css_name);
     if (!cursor && fallback_name) {
         cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, fallback_name);
@@ -335,8 +395,22 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
         return SDL_FALSE;
     }
 
+    if (cdata->cursor_data.system.num_frames != cursor->image_count) {
+        SDL_free(cdata->cursor_data.system.frames);
+        cdata->cursor_data.system.frames = SDL_calloc(cursor->image_count, sizeof(Wayland_SystemCursorFrame));
+        if (!cdata->cursor_data.system.frames) {
+            return SDL_FALSE;
+        }
+    }
+
     /* ... Set the cursor data, finally. */
-    cdata->shmBuffer.wl_buffer = WAYLAND_wl_cursor_image_get_buffer(cursor->images[0]);
+    cdata->cursor_data.system.num_frames = cursor->image_count;
+    cdata->cursor_data.system.total_duration = 0;
+    for (int i = 0; i < cursor->image_count; ++i) {
+        cdata->cursor_data.system.frames[i].wl_buffer = WAYLAND_wl_cursor_image_get_buffer(cursor->images[i]);
+        cdata->cursor_data.system.frames[i].duration = cursor->images[i]->delay;
+        cdata->cursor_data.system.total_duration += cursor->images[i]->delay;
+    }
     cdata->hot_x = cursor->images[0]->hotspot_x;
     cdata->hot_y = cursor->images[0]->hotspot_y;
     cdata->w = cursor->images[0]->width;
@@ -350,7 +424,7 @@ static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot
     if (cursor) {
         SDL_VideoDevice *vd = SDL_GetVideoDevice();
         SDL_VideoData *wd = vd->driverdata;
-        Wayland_CursorData *data = SDL_calloc(1, sizeof(Wayland_CursorData));
+        struct Wayland_CursorData *data = SDL_calloc(1, sizeof(struct Wayland_CursorData));
         if (!data) {
             SDL_free(cursor);
             return NULL;
@@ -358,7 +432,7 @@ static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot
         cursor->driverdata = (void *)data;
 
         /* Allocate shared memory buffer for this cursor */
-        if (Wayland_AllocSHMBuffer(surface->w, surface->h, &data->shmBuffer) != 0) {
+        if (Wayland_AllocSHMBuffer(surface->w, surface->h, &data->cursor_data.custom) != 0) {
             SDL_free(cursor->driverdata);
             SDL_free(cursor);
             return NULL;
@@ -367,7 +441,7 @@ static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot
         /* Wayland requires premultiplied alpha for its surfaces. */
         SDL_PremultiplyAlpha(surface->w, surface->h,
                              surface->format->format, surface->pixels, surface->pitch,
-                             SDL_PIXELFORMAT_ARGB8888, data->shmBuffer.shm_data, surface->w * 4);
+                             SDL_PIXELFORMAT_ARGB8888, data->cursor_data.custom.shm_data, surface->w * 4);
 
         data->surface = wl_compositor_create_surface(wd->compositor);
         wl_surface_set_user_data(data->surface, NULL);
@@ -386,7 +460,7 @@ static SDL_Cursor *Wayland_CreateSystemCursor(SDL_SystemCursor id)
     SDL_VideoData *data = SDL_GetVideoDevice()->driverdata;
     SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
     if (cursor) {
-        Wayland_CursorData *cdata = SDL_calloc(1, sizeof(Wayland_CursorData));
+        struct Wayland_CursorData *cdata = SDL_calloc(1, sizeof(struct Wayland_CursorData));
         if (!cdata) {
             SDL_free(cursor);
             return NULL;
@@ -403,7 +477,8 @@ static SDL_Cursor *Wayland_CreateSystemCursor(SDL_SystemCursor id)
             wl_surface_set_user_data(cdata->surface, NULL);
         }
 
-        cdata->system_cursor = id;
+        cdata->cursor_data.system.id = id;
+        cdata->is_system_cursor = SDL_TRUE;
     }
 
     return cursor;
@@ -414,13 +489,16 @@ static SDL_Cursor *Wayland_CreateDefaultCursor(void)
     return Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
 }
 
-static void Wayland_FreeCursorData(Wayland_CursorData *d)
+static void Wayland_FreeCursorData(struct Wayland_CursorData *d)
 {
     /* Buffers for system cursors must not be destroyed. */
-    if (d->shmBuffer.shm_data) {
-        Wayland_ReleaseSHMBuffer(&d->shmBuffer);
+    if (d->is_system_cursor) {
+        if (d->cursor_data.system.frame_callback) {
+            wl_callback_destroy(d->cursor_data.system.frame_callback);
+        }
+        SDL_free(d->cursor_data.system.frames);
     } else {
-        d->shmBuffer.wl_buffer = NULL;
+        Wayland_ReleaseSHMBuffer(&d->cursor_data.custom);
     }
 
     if (d->surface) {
@@ -440,7 +518,7 @@ static void Wayland_FreeCursor(SDL_Cursor *cursor)
         return;
     }
 
-    Wayland_FreeCursorData((Wayland_CursorData *)cursor->driverdata);
+    Wayland_FreeCursorData((struct Wayland_CursorData *)cursor->driverdata);
 
     SDL_free(cursor->driverdata);
     SDL_free(cursor);
@@ -531,15 +609,23 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
         return -1;
     }
 
+    /* Stop the frame callback for old animated cursors. */
+    if (input->current_cursor && input->current_cursor->is_system_cursor &&
+        input->current_cursor->cursor_data.system.frame_callback) {
+        wl_callback_destroy(input->current_cursor->cursor_data.system.frame_callback);
+        input->current_cursor->cursor_data.system.frame_callback = NULL;
+    }
+
     if (cursor) {
-        Wayland_CursorData *data = cursor->driverdata;
+        struct Wayland_CursorData *data = cursor->driverdata;
 
         /* TODO: High-DPI custom cursors? -flibit */
-        if (!data->shmBuffer.shm_data) {
+        if (data->is_system_cursor) {
             if (input->cursor_shape) {
-                Wayland_SetSystemCursorShape(input, data->system_cursor);
+                Wayland_SetSystemCursorShape(input, data->cursor_data.system.id);
 
                 input->cursor_visible = SDL_TRUE;
+                input->current_cursor = data;
 
                 if (input->relative_mode_override) {
                     Wayland_input_disable_relative_pointer(input);
@@ -558,11 +644,28 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
                               data->surface,
                               data->hot_x / scale,
                               data->hot_y / scale);
-        wl_surface_attach(data->surface, data->shmBuffer.wl_buffer, 0, 0);
-        wl_surface_damage(data->surface, 0, 0, data->w, data->h);
+        if (data->is_system_cursor) {
+            wl_surface_attach(data->surface, data->cursor_data.system.frames[0].wl_buffer, 0, 0);
+
+            /* If more than one frame is available, create a frame callback to run the animation. */
+            if (data->cursor_data.system.num_frames > 1) {
+                data->cursor_data.system.last_frame_time_ms = SDL_GetTicks();
+                data->cursor_data.system.current_frame = 0;
+                data->cursor_data.system.frame_callback = wl_surface_frame(data->surface);
+                wl_callback_add_listener(data->cursor_data.system.frame_callback, &cursor_frame_listener, data);
+            }
+        } else {
+            wl_surface_attach(data->surface, data->cursor_data.custom.wl_buffer, 0, 0);
+        }
+        if (wl_surface_get_version(data->surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
+            wl_surface_damage_buffer(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
+        } else {
+            wl_surface_damage(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
+        }
         wl_surface_commit(data->surface);
 
         input->cursor_visible = SDL_TRUE;
+        input->current_cursor = data;
 
         if (input->relative_mode_override) {
             Wayland_input_disable_relative_pointer(input);
@@ -571,6 +674,7 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
 
     } else {
         input->cursor_visible = SDL_FALSE;
+        input->current_cursor = NULL;
         wl_pointer_set_cursor(pointer, input->pointer_enter_serial, NULL, 0, 0);
     }