diff --git a/include/SDL3/SDL_mouse.h b/include/SDL3/SDL_mouse.h index 4ecd1d0010..8ca05703a3 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 f916b8f5f5..b779befedd 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 0c0806f0b4..b19ddb8c24 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 72d1b83be6..654f617534 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 95e31b2ffe..1aa4112efb 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 0114f2da84..6e049fe841 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 70282be9fa..4ea7b27ed0 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 2c210e9c5c..b0974a48b2 100644 --- a/src/video/cocoa/SDL_cocoamouse.m +++ b/src/video/cocoa/SDL_cocoamouse.m @@ -66,27 +66,59 @@ } @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)]; - } - - if (nscursor) { - cursor = SDL_calloc(1, sizeof(*cursor)); - if (cursor) { - cursor->internal = (void *)CFBridgingRetain(nscursor); + 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; } - } - return cursor; + 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 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 @@ 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; diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m index 2fee316dfc..3425737929 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 3ff173c70e..6bfb0079b8 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 63970ac943..eead6d4881 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 @@ 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,21 +618,27 @@ 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); - if (!surface) { - goto error; - } + // Need to create a cursor for this content scale + if (data->num_frames == 1) { + SDL_Surface *surface = NULL; - 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); - if (!hcursor) { - goto error; + surface = SDL_GetSurfaceImage(data->frames[0].surface, scale); + if (!surface) { + goto error; + } + + 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)); @@ -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; diff --git a/src/video/x11/SDL_x11mouse.c b/src/video/x11/SDL_x11mouse.c index 8b468675f6..1e12a622eb 100644 --- a/src/video/x11/SDL_x11mouse.c +++ b/src/video/x11/SDL_x11mouse.c @@ -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; diff --git a/src/video/x11/SDL_x11sym.h b/src/video/x11/SDL_x11sym.h index ba11faedee..62c3e00237 100644 --- a/src/video/x11/SDL_x11sym.h +++ b/src/video/x11/SDL_x11sym.h @@ -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 diff --git a/test/testcustomcursor.c b/test/testcustomcursor.c index b879528204..e88f40e559 100644 --- a/test/testcustomcursor.c +++ b/test/testcustomcursor.c @@ -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,12 +519,21 @@ int main(int argc, char *argv[]) num_cursors = 0; if (color_cursor) { - SDL_Surface *icon = load_image(color_cursor); - if (icon) { - for (i = 0; i < state->num_windows; ++i) { - SDL_SetWindowIcon(state->windows[i], icon); + /* 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); } - SDL_DestroySurface(icon); } cursor = init_color_cursor(color_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) {