From 6381d7ed20f1ed22585bc9012d277b98c3b95662 Mon Sep 17 00:00:00 2001 From: ws909 <37029098+ws909@users.noreply.github.com> Date: Thu, 9 Mar 2023 16:30:43 +0100 Subject: [PATCH] Update glfwSetWindowIcon to support application icons and icon size queries New MacOS implementation for glfwSetWindowIcon New test program: animate-icon Documentation updates for glfwSetWindowIcon --- CONTRIBUTORS.md | 1 + README.md | 1 + docs/window.dox | 17 ++++ include/GLFW/glfw3.h | 41 ++++++--- src/cocoa_window.m | 97 +++++++++++++++------ src/internal.h | 2 + src/win32_window.c | 26 +----- src/window.c | 23 ++++- tests/CMakeLists.txt | 3 +- tests/animate-icon.c | 202 +++++++++++++++++++++++++++++++++++++++++++ tests/icon.c | 2 + 11 files changed, 351 insertions(+), 64 deletions(-) create mode 100644 tests/animate-icon.c diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 020bb26e..fb590e3a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -98,6 +98,7 @@ video tutorials. - IntellectualKitty - Aaron Jacobs - JannikGM + - Andreas O. Jansen - Erik S. V. Jansson - jjYBdx4IL - Toni Jovanoski diff --git a/README.md b/README.md index 8b4a1546..61e32b30 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ information on what to include when reporting a bug. ## Changelog + - Updated `glfwSetWindowIcon` to set the MacOS Dock icon, and query for an optimal image size (#2041) - Added `GLFW_PLATFORM` init hint for runtime platform selection (#1958) - Added `GLFW_ANY_PLATFORM`, `GLFW_PLATFORM_WIN32`, `GLFW_PLATFORM_COCOA`, `GLFW_PLATFORM_WAYLAND`, `GLFW_PLATFORM_X11` and `GLFW_PLATFORM_NULL` symbols to diff --git a/docs/window.dox b/docs/window.dox index 3cec6358..560c7fa3 100644 --- a/docs/window.dox +++ b/docs/window.dox @@ -902,6 +902,23 @@ sequential rows, starting from the top-left corner. To revert to the default window icon, pass in an empty image array. +On Windows and X11, the window's title bar and taskbar icons are set with this method. +On MacOS, a `NULL` window handle may be passed to set the application's Dock icon. + +Some platforms support querying an optimal size for its icons. +You can ask for this size by passing a `NULL` window handle, a count of zero, +and a valid pointer to an image with its `pixels` field set to `NULL`: + +@code +GLFWimage image = { DEFAULT_WIDTH, DEFAULT_HEIGHT, NULL }; +glfwSetWindowIcon(NULL, 0, &image); + +image.pixels = generatePixels(image.width, image.height); + +glfwSetWindowIcon(window, 1, &image); +free(image.pixels); +@endcode + @code glfwSetWindowIcon(window, 0, NULL); @endcode diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index d4b40dd4..6b74d1db 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -3256,12 +3256,12 @@ GLFWAPI void glfwSetWindowShouldClose(GLFWwindow* window, int value); */ GLFWAPI void glfwSetWindowTitle(GLFWwindow* window, const char* title); -/*! @brief Sets the icon for the specified window. +/*! @brief Sets the icon for the specified window or application. * - * This function sets the icon of the specified window. If passed an array of - * candidate images, those of or closest to the sizes desired by the system are - * selected. If no images are specified, the window reverts to its default - * icon. + * This function sets the icon of the specified window or application. If passed an + * array of candidate images, those of or closest to the sizes desired by the system + * are selected. If no images are specified, the window or application reverts to its + * default icon. * * The pixels are 32-bit, little-endian, non-premultiplied RGBA, i.e. eight * bits per channel with the red channel first. They are arranged canonically @@ -3269,9 +3269,17 @@ GLFWAPI void glfwSetWindowTitle(GLFWwindow* window, const char* title); * * The desired image sizes varies depending on platform and system settings. * The selected images will be rescaled as needed. Good sizes include 16x16, - * 32x32 and 48x48. + * 32x32, 48x48, 64x64 and 128x128. * - * @param[in] window The window whose icon to set. + * If passed a `NULL` window handle, a count of zero, and a valid pointer to an + * image with its `pixels` field set to `NULL`, GLFW will set that image's + * `width` and `height` fields to the optimal icon size for the current platform, + * if these can be retrieved. If they can not be retrieved by GLFW, they are left + * untouched. Doing this requires an internal const-cast for the `images` parameter. + * This parameter remains qualified with `const` for backwards-compatability. + * See @ref window_icon for an example. + * + * @param[in] window The window whose icon to set, or `NULL` for the application's icon. * @param[in] count The number of images in the specified array, or zero to * revert to the default window icon. * @param[in] images The images to create the icon from. This is ignored if @@ -3284,13 +3292,22 @@ GLFWAPI void glfwSetWindowTitle(GLFWwindow* window, const char* title); * @pointer_lifetime The specified image data is copied before this function * returns. * - * @remark @macos Regular windows do not have icons on macOS. This function - * will emit @ref GLFW_FEATURE_UNAVAILABLE. The dock icon will be the same as - * the application bundle's icon. For more information on bundles, see the - * [Bundle Programming Guide](https://developer.apple.com/library/mac/documentation/CoreFoundation/Conceptual/CFBundles/) + * @remark Every platform has either a window icon, or an application icon. + * To cover all platforms, you need to set both. + * + * @remark @macos The Dock icon defaults to the application bundle's icon. + * For more information on bundles, see the [Bundle Programming Guide](https://developer.apple.com/library/mac/documentation/CoreFoundation/Conceptual/CFBundles/) * in the Mac Developer Library. * - * @remark @wayland There is no existing protocol to change an icon, the + * @remark @wayland @x11 The default icon is specified in the application's + * desktop file. + * + * @remark @macos Regular windows do not have icons on macOS. This function + * will emit @ref GLFW_FEATURE_UNAVAILABLE if a valid window handle is passed. + * Pass a `NULL` window handle to set the Dock icon. Otherwise, the dock icon + * will be the same as the application bundle's icon. + * + * @remark @wayland There is no existing protocol to change an icon; the * window will thus inherit the one defined in the application's desktop file. * This function will emit @ref GLFW_FEATURE_UNAVAILABLE. * diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 6f8aa978..489d6182 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -193,6 +193,35 @@ static NSUInteger translateKeyToModifierFlag(int key) return 0; } +// Converts a GLFWimage to an NSImage. The returned image must be explicitly freed. +// +static NSImage* imageToNative(const GLFWimage* image) +{ + NSBitmapImageRep* representation = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:image->width + pixelsHigh:image->height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bitmapFormat:NSBitmapFormatAlphaNonpremultiplied + bytesPerRow:image->width * 4 + bitsPerPixel:32]; + + if (representation == nil) + return nil; + + memcpy([representation bitmapData], image->pixels, image->width * image->height * 4); + + NSImage* native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)]; + [native addRepresentation:representation]; + [representation release]; + + return native; +} + // Defines a constant for empty ranges in NSTextInputClient // static const NSRange kEmptyRange = { NSNotFound, 0 }; @@ -1028,8 +1057,44 @@ void _glfwSetWindowTitleCocoa(_GLFWwindow* window, const char* title) void _glfwSetWindowIconCocoa(_GLFWwindow* window, int count, const GLFWimage* images) { - _glfwInputError(GLFW_FEATURE_UNAVAILABLE, - "Cocoa: Regular windows do not have icons on macOS"); + if (window != NULL) + { + _glfwInputError(GLFW_FEATURE_UNAVAILABLE, + "Cocoa: Regular windows do not have icons on macOS"); + return; + } + + // This is the actual pixel storage size of the dock, which is what we're after here. + NSSize preferredSize = [NSApp dockTile].size; + + if (count == 0 && images != NULL && images->pixels == NULL) + { + // Const-cast + ((GLFWimage*) images)->width = preferredSize.width; + ((GLFWimage*) images)->height = preferredSize.height; + return; + } + + if (count == 0) + { + NSApp.applicationIconImage = nil; + return; + } + + const GLFWimage* image = _glfwChooseImage(count, images, preferredSize.width, preferredSize.height); + + assert(image->pixels != NULL); + + NSImage* native = imageToNative(image); + + if (native == nil) + { + _glfwInputError(GLFW_PLATFORM_ERROR, NULL); + return; + } + + NSApp.applicationIconImage = native; + [native release]; } void _glfwGetWindowPosCocoa(_GLFWwindow* window, int* xpos, int* ypos) @@ -1710,35 +1775,15 @@ GLFWbool _glfwCreateCursorCocoa(_GLFWcursor* cursor, { @autoreleasepool { - NSImage* native; - NSBitmapImageRep* rep; - - rep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL - pixelsWide:image->width - pixelsHigh:image->height - bitsPerSample:8 - samplesPerPixel:4 - hasAlpha:YES - isPlanar:NO - colorSpaceName:NSCalibratedRGBColorSpace - bitmapFormat:NSBitmapFormatAlphaNonpremultiplied - bytesPerRow:image->width * 4 - bitsPerPixel:32]; - - if (rep == nil) + NSImage* native = imageToNative(image); + + if (native == nil) return GLFW_FALSE; - - memcpy([rep bitmapData], image->pixels, image->width * image->height * 4); - - native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)]; - [native addRepresentation:rep]; - + cursor->ns.object = [[NSCursor alloc] initWithImage:native hotSpot:NSMakePoint(xhot, yhot)]; [native release]; - [rep release]; if (cursor->ns.object == nil) return GLFW_FALSE; diff --git a/src/internal.h b/src/internal.h index 781c8cdc..c9b6423d 100644 --- a/src/internal.h +++ b/src/internal.h @@ -1007,3 +1007,5 @@ void* _glfw_calloc(size_t count, size_t size); void* _glfw_realloc(void* pointer, size_t size); void _glfw_free(void* pointer); +const GLFWimage* _glfwChooseImage(int count, const GLFWimage* images, + int width, int height); diff --git a/src/win32_window.c b/src/win32_window.c index 676640bf..c12edcfd 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -75,28 +75,6 @@ static DWORD getWindowExStyle(const _GLFWwindow* window) return style; } -// Returns the image whose area most closely matches the desired one -// -static const GLFWimage* chooseImage(int count, const GLFWimage* images, - int width, int height) -{ - int i, leastDiff = INT_MAX; - const GLFWimage* closest = NULL; - - for (i = 0; i < count; i++) - { - const int currDiff = abs(images[i].width * images[i].height - - width * height); - if (currDiff < leastDiff) - { - closest = images + i; - leastDiff = currDiff; - } - } - - return closest; -} - // Creates an RGBA icon or cursor // static HICON createIcon(const GLFWimage* image, int xhot, int yhot, GLFWbool icon) @@ -1545,10 +1523,10 @@ void _glfwSetWindowIconWin32(_GLFWwindow* window, int count, const GLFWimage* im if (count) { - const GLFWimage* bigImage = chooseImage(count, images, + const GLFWimage* bigImage = _glfwChooseImage(count, images, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); - const GLFWimage* smallImage = chooseImage(count, images, + const GLFWimage* smallImage = _glfwChooseImage(count, images, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); diff --git a/src/window.c b/src/window.c index 1c8519ff..3c5e53cf 100644 --- a/src/window.c +++ b/src/window.c @@ -175,6 +175,28 @@ void _glfwInputWindowMonitor(_GLFWwindow* window, _GLFWmonitor* monitor) window->monitor = monitor; } +// Returns the image whose area most closely matches the desired one +// +const GLFWimage* _glfwChooseImage(int count, const GLFWimage* images, + int width, int height) +{ + int i, leastDiff = INT_MAX; + const GLFWimage* closest = NULL; + + for (i = 0; i < count; i++) + { + const int currDiff = abs(images[i].width * images[i].height - + width * height); + if (currDiff < leastDiff) + { + closest = images + i; + leastDiff = currDiff; + } + } + + return closest; +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW public API ////// ////////////////////////////////////////////////////////////////////////// @@ -531,7 +553,6 @@ GLFWAPI void glfwSetWindowIcon(GLFWwindow* handle, int i; _GLFWwindow* window = (_GLFWwindow*) handle; - assert(window != NULL); assert(count >= 0); assert(count == 0 || images != NULL); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f81cfeb9..e4bdeded 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(monitors monitors.c ${GETOPT} ${GLAD_GL}) add_executable(reopen reopen.c ${GLAD_GL}) add_executable(cursor cursor.c ${GLAD_GL}) +add_executable(animate-icon WIN32 MACOSX_BUNDLE animate-icon.c ${GLAD_GL}) add_executable(empty WIN32 MACOSX_BUNDLE empty.c ${TINYCTHREAD} ${GLAD_GL}) add_executable(gamma WIN32 MACOSX_BUNDLE gamma.c ${GLAD_GL}) add_executable(icon WIN32 MACOSX_BUNDLE icon.c ${GLAD_GL}) @@ -48,7 +49,7 @@ if (RT_LIBRARY) target_link_libraries(threads "${RT_LIBRARY}") endif() -set(GUI_ONLY_BINARIES empty gamma icon inputlag joysticks tearing threads +set(GUI_ONLY_BINARIES animate-icon empty gamma icon inputlag joysticks tearing threads timeout title triangle-vulkan window) set(CONSOLE_BINARIES allocator clipboard events msaa glfwinfo iconify monitors reopen cursor) diff --git a/tests/animate-icon.c b/tests/animate-icon.c new file mode 100644 index 00000000..bac61162 --- /dev/null +++ b/tests/animate-icon.c @@ -0,0 +1,202 @@ +//======================================================================== +// Window icon animation test program +// Copyright (c) Camilla Löwy +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would +// be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not +// be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source +// distribution. +// +//======================================================================== +// +// This program is used to test the icon feature. +// +//======================================================================== + +#define GLAD_GL_IMPLEMENTATION +#include +#define GLFW_INCLUDE_NONE +#include + +#include +#include +#include +#include +#include + +#define ALPHA 220 +#define WIDTH 3 +#define HEIGHT 3 +#define SPEED 0.3 + +const unsigned char iconColors[5][3] = +{ + { 1, 0, 0 }, // Red + { 0, 1, 0 }, // Green + { 0, 0, 1 }, // Blue + { 1, 1, 1 } // All +}; + +static int currentIconColor = -1; +static int animate = GLFW_FALSE; +static int smooth = GLFW_FALSE; +static int useOptimalSize = GLFW_FALSE; +static int width = WIDTH, height = HEIGHT; + +static void error_callback(int error_code, const char* description) +{ + fprintf(stderr, "Error %i: %s\n", error_code, description); +} + +static void set_icon(GLFWwindow* window, int iconColor) +{ + int x, y; + double time = glfwGetTime(); + + double red = (cos(SPEED * time * M_PI ) + 1.0) / 2.0; + double green = (cos(SPEED * time * M_PI * 1.5) + 1.0) / 2.0; + double blue = (cos(SPEED * time * M_PI * 2.0) + 1.0) / 2.0; + + unsigned char pixels[width * height * 4]; + + for (x = 0; x < width; ++x) + { + for (y = 0; y < height; ++y) + { + int offset = x * height * 4 + y * 4; + + pixels[offset + 0] = iconColors[currentIconColor][0] * (char) (255.0 * red); + pixels[offset + 1] = iconColors[currentIconColor][1] * (char) (255.0 * green); + pixels[offset + 2] = iconColors[currentIconColor][2] * (char) (255.0 * blue); + pixels[offset + 3] = (char) (255.0 * ((double) (x + y) / (double) (width + height))); + } + } + + GLFWimage image = { width, height, pixels }; + + glfwSetErrorCallback(NULL); + + glfwSetWindowIcon(window, 1, &image); + glfwSetWindowIcon(0, 1, &image); + + glfwSetErrorCallback(error_callback); +} + +static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) +{ + if (action != GLFW_PRESS) + return; + + switch (key) + { + case GLFW_KEY_ESCAPE: + glfwSetWindowShouldClose(window, GLFW_TRUE); + break; + case GLFW_KEY_SPACE: + currentIconColor = (currentIconColor + 1) % 4; + animate = GLFW_TRUE; + break; + case GLFW_KEY_S: + smooth = !smooth; + break; + case GLFW_KEY_O: + { + GLFWimage image = { GLFW_DONT_CARE, GLFW_DONT_CARE, NULL }; + glfwSetWindowIcon(NULL, 0, &image); + printf("Optimal size: [%i, %i]\n", image.width, image.height); + useOptimalSize = !useOptimalSize; + break; + } + case GLFW_KEY_X: + glfwSetWindowIcon(window, 0, NULL); + glfwSetWindowIcon(0, 0, NULL); + currentIconColor = -1; + animate = GLFW_FALSE; + break; + } +} + +int main(int argc, char** argv) +{ + GLFWwindow* window; + + glfwSetErrorCallback(error_callback); + + if (!glfwInit()) + { + fprintf(stderr, "Failed to initialize GLFW\n"); + exit(EXIT_FAILURE); + } + + window = glfwCreateWindow(200, 200, "Window Icon", NULL, NULL); + if (!window) + { + glfwTerminate(); + + fprintf(stderr, "Failed to open GLFW window\n"); + exit(EXIT_FAILURE); + } + + glfwMakeContextCurrent(window); + gladLoadGL(glfwGetProcAddress); + glfwSwapInterval(60); + + glfwSetKeyCallback(window, key_callback); + + double lastTime = 0.0; + while (!glfwWindowShouldClose(window)) + { + + if (useOptimalSize) + { + GLFWimage image = { GLFW_DONT_CARE, GLFW_DONT_CARE, NULL }; + glfwSetWindowIcon(NULL, 0, &image); + + if (image.width != GLFW_DONT_CARE) + { + assert(image.height != GLFW_DONT_CARE); + + width = image.width; + height = image.height; + } + } + else + { + width = WIDTH; + height = HEIGHT; + } + + double time = glfwGetTime(); + if (animate && (time - lastTime) > (smooth ? 0.01 : 0.15)) + { + set_icon(window, currentIconColor); + lastTime = time; + } + glClear(GL_COLOR_BUFFER_BIT); + glfwSwapBuffers(window); + + glfwPollEvents(); + } + + glfwDestroyWindow(window); + glfwTerminate(); + exit(EXIT_SUCCESS); +} + + +// TODO: rollback all changes in this branch. Create a new competing branch to set-application-icon, add this file's content as a new test program (animateIcon?) +// Do not add glfwSetApplicationIcon in the new one. Rather, use glfwSetWindowIcon with NULL handle, and add support for getting an optimal max size. diff --git a/tests/icon.c b/tests/icon.c index d5baf0a0..2159cceb 100644 --- a/tests/icon.c +++ b/tests/icon.c @@ -89,6 +89,7 @@ static void set_icon(GLFWwindow* window, int icon_color) } glfwSetWindowIcon(window, 1, &img); + glfwSetWindowIcon(NULL, 1, &img); } static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) @@ -107,6 +108,7 @@ static void key_callback(GLFWwindow* window, int key, int scancode, int action, break; case GLFW_KEY_X: glfwSetWindowIcon(window, 0, NULL); + glfwSetWindowIcon(NULL, 0, NULL); break; } }