From 31ed3665adcbba20d3d0eecb7259ccbc3bc1c31f Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 1 Aug 2024 11:38:17 -0700 Subject: [PATCH] Added support for high-DPI cursors and icons Fixes https://github.com/libsdl-org/SDL/issues/9838 --- include/SDL3/SDL_mouse.h | 2 + include/SDL3/SDL_video.h | 2 + src/video/cocoa/SDL_cocoavideo.m | 75 ++++++++++-------- src/video/windows/SDL_windowsmouse.c | 111 +++++++++++++++++++++++++-- test/icon2x.bmp | Bin 0 -> 2198 bytes test/testcustomcursor.c | 52 +++++++++++-- 6 files changed, 200 insertions(+), 42 deletions(-) create mode 100644 test/icon2x.bmp diff --git a/include/SDL3/SDL_mouse.h b/include/SDL3/SDL_mouse.h index 185757aee..d8489bcb8 100644 --- a/include/SDL3/SDL_mouse.h +++ b/include/SDL3/SDL_mouse.h @@ -413,6 +413,8 @@ extern SDL_DECLSPEC SDL_Cursor * SDLCALL SDL_CreateCursor(const Uint8 * data, /** * Create a color cursor. * + * If this function is passed a surface with alternate representations, the surface 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 surface is 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 size image will be scaled to the appropriate size and be used instead. + * * \param surface an SDL_Surface structure representing the cursor image. * \param hot_x the x position of the cursor hot spot. * \param hot_y the y position of the cursor hot spot. diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 387f91140..c73f5b72c 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1334,6 +1334,8 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetWindowTitle(SDL_Window *window); /** * Set the icon for a window. * + * If this function is passed a surface with alternate representations, the surface 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 surface is 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 size image will be scaled to the appropriate size and be used instead. + * * \param window the window to change. * \param icon an SDL_Surface structure containing the icon for the window. * \returns 0 on success or a negative error code on failure; call diff --git a/src/video/cocoa/SDL_cocoavideo.m b/src/video/cocoa/SDL_cocoavideo.m index 5013d23c7..7b1aecc2f 100644 --- a/src/video/cocoa/SDL_cocoavideo.m +++ b/src/video/cocoa/SDL_cocoavideo.m @@ -250,43 +250,54 @@ SDL_SystemTheme Cocoa_GetSystemTheme(void) /* This function assumes that it's called from within an autorelease pool */ NSImage *Cocoa_CreateImage(SDL_Surface *surface) { - SDL_Surface *converted; - NSBitmapImageRep *imgrep; - Uint8 *pixels; NSImage *img; - converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32); - if (!converted) { - return nil; - } - - /* Premultiply the alpha channel */ - SDL_PremultiplySurfaceAlpha(converted, SDL_FALSE); - - imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL - pixelsWide:converted->w - pixelsHigh:converted->h - bitsPerSample:8 - samplesPerPixel:4 - hasAlpha:YES - isPlanar:NO - colorSpaceName:NSDeviceRGBColorSpace - bytesPerRow:converted->pitch - bitsPerPixel:SDL_BITSPERPIXEL(converted->format)]; - if (imgrep == nil) { - SDL_DestroySurface(converted); - return nil; - } - - /* Copy the pixels */ - pixels = [imgrep bitmapData]; - SDL_memcpy(pixels, converted->pixels, (size_t)converted->h * converted->pitch); - SDL_DestroySurface(converted); - img = [[NSImage alloc] initWithSize:NSMakeSize(surface->w, surface->h)]; - if (img != nil) { + if (img == nil) { + return nil; + } + + SDL_Surface **images = SDL_GetSurfaceImages(surface, NULL); + if (!images) { + return nil; + } + + for (int i = 0; images[i]; ++i) { + SDL_Surface *converted = SDL_ConvertSurface(images[i], SDL_PIXELFORMAT_RGBA32); + if (!converted) { + SDL_free(images); + return nil; + } + + /* Premultiply the alpha channel */ + SDL_PremultiplySurfaceAlpha(converted, SDL_FALSE); + + NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL + pixelsWide:converted->w + pixelsHigh:converted->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:converted->pitch + bitsPerPixel:SDL_BITSPERPIXEL(converted->format)]; + if (imgrep == nil) { + SDL_free(images); + SDL_DestroySurface(converted); + return nil; + } + + /* Copy the pixels */ + Uint8 *pixels = [imgrep bitmapData]; + SDL_memcpy(pixels, converted->pixels, (size_t)converted->h * converted->pitch); + SDL_DestroySurface(converted); + + /* Add the image representation */ [img addRepresentation:imgrep]; } + SDL_free(images); + return img; } diff --git a/src/video/windows/SDL_windowsmouse.c b/src/video/windows/SDL_windowsmouse.c index b42024ba5..b9f177a97 100644 --- a/src/video/windows/SDL_windowsmouse.c +++ b/src/video/windows/SDL_windowsmouse.c @@ -27,12 +27,24 @@ #include "SDL_windowsrawinput.h" #include "../SDL_video_c.h" +#include "../SDL_blit.h" #include "../../events/SDL_mouse_c.h" #include "../../joystick/usb_ids.h" +typedef struct CachedCursor +{ + float scale; + HCURSOR cursor; + struct CachedCursor *next; +} CachedCursor; + struct SDL_CursorData { + SDL_Surface *surface; + int hot_x; + int hot_y; + CachedCursor *cache; HCURSOR cursor; }; @@ -189,6 +201,7 @@ static HCURSOR WIN_CreateHCursor(SDL_Surface *surface, int hot_x, int hot_y) ii.hbmColor = is_monochrome ? NULL : CreateColorBitmap(surface); if (!ii.hbmMask || (!is_monochrome && !ii.hbmColor)) { + SDL_SetError("Couldn't create cursor bitmaps"); return NULL; } @@ -208,11 +221,29 @@ static HCURSOR WIN_CreateHCursor(SDL_Surface *surface, int hot_x, int hot_y) static SDL_Cursor *WIN_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y) { - HCURSOR hcursor = WIN_CreateHCursor(surface, hot_x, hot_y); - if (!hcursor) { - return NULL; + if (!SDL_SurfaceHasAlternateImages(surface)) { + HCURSOR hcursor = WIN_CreateHCursor(surface, hot_x, hot_y); + if (!hcursor) { + return NULL; + } + return WIN_CreateCursorAndData(hcursor); } - 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)); + if (!data) { + SDL_free(cursor); + return NULL; + } + data->hot_x = hot_x; + data->hot_y = hot_y; + data->surface = surface; + ++surface->refcount; + cursor->internal = data; + } + return cursor; } static SDL_Cursor *WIN_CreateBlankCursor(void) @@ -302,6 +333,15 @@ static void WIN_FreeCursor(SDL_Cursor *cursor) { SDL_CursorData *data = cursor->internal; + if (data->surface) { + SDL_DestroySurface(data->surface); + } + while (data->cache) { + CachedCursor *entry = data->cache; + data->cache = entry->next; + DestroyCursor(entry->cursor); + SDL_free(entry); + } if (data->cursor) { DestroyCursor(data->cursor); } @@ -309,13 +349,74 @@ static void WIN_FreeCursor(SDL_Cursor *cursor) SDL_free(cursor); } +static HCURSOR GetCachedCursor(SDL_Cursor *cursor) +{ + SDL_CursorData *data = cursor->internal; + + SDL_Window *focus = SDL_GetMouseFocus(); + if (!focus) { + return NULL; + } + + float scale = SDL_GetDisplayContentScale(SDL_GetDisplayForWindow(focus)); + for (CachedCursor *entry = data->cache; entry; entry = entry->next) { + if (scale == entry->scale) { + return entry->cursor; + } + } + + // Need to create a cursor for this content scale + SDL_Surface *surface = NULL; + HCURSOR hcursor = NULL; + CachedCursor *entry = NULL; + + surface = SDL_GetSurfaceImage(data->surface, scale); + if (!surface) { + goto error; + } + + int hot_x = (int)SDL_round(data->hot_x * scale); + int hot_y = (int)SDL_round(data->hot_x * scale); + hcursor = WIN_CreateHCursor(surface, hot_x, hot_y); + if (!hcursor) { + goto error; + } + + entry = (CachedCursor *)SDL_malloc(sizeof(*entry)); + if (!entry) { + goto error; + } + entry->cursor = hcursor; + entry->scale = scale; + entry->next = data->cache; + data->cache = entry; + + SDL_DestroySurface(surface); + + return hcursor; + +error: + if (surface) { + SDL_DestroySurface(surface); + } + if (hcursor) { + DestroyCursor(hcursor); + } + SDL_free(entry); + return NULL; +} + static int WIN_ShowCursor(SDL_Cursor *cursor) { if (!cursor) { cursor = SDL_blank_cursor; } if (cursor) { - SDL_cursor = cursor->internal->cursor; + if (cursor->internal->surface) { + SDL_cursor = GetCachedCursor(cursor); + } else { + SDL_cursor = cursor->internal->cursor; + } } else { SDL_cursor = NULL; } diff --git a/test/icon2x.bmp b/test/icon2x.bmp new file mode 100644 index 0000000000000000000000000000000000000000..21e71860ee2d877652926d09bc1b258619d1ec7e GIT binary patch literal 2198 zcmeH{I}XAy3_#6iKw`=jI0@>)#KJw8x^j+Qx7cx9JD)b8hyj7s#7>_-aZ<5%6~)_dcHX`A?tBjzL#&8wfEU68qhSmw^1f;d;o zP~jBK1<1$~R5)VU9u>+GRCt672`Zei_D^fuLIamz(P_NA7i~GF&&4>jbG;JSM|BZP1d?_zo6T>518t5e$zfQanrhQL(6mg66YOTkEnbhLBi7n mDX#L^Aq!7-P~f1S=Bob>9v9sJi9I=MGo9zw_ium42|NK=kY67F literal 0 HcmV?d00001 diff --git a/test/testcustomcursor.c b/test/testcustomcursor.c index 6a4910fa8..79cb20cca 100644 --- a/test/testcustomcursor.c +++ b/test/testcustomcursor.c @@ -109,10 +109,8 @@ static const char *cross[] = { "0,0" }; -static SDL_Cursor * -init_color_cursor(const char *file) +static SDL_Surface *load_image_file(const char *file) { - SDL_Cursor *cursor = NULL; SDL_Surface *surface = SDL_LoadBMP(file); if (surface) { if (SDL_GetSurfacePalette(surface)) { @@ -138,14 +136,50 @@ init_color_cursor(const char *file) break; } } + } + return surface; +} + +static SDL_Surface *load_image(const char *file) +{ + SDL_Surface *surface = load_image_file(file); + if (surface) { + /* Add a 2x version of this image, if available */ + SDL_Surface *surface2x = NULL; + const char *ext = SDL_strrchr(file, '.'); + size_t len = SDL_strlen(file) + 2 + 1; + char *file2x = (char *)SDL_malloc(len); + if (file2x) { + SDL_strlcpy(file2x, file, len); + if (ext) { + SDL_memcpy(file2x + (ext - file), "2x", 3); + SDL_strlcat(file2x, ext, len); + } else { + SDL_strlcat(file2x, "2x", len); + } + surface2x = load_image_file(file2x); + SDL_free(file2x); + } + if (surface2x) { + SDL_AddSurfaceAlternateImage(surface, surface2x); + SDL_DestroySurface(surface2x); + } + } + return surface; +} + +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); } return cursor; } -static SDL_Cursor * -init_system_cursor(const char *image[]) +static SDL_Cursor *init_system_cursor(const char *image[]) { int i, row, col; Uint8 data[4 * 32]; @@ -373,6 +407,14 @@ 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); + } + SDL_DestroySurface(icon); + } + cursor = init_color_cursor(color_cursor); if (cursor) { cursors[num_cursors] = cursor;