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 185757aee9..d8489bcb86 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 387f911404..c73f5b72c8 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 5013d23c74..7b1aecc2f4 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 b42024ba5b..b9f177a970 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 zcmZ?royNfc2GbZA7-|?87#yIOk%0v)&cMOIkjo9h%nS?+Ak4t#FK(hfGZ%N(t_CK1qGF$KoF}60`ef;5DS!)l&}~;KweNu38EWB zVljY#Jc=~PrD!f7APMAPUKY2>U@2P)u!kNWy_6Bajv7<`b4@0J~FA5K1Gm392}zJXiyo`AF6?5G4W`!2}K?mnum_windows; ++i) { + SDL_SetWindowIcon(state->windows[i], icon); + } + SDL_DestroySurface(icon); + } + cursor = init_color_cursor(color_cursor); if (cursor) { cursors[num_cursors] = cursor;