From dcb8a6521c887a30874e02726a165499022b2793 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 11 Oct 2025 13:08:25 -0400
Subject: [PATCH] Add animated cursor support
Adds support for animated cursors on Cocoa, Wayland, Win32, and X11.
testcursor can take a semicolon separated list of filenames and load an animated cursor from them.
---
include/SDL3/SDL_mouse.h | 65 ++++++
src/dynapi/SDL_dynapi.sym | 1 +
src/dynapi/SDL_dynapi_overrides.h | 1 +
src/dynapi/SDL_dynapi_procs.h | 1 +
src/events/SDL_mouse.c | 97 +++++++++
src/events/SDL_mouse_c.h | 3 +
src/video/cocoa/SDL_cocoamouse.h | 13 ++
src/video/cocoa/SDL_cocoamouse.m | 77 ++++++--
src/video/cocoa/SDL_cocoawindow.m | 34 +++-
src/video/wayland/SDL_waylandmouse.c | 86 ++++++--
src/video/windows/SDL_windowsmouse.c | 282 ++++++++++++++++++++++++---
src/video/x11/SDL_x11mouse.c | 55 ++++++
src/video/x11/SDL_x11sym.h | 3 +
test/testcustomcursor.c | 159 +++++++++++++--
14 files changed, 810 insertions(+), 67 deletions(-)
diff --git a/include/SDL3/SDL_mouse.h b/include/SDL3/SDL_mouse.h
index 4ecd1d001045b..8ca05703a384b 100644
--- a/include/SDL3/SDL_mouse.h
+++ b/include/SDL3/SDL_mouse.h
@@ -130,6 +130,17 @@ typedef enum SDL_MouseWheelDirection
SDL_MOUSEWHEEL_FLIPPED /**< The scroll direction is flipped / natural */
} SDL_MouseWheelDirection;
+/**
+ * Animated cursor frame info.
+ *
+ * \since This struct is available since SDL 3.4.0.
+ */
+typedef struct SDL_CursorFrameInfo
+{
+ SDL_Surface *surface; /**< The surface data for this frame */
+ Uint32 duration; /**< The frame duration in milliseconds (a duration of 0 is infinite) */
+} SDL_CursorFrameInfo;
+
/**
* A bitmask of pressed mouse buttons, as reported by SDL_GetMouseState, etc.
*
@@ -565,6 +576,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_CaptureMouse(bool enabled);
*
* \since This function is available since SDL 3.2.0.
*
+ * \sa SDL_CreateAnimatedCursor
* \sa SDL_CreateColorCursor
* \sa SDL_CreateSystemCursor
* \sa SDL_DestroyCursor
@@ -600,6 +612,7 @@ extern SDL_DECLSPEC SDL_Cursor * SDLCALL SDL_CreateCursor(const Uint8 *data,
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_AddSurfaceAlternateImage
+ * \sa SDL_CreateAnimatedCursor
* \sa SDL_CreateCursor
* \sa SDL_CreateSystemCursor
* \sa SDL_DestroyCursor
@@ -609,6 +622,57 @@ extern SDL_DECLSPEC SDL_Cursor * SDLCALL SDL_CreateColorCursor(SDL_Surface *surf
int hot_x,
int hot_y);
+/**
+ * Create an animated color cursor.
+ *
+ * Animated cursors are composed of a sequential array of frames, specified
+ * as surfaces and durations in an array of SDL_CursorFrameInfo structs.
+ * The hot spot coordinates are universal to all frames, and all frames must
+ * have the same dimensions.
+ *
+ * Frame durations are specified in milliseconds. A duration of 0 implies an
+ * infinite frame time, and the animation will stop on that frame. To create
+ * a one-shot animation, set the duration of the last frame in the sequence
+ * to 0.
+ *
+ * If this function is passed surfaces with alternate representations added
+ * with SDL_AddSurfaceAlternateImage(), the surfaces will be interpreted as the
+ * content to be used for 100% display scale, and the alternate
+ * representations will be used for high DPI situations. For example, if the
+ * original surfaces are 32x32, then on a 2x macOS display or 200% display scale
+ * on Windows, a 64x64 version of the image will be used, if available. If a
+ * matching version of the image isn't available, the closest larger size
+ * image will be downscaled to the appropriate size and be used instead, if
+ * available. Otherwise, the closest smaller image will be upscaled and be
+ * used instead.
+ *
+ * If the underlying platform does not support animated cursors, this function
+ * will fall back to creating a static color cursor using the first frame in
+ * the sequence.
+ *
+ * \param frames an array of cursor images composing the animation.
+ * \param frame_count the number of frames in the sequence.
+ * \param hot_x the x position of the cursor hot spot.
+ * \param hot_y the y position of the cursor hot spot.
+ * \returns the new cursor on success or NULL on failure; call SDL_GetError()
+ * for more information.
+ *
+ * \threadsafety This function should only be called on the main thread.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_AddSurfaceAlternateImage
+ * \sa SDL_CreateCursor
+ * \sa SDL_CreateColorCursor
+ * \sa SDL_CreateSystemCursor
+ * \sa SDL_DestroyCursor
+ * \sa SDL_SetCursor
+ */
+extern SDL_DECLSPEC SDL_Cursor *SDLCALL SDL_CreateAnimatedCursor(SDL_CursorFrameInfo *frames,
+ int frame_count,
+ int hot_x,
+ int hot_y);
+
/**
* Create a system cursor.
*
@@ -687,6 +751,7 @@ extern SDL_DECLSPEC SDL_Cursor * SDLCALL SDL_GetDefaultCursor(void);
*
* \since This function is available since SDL 3.2.0.
*
+ * \sa SDL_CreateAnimatedCursor
* \sa SDL_CreateColorCursor
* \sa SDL_CreateCursor
* \sa SDL_CreateSystemCursor
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index f916b8f5f59e3..b779befedd12c 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1266,6 +1266,7 @@ SDL3_0.0.0 {
SDL_SavePNG;
SDL_GetSystemPageSize;
SDL_GetPenDeviceType;
+ SDL_CreateAnimatedCursor;
# extra symbols go here (don't modify this line)
local: *;
};
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 0c0806f0b41b3..b19ddb8c241fa 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1292,3 +1292,4 @@
#define SDL_SavePNG SDL_SavePNG_REAL
#define SDL_GetSystemPageSize SDL_GetSystemPageSize_REAL
#define SDL_GetPenDeviceType SDL_GetPenDeviceType_REAL
+#define SDL_CreateAnimatedCursor SDL_CreateAnimatedCursor_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 72d1b83be6027..654f617534f46 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1300,3 +1300,4 @@ SDL_DYNAPI_PROC(bool,SDL_SavePNG_IO,(SDL_Surface *a,SDL_IOStream *b,bool c),(a,b
SDL_DYNAPI_PROC(bool,SDL_SavePNG,(SDL_Surface *a,const char *b),(a,b),return)
SDL_DYNAPI_PROC(int,SDL_GetSystemPageSize,(void),(),return)
SDL_DYNAPI_PROC(SDL_PenDeviceType,SDL_GetPenDeviceType,(SDL_PenID a),(a),return)
+SDL_DYNAPI_PROC(SDL_Cursor*,SDL_CreateAnimatedCursor,(SDL_CursorFrameInfo *a,int b,int c,int d),(a,b,c,d),return)
diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c
index 95e31b2ffed7c..1aa4112efbb12 100644
--- a/src/events/SDL_mouse.c
+++ b/src/events/SDL_mouse.c
@@ -1552,6 +1552,103 @@ SDL_Cursor *SDL_CreateCursor(const Uint8 *data, const Uint8 *mask, int w, int h,
return cursor;
}
+SDL_Cursor *SDL_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
+{
+ SDL_Mouse *mouse = SDL_GetMouse();
+ SDL_Cursor *cursor = NULL;
+
+ CHECK_PARAM(!frames) {
+ SDL_InvalidParamError("frames");
+ return NULL;
+ }
+
+ CHECK_PARAM(!frame_count) {
+ SDL_InvalidParamError("frame_count");
+ return NULL;
+ }
+
+ // Fall back to a static cursor if the platform doesn't support animated cursors.
+ if (!mouse->CreateAnimatedCursor) {
+ // If there is a frame with infinite duration, use it; otherwise, use the first.
+ for (int i = 0; i < frame_count; ++i) {
+ if (!frames[i].duration) {
+ return SDL_CreateColorCursor(frames[i].surface, hot_x, hot_y);
+ }
+ }
+
+ return SDL_CreateColorCursor(frames[0].surface, hot_x, hot_y);
+ }
+
+ // Allow specifying the hot spot via properties on the surface
+ SDL_PropertiesID props = SDL_GetSurfaceProperties(frames[0].surface);
+ hot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, hot_x);
+ hot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, hot_y);
+
+ // Sanity check the hot spot
+ CHECK_PARAM((hot_x < 0) || (hot_y < 0) ||
+ (hot_x >= frames[0].surface->w) || (hot_y >= frames[0].surface->h)) {
+ SDL_SetError("Cursor hot spot doesn't lie within cursor");
+ return NULL;
+ }
+
+ CHECK_PARAM(!frames[0].surface) {
+ SDL_SetError("Null surface in frame 0");
+ return NULL;
+ }
+
+ bool isstack;
+ SDL_CursorFrameInfo *temp_frames = SDL_small_alloc(SDL_CursorFrameInfo, frame_count, &isstack);
+ if (!temp_frames) {
+ return NULL;
+ }
+ SDL_memset(temp_frames, 0, sizeof(SDL_CursorFrameInfo) * frame_count);
+
+ const int w = frames[0].surface->w;
+ const int h = frames[0].surface->h;
+
+ for (int i = 0; i < frame_count; ++i) {
+ CHECK_PARAM(!frames[i].surface) {
+ SDL_SetError("Null surface in frame %i", i);
+ goto cleanup;
+ }
+
+ // All cursor images should be the same size.
+ CHECK_PARAM(frames[i].surface->w != w || frames[i].surface->h != h) {
+ SDL_SetError("All frames in an animated sequence must have the same dimensions");
+ goto cleanup;
+ }
+
+ if (frames[i].surface->format == SDL_PIXELFORMAT_ARGB8888) {
+ temp_frames[i].surface = frames[i].surface;
+ } else {
+ SDL_Surface *temp = SDL_ConvertSurface(frames[i].surface, SDL_PIXELFORMAT_ARGB8888);
+ if (!temp) {
+ goto cleanup;
+ }
+ temp_frames[i].surface = temp;
+ }
+ temp_frames[i].duration = frames[i].duration;
+ }
+
+ cursor = mouse->CreateAnimatedCursor(temp_frames, frame_count, hot_x, hot_y);
+ if (cursor) {
+ cursor->next = mouse->cursors;
+ mouse->cursors = cursor;
+ }
+
+cleanup:
+ // Clean up any temporary converted surfaces.
+ for (int i = 0; i < frame_count; ++i) {
+ if (temp_frames[i].surface && frames[i].surface != temp_frames[i].surface) {
+ SDL_DestroySurface(temp_frames[i].surface);
+ }
+ }
+
+ SDL_small_free(temp_frames, isstack);
+
+ return cursor;
+}
+
SDL_Cursor *SDL_CreateColorCursor(SDL_Surface *surface, int hot_x, int hot_y)
{
SDL_Mouse *mouse = SDL_GetMouse();
diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h
index 0114f2da84ae3..6e049fe841921 100644
--- a/src/events/SDL_mouse_c.h
+++ b/src/events/SDL_mouse_c.h
@@ -60,6 +60,9 @@ typedef struct
// Create a cursor from a surface
SDL_Cursor *(*CreateCursor)(SDL_Surface *surface, int hot_x, int hot_y);
+ // Create an animated cursor from a sequence of surfaces
+ SDL_Cursor *(*CreateAnimatedCursor)(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y);
+
// Create a system cursor
SDL_Cursor *(*CreateSystemCursor)(SDL_SystemCursor id);
diff --git a/src/video/cocoa/SDL_cocoamouse.h b/src/video/cocoa/SDL_cocoamouse.h
index 70282be9fa0d4..4ea7b27ed009f 100644
--- a/src/video/cocoa/SDL_cocoamouse.h
+++ b/src/video/cocoa/SDL_cocoamouse.h
@@ -32,6 +32,19 @@ extern void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event);
extern void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y);
extern void Cocoa_QuitMouse(SDL_VideoDevice *_this);
+struct SDL_CursorData
+{
+ NSTimer *frameTimer;
+ int current_frame;
+
+ int num_cursors;
+ struct
+ {
+ void *cursor;
+ Uint32 duration;
+ } frames[];
+};
+
typedef struct
{
// Whether we've seen a cursor warp since the last move event.
diff --git a/src/video/cocoa/SDL_cocoamouse.m b/src/video/cocoa/SDL_cocoamouse.m
index 2c210e9c5cb20..b0974a48b2db0 100644
--- a/src/video/cocoa/SDL_cocoamouse.m
+++ b/src/video/cocoa/SDL_cocoamouse.m
@@ -66,27 +66,59 @@ + (NSCursor *)invisibleCursor
}
@end
-static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
+static SDL_Cursor *Cocoa_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
{
@autoreleasepool {
NSImage *nsimage;
NSCursor *nscursor = NULL;
SDL_Cursor *cursor = NULL;
- nsimage = Cocoa_CreateImage(surface);
- if (nsimage) {
- nscursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(hot_x, hot_y)];
- }
+ cursor = SDL_calloc(1, sizeof(*cursor));
+ if (cursor) {
+ SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata) + (sizeof(*cdata->frames) * frame_count));
+ if (!cdata) {
+ SDL_free(cursor);
+ return NULL;
+ }
- if (nscursor) {
- cursor = SDL_calloc(1, sizeof(*cursor));
- if (cursor) {
- cursor->internal = (void *)CFBridgingRetain(nscursor);
+ cursor->internal = cdata;
+
+ for (int i = 0; i < frame_count; ++i) {
+ nsimage = Cocoa_CreateImage(frames[i].surface);
+ if (nsimage) {
+ nscursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(hot_x, hot_y)];
+ }
+
+ if (nscursor) {
+ ++cdata->num_cursors;
+ cdata->frames[i].cursor = (void *)CFBridgingRetain(nscursor);
+ cdata->frames[i].duration = frames[i].duration;
+ } else {
+ for (int j = 0; j < i; ++j) {
+ CFBridgingRelease(cdata->frames[i].cursor);
+ }
+
+ SDL_free(cdata);
+ SDL_free(cursor);
+ cursor = NULL;
+ break;
+ }
}
- }
- return cursor;
+ return cursor;
+ }
}
+
+ return NULL;
+}
+
+static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
+{
+ SDL_CursorFrameInfo frame = {
+ surface, 0
+ };
+
+ return Cocoa_CreateAnimatedCursor(&frame, 1, hot_x, hot_y);
}
/* there are .pdf files of some of the cursors we need, installed by default on macOS, but not available through NSCursor.
@@ -204,8 +236,11 @@ + (NSCursor *)invisibleCursor
if (nscursor) {
cursor = SDL_calloc(1, sizeof(*cursor));
if (cursor) {
+ SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata) + sizeof(*cdata->frames));
// We'll free it later, so retain it here
- cursor->internal = (void *)CFBridgingRetain(nscursor);
+ cursor->internal = cdata;
+ cdata->frames[0].cursor = (void *)CFBridgingRetain(nscursor);
+ cdata->num_cursors = 1;
}
}
@@ -222,7 +257,14 @@ + (NSCursor *)invisibleCursor
static void Cocoa_FreeCursor(SDL_Cursor *cursor)
{
@autoreleasepool {
- CFBridgingRelease((void *)cursor->internal);
+ SDL_CursorData *cdata = cursor->internal;
+ if (cdata->frameTimer) {
+ [cdata->frameTimer invalidate];
+ }
+ for (int i = 0; i < cdata->num_cursors; ++i) {
+ CFBridgingRelease(cdata->frames[i].cursor);
+ }
+ SDL_free(cdata);
SDL_free(cursor);
}
}
@@ -232,6 +274,14 @@ static bool Cocoa_ShowCursor(SDL_Cursor *cursor)
@autoreleasepool {
SDL_VideoDevice *device = SDL_GetVideoDevice();
SDL_Window *window = (device ? device->windows : NULL);
+
+ SDL_CursorData *cdata = cursor->internal;
+ cdata->current_frame = 0;
+ if (cdata->frameTimer) {
+ [cdata->frameTimer invalidate];
+ cdata->frameTimer = nil;
+ }
+
for (; window != NULL; window = window->next) {
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
if (data) {
@@ -381,6 +431,7 @@ bool Cocoa_InitMouse(SDL_VideoDevice *_this)
mouse->internal = data;
mouse->CreateCursor = Cocoa_CreateCursor;
+ mouse->CreateAnimatedCursor = Cocoa_CreateAnimatedCursor;
mouse->CreateSystemCursor = Cocoa_CreateSystemCursor;
mouse->ShowCursor = Cocoa_ShowCursor;
mouse->FreeCursor = Cocoa_FreeCursor;
diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index 2fee316dfcfb6..3425737929b07 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -761,12 +761,44 @@ static void Cocoa_WaitForMiniaturizable(SDL_Window *window)
}
}
+static void Cocoa_IncrementCursorFrame(void)
+{
+ SDL_Mouse *mouse = SDL_GetMouse();
+
+ if (mouse->cur_cursor) {
+ SDL_CursorData *cdata = mouse->cur_cursor->internal;
+ cdata->current_frame = (cdata->current_frame + 1) % cdata->num_cursors;
+
+ SDL_Window *focus = SDL_GetMouseFocus();
+ if (focus) {
+ SDL_CocoaWindowData *_data = (__bridge SDL_CocoaWindowData *)focus->internal;
+ [_data.nswindow invalidateCursorRectsForView:_data.sdlContentView];
+ }
+ }
+}
+
static NSCursor *Cocoa_GetDesiredCursor(void)
{
SDL_Mouse *mouse = SDL_GetMouse();
if (mouse->cursor_visible && mouse->cur_cursor && !mouse->relative_mode) {
- return (__bridge NSCursor *)mouse->cur_cursor->internal;
+ SDL_CursorData *cdata = mouse->cur_cursor->internal;
+
+ if (cdata) {
+ if (cdata->num_cursors > 1 && cdata->frames[cdata->current_frame].duration && !cdata->frameTimer) {
+ const NSTimeInterval interval = cdata->frames[cdata->current_frame].duration * 0.001;
+ cdata->frameTimer = [NSTimer timerWithTimeInterval:interval
+ repeats:NO
+ block:^(NSTimer *timer) {
+ cdata->frameTimer = nil;
+ Cocoa_IncrementCursorFrame();
+ }];
+
+ [[NSRunLoop currentRunLoop] addTimer:cdata->frameTimer forMode:NSRunLoopCommonModes];
+ }
+
+ return (__bridge NSCursor *)cdata->frames[cdata->current_frame].cursor;
+ }
}
return [NSCursor invisibleCursor];
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index 3ff173c70eded..6bfb0079b81d8 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -55,6 +55,8 @@ typedef struct
typedef struct
{
+ int width;
+ int height;
int hot_x;
int hot_y;
struct wl_list scaled_cursor_cache;
@@ -304,7 +306,7 @@ static struct wl_buffer *Wayland_SeatGetCursorFrame(SDL_WaylandSeat *seat, int f
}
static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time);
-struct wl_callback_listener cursor_frame_listener = {
+static const struct wl_callback_listener cursor_frame_listener = {
cursor_frame_done
};
@@ -323,10 +325,6 @@ static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
Uint32 advance = 0;
int next = seat->pointer.cursor_state.current_frame;
- wl_callback_destroy(cb);
- 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);
-
seat->pointer.cursor_state.current_frame_time_ms += elapsed;
// Calculate the next frame based on the elapsed duration.
@@ -340,6 +338,15 @@ 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;
+
+ // 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);
+ }
+
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;
@@ -538,6 +545,11 @@ static Wayland_ScaledCustomCursor *Wayland_CacheScaledCustomCursor(SDL_CursorDat
for (int i = 0; i < cursor->num_frames; ++i) {
if (!surface) {
surface = SDL_GetSurfaceImage(cursor->cursor_data.custom.sdl_cursor_surfaces[i], (float)scale);
+ if (!surface) {
+ Wayland_ReleaseSHMPool(cache->shmPool);
+ SDL_free(cache);
+ return NULL;
+ }
}
// Wayland requires premultiplied alpha for its surfaces.
@@ -564,7 +576,8 @@ static bool Wayland_GetCustomCursor(SDL_CursorData *cursor, SDL_WaylandSeat *sea
SDL_Window *focus = SDL_GetMouseFocus();
double scale_factor = 1.0;
- if (focus && SDL_SurfaceHasAlternateImages(custom_cursor->sdl_cursor_surfaces[0])) {
+ // If the surfaces were released, there are no scaled images.
+ if (focus && custom_cursor->sdl_cursor_surfaces[0]) {
scale_factor = focus->internal->scale_factor;
}
@@ -580,42 +593,83 @@ static bool Wayland_GetCustomCursor(SDL_CursorData *cursor, SDL_WaylandSeat *sea
seat->pointer.cursor_state.cursor_handle = c;
*scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0;
- *dst_width = custom_cursor->sdl_cursor_surfaces[0]->w;
- *dst_height = custom_cursor->sdl_cursor_surfaces[0]->h;
+ *dst_width = custom_cursor->width;
+ *dst_height = custom_cursor->height;
*hot_x = custom_cursor->hot_x;
*hot_y = custom_cursor->hot_y;
return true;
}
-static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
+static SDL_Cursor *Wayland_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
{
SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
if (cursor) {
- SDL_CursorData *data = SDL_calloc(1, sizeof(*data) + sizeof(SDL_Surface *));
+ SDL_CursorData *data = SDL_calloc(1, sizeof(*data) + (sizeof(SDL_Surface *) * frame_count));
if (!data) {
SDL_free(cursor);
return NULL;
}
+
+ data->frame_durations_ms = SDL_calloc(frame_count, sizeof(Uint32));
+ if (!data->frame_durations_ms) {
+ SDL_free(data);
+ SDL_free(cursor);
+ return NULL;
+ }
+
cursor->internal = data;
WAYLAND_wl_list_init(&data->cursor_data.custom.scaled_cursor_cache);
- data->num_frames = 1;
+ data->cursor_data.custom.width = frames[0].surface->w;
+ data->cursor_data.custom.height = frames[0].surface->h;
data->cursor_data.custom.hot_x = hot_x;
data->cursor_data.custom.hot_y = hot_y;
+ data->num_frames = frame_count;
- data->cursor_data.custom.sdl_cursor_surfaces[0] = surface;
- ++surface->refcount;
+ for (int i = 0; i < frame_count; ++i) {
+ data->frame_durations_ms[i] = frames[i].duration;
+ if (data->total_duration_ms < SDL_MAX_UINT32) {
+ if (data->frame_durations_ms[i] > 0) {
+ data->total_duration_ms += data->frame_durations_ms[i];
+ } else {
+ data->total_duration_ms = SDL_MAX_UINT32;
+ }
+ }
+ data->cursor_data.custom.sdl_cursor_surfaces[i] = frames[i].surface;
+ ++frames[i].surface->refcount;
+ }
// If the cursor has only one size, just prepare it now.
- if (!SDL_SurfaceHasAlternateImages(surface)) {
- Wayland_CacheScaledCustomCursor(data, 1.0);
+ if (!SDL_SurfaceHasAlternateImages(frames[0].surface)) {
+ bool success = !!Wayland_CacheScaledCustomCursor(data, 1.0);
+
+ // Done with the surfaces.
+ for (int i = 0; i < frame_count; ++i) {
+ SDL_DestroySurface(data->cursor_data.custom.sdl_cursor_surfaces[i]);
+ data->cursor_data.custom.sdl_cursor_surfaces[i] = NULL;
+ }
+
+ if (!success) {
+ SDL_free(data);
+ SDL_free(cursor);
+ return NULL;
+ }
}
}
return cursor;
}
+static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
+{
+ SDL_CursorFrameInfo frame = {
+ surface, 0
+ };
+
+ return Wayland_CreateAnimatedCursor(&frame, 1, hot_x, hot_y);
+}
+
static SDL_Cursor *Wayland_CreateSystemCursor(SDL_SystemCursor id)
{
SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
@@ -660,7 +714,6 @@ static void Wayland_FreeCursorData(SDL_CursorData *d)
wl_surface_attach(seat->pointer.cursor_state.surface, NULL, 0, 0);
}
-
seat->pointer.current_cursor = NULL;
}
}
@@ -1120,6 +1173,7 @@ void Wayland_InitMouse(void)
SDL_Mouse *mouse = SDL_GetMouse();
mouse->CreateCursor = Wayland_CreateCursor;
+ mouse->CreateAnimatedCursor = Wayland_CreateAnimatedCursor;
mouse->CreateSystemCursor = Wayland_CreateSystemCursor;
mouse->ShowCursor = Wayland_ShowCursor;
mouse->FreeCursor = Wayland_FreeCursor;
diff --git a/src/video/windows/SDL_windowsmouse.c b/src/video/windows/SDL_windowsmouse.c
index 63970ac943364..eead6d4881fb5 100644
--- a/src/video/windows/SDL_windowsmouse.c
+++ b/src/video/windows/SDL_windowsmouse.c
@@ -31,6 +31,66 @@
#include "../../joystick/usb_ids.h"
#include "../../core/windows/SDL_windows.h" // for checking windows version
+#pragma pack(push, 1)
+
+#define RIFF_FOURCC(c0, c1, c2, c3) \
+ ((DWORD)(BYTE)(c0) | ((DWORD)(BYTE)(c1) << 8) | \
+ ((DWORD)(BYTE)(c2) << 16) | ((DWORD)(BYTE)(c3) << 24))
+
+#define ANI_FLAG_ICON 0x1
+
+typedef struct
+{
+ BYTE bWidth;
+ BYTE bHeight;
+ BYTE bColorCount;
+ BYTE bReserved;
+ WORD xHotspot;
+ WORD yHotspot;
+ DWORD dwDIBSize;
+ DWORD dwDIBOffset;
+} CURSORICONFILEDIRENTRY;
+
+typedef struct
+{
+ WORD idReserved;
+ WORD idType;
+ WORD idCount;
+ CURSORICONFILEDIRENTRY idEntries;
+} CURSORICONFILEDIR;
+
+typedef struct
+{
+ DWORD chunkType; // 'icon'
+ DWORD chunkSize;
+
+ CURSORICONFILEDIR icon_info;
+ BITMAPINFOHEADER bmi_header;
+} ANIMICONINFO;
+
+typedef struct
+{
+ DWORD riffID;
+ DWORD riffSizeof;
+
+ DWORD aconChunkID; // 'ACON'
+ DWORD aniChunkID; // 'anih'
+ DWORD aniSizeof; // sizeof(ANIHEADER) = 36 bytes
+ struct
+ {
+ DWORD cbSizeof; // sizeof(ANIHEADER) = 36 bytes.
+ DWORD frames; // Number of frames in the frame list.
+ DWORD steps; // Number of steps in the animation loop.
+ DWORD width; // Width
+ DWORD height; // Height
+ DWORD bpp; // bpp
+ DWORD planes; // Not used
+ DWORD jifRate; // Default display rate, in jiffies (1/60s)
+ DWORD fl; // AF_ICON should be set. AF_SEQUENCE is optional
+ } ANIHEADER;
+} RIFFHEADER;
+
+#pragma pack(pop)
typedef struct CachedCursor
{
@@ -41,11 +101,12 @@ typedef struct CachedCursor
struct SDL_CursorData
{
- SDL_Surface *surface;
int hot_x;
int hot_y;
+ int num_frames;
CachedCursor *cache;
HCURSOR cursor;
+ SDL_CursorFrameInfo frames[1];
};
typedef struct
@@ -207,12 +268,12 @@ static HCURSOR WIN_CreateHCursor(SDL_Surface *surface, int hot_x, int hot_y)
{
HCURSOR hcursor = NULL;
bool is_monochrome = IsMonochromeSurface(surface);
- ICONINFO ii = {
- .fIcon = FALSE,
- .xHotspot = (DWORD)hot_x,
+ ICONINFO ii = {
+ .fIcon = FALSE,
+ .xHotspot = (DWORD)hot_x,
.yHotspot = (DWORD)hot_y,
.hbmMask = CreateMaskBitmap(surface, is_monochrome),
- .hbmColor = is_monochrome ? NULL : CreateColorBitmap(surface)
+ .hbmColor = is_monochrome ? NULL : CreateColorBitmap(surface)
};
if (!ii.hbmMask || (!is_monochrome && !ii.hbmColor)) {
@@ -236,6 +297,141 @@ static HCURSOR WIN_CreateHCursor(SDL_Surface *surface, int hot_x, int hot_y)
return hcursor;
}
+/* Windows doesn't have an API to easily create animated cursors from a sequence of images,
+ * so we have to build an animated cursor resource file in memory and load it.
+ */
+static HCURSOR WIN_CreateAnimatedCursorInternal(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y, float scale)
+{
+ static const double WIN32_JIFFY = 1000.0 / 60.0;
+ SDL_Surface *surface = NULL;
+ bool use_scaled_surfaces = scale != 1.0f;
+
+ if (use_scaled_surfaces) {
+ surface = SDL_GetSurfaceImage(frames[0].surface, scale);
+ } else {
+ surface = frames[0].surface;
+ }
+
+ // Since XP and still as of Win11, Windows cursors have a hard size limit of 256x256.
+ if (!surface || surface->w > 256 || surface->h > 256) {
+ return NULL;
+ }
+
+ const DWORD image_data_size = surface->w * surface->pitch * 2;
+ const DWORD total_image_data_size = image_data_size * frame_count;
+ const DWORD alloc_size = sizeof(RIFFHEADER) + (sizeof(DWORD) * (5 + frame_count)) + (sizeof(ANIMICONINFO) * frame_count) + total_image_data_size;
+ const int w = surface->w;
+ const int h = surface->h;
+
+ hot_x = (int)SDL_round(hot_x * scale);
+ hot_y = (int)SDL_round(hot_y * scale);
+
+ BYTE *membase = SDL_malloc(alloc_size);
+ if (!membase) {
+ return NULL;
+ }
+
+ RIFFHEADER *riff = (RIFFHEADER *)membase;
+ riff->riffID = RIFF_FOURCC('R', 'I', 'F', 'F');
+ riff->riffSizeof = alloc_size - (sizeof(DWORD) * 2); // The total size, minus the RIFF header DWORDs.
+ riff->aconChunkID = RIFF_FOURCC('A', 'C', 'O', 'N');
+ riff->aniChunkID = RIFF_FOURCC('a', 'n', 'i', 'h');
+ riff->aniSizeof = sizeof(riff->ANIHEADER);
+ riff->ANIHEADER.cbSizeof = sizeof(riff->ANIHEADER);
+ riff->ANIHEADER.frames = frame_count;
+ riff->ANIHEADER.steps = frame_count;
+ riff->ANIHEADER.width = w;
+ riff->ANIHEADER.height = h;
+ riff->ANIHEADER.bpp = 32;
+ riff->ANIHEADER.planes = 1;
+ riff->ANIHEADER.jifRate = 1;
+ riff->ANIHEADER.fl = ANI_FLAG_ICON;
+
+ DWORD *dwptr = (DWORD *)(membase + sizeof(*riff));
+
+ // Rate chunk
+ *dwptr++ = RIFF_FOURCC('r', 'a', 't', 'e');
+ *dwptr++ = sizeof(DWORD) * frame_count;
+ for (int i = 0; i < frame_count; ++i) {
+ // Animated Win32 cursors are in jiffy units, and one jiffy is 1/60 of a second.
+ *dwptr++ = frames[i].duration ? SDL_lround(frames[i].duration / WIN32_JIFFY) : 0xFFFFFFFF;
+ }
+
+ // Frame list chunk
+ *dwptr++ = RIFF_FOURCC('L', 'I', 'S', 'T');
+ *dwptr++ = (sizeof(ANIMICONINFO) * frame_count) + total_image_data_size + sizeof(DWORD);
+ *dwptr++ = RIFF_FOURCC('f', 'r', 'a', 'm');
+
+ BYTE *icon_data = (BYTE *)dwptr;
+
+ for (int i = 0; i < frame_count; ++i) {
+ if (!surface) {
+ if (use_scaled_surfaces) {
+ surface = SDL_GetSurfaceImage(frames[i].surface, scale);
+ if (!surface) {
+
(Patch may be truncated, please check the link at the top of this post.)