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.
This commit is contained in:
Frank Praznik
2025-10-11 13:08:25 -04:00
parent 69692de8b8
commit dcb8a6521c
14 changed files with 811 additions and 68 deletions

View File

@@ -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

View File

@@ -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: *;
};

View File

@@ -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

View File

@@ -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)

View File

@@ -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();

View File

@@ -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);

View File

@@ -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.

View File

@@ -66,22 +66,42 @@
}
@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);
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;
}
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) {
cursor = SDL_calloc(1, sizeof(*cursor));
if (cursor) {
cursor->internal = (void *)CFBridgingRetain(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;
}
}
@@ -89,6 +109,18 @@ static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y
}
}
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.
If we can load them ourselves, use them, otherwise fallback to something standard but not super-great.
Since these are under /System, they should be available even to sandboxed apps. */
@@ -204,8 +236,11 @@ static SDL_Cursor *Cocoa_CreateSystemCursor(SDL_SystemCursor id)
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 @@ static SDL_Cursor *Cocoa_CreateDefaultCursor(void)
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;

View File

@@ -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];

View File

@@ -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;

View File

@@ -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
@@ -236,6 +297,141 @@ cleanup:
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) {
SDL_free(membase);
return NULL;
}
}
} else {
surface = frames[i].surface;
}
/* Cursor data is double height (DIB and mask), and has a max width and height of 256 (represented by a value of 0).
* https://devblogs.microsoft.com/oldnewthing/20101018-00/?p=12513
*/
ANIMICONINFO *icon_info = (ANIMICONINFO *)icon_data;
icon_info->chunkType = RIFF_FOURCC('i', 'c', 'o', 'n');
icon_info->chunkSize = sizeof(ANIMICONINFO) + image_data_size - (sizeof(DWORD) * 2);
icon_info->icon_info.idReserved = 0;
icon_info->icon_info.idType = 2;
icon_info->icon_info.idCount = 1;
icon_info->icon_info.idEntries.bWidth = w < 256 ? w : 0; // 0 means a width of 256
icon_info->icon_info.idEntries.bHeight = h < 256 ? h : 0; // 0 means a height of 256
icon_info->icon_info.idEntries.bColorCount = 0;
icon_info->icon_info.idEntries.bReserved = 0;
icon_info->icon_info.idEntries.xHotspot = hot_x;
icon_info->icon_info.idEntries.yHotspot = hot_y;
icon_info->icon_info.idEntries.dwDIBSize = image_data_size;
icon_info->icon_info.idEntries.dwDIBOffset = offsetof(ANIMICONINFO, bmi_header) - (sizeof(DWORD) * 2);
icon_info->bmi_header.biSize = sizeof(BITMAPINFOHEADER);
icon_info->bmi_header.biWidth = w;
icon_info->bmi_header.biHeight = h * 2;
icon_info->bmi_header.biPlanes = 1;
icon_info->bmi_header.biBitCount = 32;
icon_info->bmi_header.biCompression = BI_RGB;
icon_info->bmi_header.biSizeImage = 0;
icon_info->bmi_header.biXPelsPerMeter = 0;
icon_info->bmi_header.biYPelsPerMeter = 0;
icon_info->bmi_header.biClrUsed = 0;
icon_info->bmi_header.biClrImportant = 0;
icon_data += sizeof(ANIMICONINFO);
// Cursor DIB images are stored bottom-up and double height: the bitmap, and the mask
const Uint8 *pix = frames[i].surface->pixels;
pix += (frames[i].surface->h - 1) * frames[i].surface->pitch;
for (int j = 0; j < frames[i].surface->h; j++) {
SDL_memcpy(icon_data, pix, frames[i].surface->pitch);
pix -= frames[i].surface->pitch;
icon_data += frames[i].surface->pitch;
}
// Should we generate mask data here?
icon_data += (image_data_size / 2);
if (use_scaled_surfaces) {
SDL_DestroySurface(surface);
}
surface = NULL;
}
HCURSOR hcursor = (HCURSOR)CreateIconFromResource(membase, alloc_size, FALSE, 0x00030000);
SDL_free(membase);
return hcursor;
}
static SDL_Cursor *WIN_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
{
if (!SDL_SurfaceHasAlternateImages(surface)) {
@@ -256,13 +452,45 @@ static SDL_Cursor *WIN_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
}
data->hot_x = hot_x;
data->hot_y = hot_y;
data->surface = surface;
data->num_frames = 1;
data->frames[0].surface = surface;
++surface->refcount;
cursor->internal = data;
}
return cursor;
}
static SDL_Cursor *WIN_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
{
if (!SDL_SurfaceHasAlternateImages(frames[0].surface)) {
HCURSOR hcursor = WIN_CreateAnimatedCursorInternal(frames, frame_count, hot_x, hot_y, 1.0f);
if (!hcursor) {
return NULL;
}
return WIN_CreateCursorAndData(hcursor);
}
// Dynamically generate cursors at the appropriate DPI
SDL_Cursor *cursor = (SDL_Cursor *)SDL_calloc(1, sizeof(*cursor));
if (cursor) {
SDL_CursorData *data = (SDL_CursorData *)SDL_calloc(1, sizeof(*data) + (sizeof(SDL_CursorFrameInfo) * (frame_count - 1)));
if (!data) {
SDL_free(cursor);
return NULL;
}
data->hot_x = hot_x;
data->hot_y = hot_y;
data->num_frames = 1;
for (int i = 0; i < frame_count; ++i) {
data->frames[i].surface = frames[i].surface;
data->frames[i].duration = frames[i].duration;
++frames[i].surface->refcount;
}
cursor->internal = data;
}
return cursor;
}
static SDL_Cursor *WIN_CreateBlankCursor(void)
{
SDL_Cursor *cursor = NULL;
@@ -356,8 +584,8 @@ static void WIN_FreeCursor(SDL_Cursor *cursor)
{
SDL_CursorData *data = cursor->internal;
if (data->surface) {
SDL_DestroySurface(data->surface);
for (int i = 0; i < data->num_frames; ++i) {
SDL_DestroySurface(data->frames[i].surface);
}
while (data->cache) {
CachedCursor *entry = data->cache;
@@ -390,12 +618,14 @@ static HCURSOR GetCachedCursor(SDL_Cursor *cursor)
}
}
// Need to create a cursor for this content scale
SDL_Surface *surface = NULL;
HCURSOR hcursor = NULL;
CachedCursor *entry = NULL;
HCURSOR hcursor = NULL;
surface = SDL_GetSurfaceImage(data->surface, scale);
// Need to create a cursor for this content scale
if (data->num_frames == 1) {
SDL_Surface *surface = NULL;
surface = SDL_GetSurfaceImage(data->frames[0].surface, scale);
if (!surface) {
goto error;
}
@@ -403,9 +633,13 @@ static HCURSOR GetCachedCursor(SDL_Cursor *cursor)
int hot_x = (int)SDL_round(data->hot_x * scale);
int hot_y = (int)SDL_round(data->hot_y * scale);
hcursor = WIN_CreateHCursor(surface, hot_x, hot_y);
SDL_DestroySurface(surface);
if (!hcursor) {
goto error;
}
} else {
hcursor = WIN_CreateAnimatedCursorInternal(data->frames, data->num_frames, data->hot_x, data->hot_y, scale);
}
entry = (CachedCursor *)SDL_malloc(sizeof(*entry));
if (!entry) {
@@ -416,14 +650,9 @@ static HCURSOR GetCachedCursor(SDL_Cursor *cursor)
entry->next = data->cache;
data->cache = entry;
SDL_DestroySurface(surface);
return hcursor;
error:
if (surface) {
SDL_DestroySurface(surface);
}
if (hcursor) {
DestroyCursor(hcursor);
}
@@ -440,7 +669,7 @@ static bool WIN_ShowCursor(SDL_Cursor *cursor)
}
}
if (cursor) {
if (cursor->internal->surface) {
if (cursor->internal->num_frames) {
SDL_cursor = GetCachedCursor(cursor);
} else {
SDL_cursor = cursor->internal->cursor;
@@ -644,6 +873,7 @@ void WIN_InitMouse(SDL_VideoDevice *_this)
SDL_Mouse *mouse = SDL_GetMouse();
mouse->CreateCursor = WIN_CreateCursor;
mouse->CreateAnimatedCursor = WIN_CreateAnimatedCursor;
mouse->CreateSystemCursor = WIN_CreateSystemCursor;
mouse->ShowCursor = WIN_ShowCursor;
mouse->FreeCursor = WIN_FreeCursor;

View File

@@ -116,6 +116,44 @@ static Cursor X11_CreateXCursorCursor(SDL_Surface *surface, int hot_x, int hot_y
return cursor;
}
static Cursor X11_CreateAnimatedXCursorCursor(SDL_CursorFrameInfo *frames, int num_frames, int hot_x, int hot_y)
{
Display *display = GetDisplay();
Cursor cursor = None;
XcursorImage *image;
XcursorImages *images;
images = X11_XcursorImagesCreate(num_frames);
if (!images) {
SDL_OutOfMemory();
return None;
}
for (int i = 0; i < num_frames; ++i) {
image = X11_XcursorImageCreate(frames[i].surface->w, frames[i].surface->h);
if (!image) {
SDL_OutOfMemory();
goto cleanup;
}
image->xhot = hot_x;
image->yhot = hot_y;
image->delay = frames[i].duration;
SDL_assert(frames[i].surface->format == SDL_PIXELFORMAT_ARGB8888);
SDL_assert(frames[i].surface->pitch == frames[i].surface->w * 4);
SDL_memcpy(image->pixels, frames[i].surface->pixels, (size_t)frames[i].surface->h * frames[i].surface->pitch);
images->images[i] = image;
images->nimage++;
}
cursor = X11_XcursorImagesLoadCursor(display, images);
cleanup:
X11_XcursorImagesDestroy(images);
return cursor;
}
#endif // SDL_VIDEO_DRIVER_X11_XCURSOR
static Cursor X11_CreatePixmapCursor(SDL_Surface *surface, int hot_x, int hot_y)
@@ -219,6 +257,22 @@ static SDL_Cursor *X11_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
return X11_CreateCursorAndData(x11_cursor);
}
static SDL_Cursor *X11_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int num_frames, int hot_x, int hot_y)
{
Cursor x11_cursor = None;
#ifdef SDL_VIDEO_DRIVER_X11_XCURSOR
if (SDL_X11_HAVE_XCURSOR) {
x11_cursor = X11_CreateAnimatedXCursorCursor(frames, num_frames, hot_x, hot_y);
}
#endif
if (x11_cursor == None) {
x11_cursor = X11_CreatePixmapCursor(frames[0].surface, hot_x, hot_y);
}
return X11_CreateCursorAndData(x11_cursor);;
}
static unsigned int GetLegacySystemCursorShape(SDL_SystemCursor id)
{
switch (id) {
@@ -499,6 +553,7 @@ void X11_InitMouse(SDL_VideoDevice *_this)
SDL_Mouse *mouse = SDL_GetMouse();
mouse->CreateCursor = X11_CreateCursor;
mouse->CreateAnimatedCursor = X11_CreateAnimatedCursor;
mouse->CreateSystemCursor = X11_CreateSystemCursor;
mouse->ShowCursor = X11_ShowCursor;
mouse->FreeCursor = X11_FreeCursor;

View File

@@ -277,8 +277,11 @@ SDL_X11_SYM(int,ipUnallocateAndSendData,(ChannelPtr a,IPCard b))
#ifdef SDL_VIDEO_DRIVER_X11_XCURSOR
SDL_X11_MODULE(XCURSOR)
SDL_X11_SYM(XcursorImage*,XcursorImageCreate,(int a,int b))
SDL_X11_SYM(XcursorImages*,XcursorImagesCreate,(int a))
SDL_X11_SYM(void,XcursorImageDestroy,(XcursorImage *a))
SDL_X11_SYM(void,XcursorImagesDestroy,(XcursorImages *a))
SDL_X11_SYM(Cursor,XcursorImageLoadCursor,(Display *a,const XcursorImage *b))
SDL_X11_SYM(Cursor,XcursorImagesLoadCursor,(Display *a,const XcursorImages *b))
SDL_X11_SYM(Cursor,XcursorLibraryLoadCursor,(Display *a, const char *b))
#endif

View File

@@ -171,11 +171,51 @@ static SDL_Surface *load_image(const char *file)
static SDL_Cursor *init_color_cursor(const char *file)
{
SDL_Cursor *cursor = NULL;
SDL_Surface *surface = load_image(file);
if (surface) {
cursor = SDL_CreateColorCursor(surface, 0, 0);
SDL_DestroySurface(surface);
SDL_CursorFrameInfo *frames = NULL;
int frame_cnt = 0;
int i;
char *str = SDL_strdup(file);
if (!str) {
return NULL;
}
char *saveptr = NULL;
char *token = SDL_strtok_r(str, ";", &saveptr);
while(token != NULL) {
SDL_Surface *img = load_image(token);
if (!img) {
goto cleanup;
}
frames = SDL_realloc(frames, (frame_cnt + 1) * sizeof(SDL_CursorFrameInfo));
if (!frames) {
goto cleanup;
}
frames[frame_cnt].surface = img;
frames[frame_cnt++].duration = 150;
token = SDL_strtok_r(NULL, ";", &saveptr);
}
if (frame_cnt == 1) {
cursor = SDL_CreateColorCursor(frames[0].surface, 0, 0);
} else {
cursor = SDL_CreateAnimatedCursor(frames, frame_cnt, 0, 0);
}
cleanup:
if (frames) {
for (i = 0; i < frame_cnt; ++i) {
SDL_DestroySurface(frames[i].surface);
}
SDL_free(frames);
}
SDL_free(str);
return cursor;
}
@@ -217,10 +257,78 @@ static SDL_Cursor *init_system_cursor(const char *image[])
return SDL_CreateCursor(data, mask, 32, 32, hot_x, hot_y);
}
static SDL_Cursor *init_animated_cursor(const char *image[], bool oneshot)
{
int row, col;
SDL_Surface *surface, *invsurface;
Uint32 *pixels, *invpixels;
SDL_CursorFrameInfo frames[6];
int hot_x = 0;
int hot_y = 0;
surface = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_ARGB8888);
if (!surface) {
return NULL;
}
invsurface = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_ARGB8888);
if (!invsurface) {
SDL_DestroySurface(surface);
return NULL;
}
for (row = 4; row < 36; ++row) {
pixels = (Uint32 *)((Uint8 *)surface->pixels + ((row - 4) * surface->pitch));
invpixels = (Uint32 *)((Uint8 *)invsurface->pixels + ((row - 4) * surface->pitch));
for (col = 0; col < 32; ++col) {
switch (image[row][col]) {
case 'X':
pixels[col] = 0xFFFFFFFF;
invpixels[col] = 0xFF000000;
break;
case '.':
pixels[col] = 0xFF000000;
invpixels[col] = 0xFFFFFFFF;
break;
case ' ':
pixels[col] = 0;
invpixels[col] = 0;
break;
}
}
}
int frame_count = 2;
frames[0].surface = surface;
frames[0].duration = 100;
frames[1].surface = invsurface;
frames[1].duration = 100;
if (oneshot) {
frames[2].surface = surface;
frames[2].duration = 200;
frames[3].surface = invsurface;
frames[3].duration = 300;
frames[4].surface = surface;
frames[4].duration = 400;
frames[5].surface = invsurface;
frames[5].duration = 0;
frame_count = 6;
}
return SDL_CreateAnimatedCursor(frames, frame_count, hot_x, hot_y);
}
static SDLTest_CommonState *state;
static int done;
static SDL_Cursor *cursors[3 + SDL_SYSTEM_CURSOR_COUNT];
static SDL_SystemCursor cursor_types[3 + SDL_SYSTEM_CURSOR_COUNT];
static SDL_Cursor *cursors[5 + SDL_SYSTEM_CURSOR_COUNT];
static SDL_SystemCursor cursor_types[5 + SDL_SYSTEM_CURSOR_COUNT];
static int num_cursors;
static int current_cursor;
static bool show_cursor;
@@ -257,6 +365,12 @@ static void loop(void)
SDL_SetCursor(cursors[current_cursor]);
switch ((int)cursor_types[current_cursor]) {
case (SDL_SystemCursor)-3:
SDL_Log("Animated custom cursor (one-shot)");
break;
case (SDL_SystemCursor)-2:
SDL_Log("Animated custom cursor");
break;
case (SDL_SystemCursor)-1:
SDL_Log("Custom cursor");
break;
@@ -405,13 +519,22 @@ int main(int argc, char *argv[])
num_cursors = 0;
if (color_cursor) {
SDL_Surface *icon = load_image(color_cursor);
/* Only load the first file in the list for the icon. */
char *icon_str = SDL_strdup(color_cursor);
if (icon_str) {
char *tok = SDL_strchr(icon_str, ';');
if (tok) {
*tok = '\0';
}
SDL_Surface *icon = load_image(icon_str);
SDL_free(icon_str);
if (icon) {
for (i = 0; i < state->num_windows; ++i) {
SDL_SetWindowIcon(state->windows[i], icon);
}
SDL_DestroySurface(icon);
}
}
cursor = init_color_cursor(color_cursor);
if (cursor) {
@@ -435,6 +558,20 @@ int main(int argc, char *argv[])
num_cursors++;
}
cursor = init_animated_cursor(arrow, false);
if (cursor) {
cursors[num_cursors] = cursor;
cursor_types[num_cursors] = (SDL_SystemCursor)-2;
num_cursors++;
}
cursor = init_animated_cursor(arrow, true);
if (cursor) {
cursors[num_cursors] = cursor;
cursor_types[num_cursors] = (SDL_SystemCursor)-3;
num_cursors++;
}
for (i = 0; i < SDL_SYSTEM_CURSOR_COUNT; ++i) {
cursor = SDL_CreateSystemCursor((SDL_SystemCursor)i);
if (cursor) {