diff options
| author | Andre Weissflog <floooh@gmail.com> | 2021-04-09 18:44:16 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-04-09 18:44:16 +0200 |
| commit | 2c05e244e48a032676d22e2c1a33399b31a02f84 (patch) | |
| tree | 28ab138dc5be7da70bcf3eb33aa92fa1d1f1dc3e /sokol_app.h | |
| parent | 91e7e83c96c0964f4eabf4a4f0d675984f57b379 (diff) | |
Window icon support for sokol_app.h (#509)
Diffstat (limited to 'sokol_app.h')
| -rw-r--r-- | sokol_app.h | 678 |
1 files changed, 623 insertions, 55 deletions
diff --git a/sokol_app.h b/sokol_app.h index 98e84f59..a7e34020 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -54,8 +54,8 @@ For example code, see https://github.com/floooh/sokol-samples/tree/master/sapp - Portions of the Windows and Linux GL initialization and event code have been - taken from GLFW (http://www.glfw.org/) + Portions of the Windows and Linux GL initialization, event-, icon- etc... code + have been taken from GLFW (http://www.glfw.org/) iOS onscreen keyboard support 'inspired' by libgdx. @@ -135,14 +135,7 @@ clipboard | YES | YES | TODO | --- | --- | TODO | --- | YES MSAA | YES | YES | YES | YES | YES | TODO | TODO | YES drag'n'drop | YES | YES | YES | --- | --- | TODO | TODO | YES - - TODO - ==== - - Linux: - - clipboard support - - UWP: - - clipboard, mouselock - - sapp_consume_event() on non-web platforms? + window icon | YES | --- | YES | --- | --- | TODO | TODO | YES STEP BY STEP ============ @@ -776,6 +769,113 @@ To check if the application window is currently in fullscreen mode, call sapp_is_fullscreen(). + WINDOW ICON SUPPORT + =================== + Some sokol_app.h backends allow to change the window icon programmatically: + + - on Win32: the small icon in the window's title bar, and the + bigger icon in the task bar + - on Linux: highly dependent on the used window manager, but usually + the window's title bar icon and/or the task bar icon + - on HTML5: the favicon shown in the page's browser tab + + NOTE that it is not possible to set the actual application icon which is + displayed by the operating system on the desktop or 'home screen'. Those + icons must be provided 'traditionally' through operating-system-specific + resources which are associated with the application (sokol_app.h might + later support setting the window icon from platform specific resource data + though). + + There are two ways to set the window icon: + + - at application start in the sokol_main() function by initializing + the sapp_desc.icon nested struct + - or later by calling the function sapp_set_icon() + + As a convenient shortcut, sokol_app.h comes with a builtin default-icon + (a rainbow-colored 'S', which at least looks a bit better than the Windows + default icon for applications), which can be activated like this: + + At startup in sokol_main(): + + sapp_desc sokol_main(...) { + return (sapp_desc){ + ... + icon.sokol_default = true + }; + } + + Or later by calling: + + sapp_set_icon(&(sapp_icon_desc){ .sokol_default = true }); + + NOTE that a completely zero-initialized sapp_icon_desc struct will not + update the window icon in any way. This is an 'escape hatch' so that you + can handle the window icon update yourself (or if you do this already, + sokol_app.h won't get in your way, in this case just leave the + sapp_desc.icon struct zero-initialized). + + Providing your own icon images works exactly like in GLFW (down to the + data format): + + You provide one or more 'candidate images' in different sizes, and the + sokol_app.h platform backends pick the best match for the specific backend + and icon type. + + For each candidate image, you need to provide: + + - the width in pixels + - the height in pixels + - and the actual pixel data in RGBA8 pixel format (e.g. 0xFFCC8844 + on a little-endian CPU means: alpha=0xFF, blue=0xCC, green=0x88, red=0x44) + + For instance, if you have 3 candidate images (small, medium, big) of + sizes 16x16, 32x32 and 64x64 the corresponding sapp_icon_desc struct is setup + like this: + + // the actual pixel data (RGBA8, origin top-left) + const uint32_t small[16][16] = { ... }; + const uint32_t medium[32][32] = { ... }; + const uint32_t big[64][64] = { ... }; + + const sapp_icon_desc icon_desc = { + .images = { + { .width = 16, .height = 16, .pixels = SAPP_RANGE(small) }, + { .width = 32, .height = 32, .pixels = SAPP_RANGE(medium) }, + // ...or without the SAPP_RANGE helper macro: + { .width = 64, .height = 64, .pixels = { .ptr=big, .size=sizeof(big) } } + } + }; + + An sapp_icon_desc struct initialized like this can then either be applied + at application start in sokol_main: + + sapp_desc sokol_main(...) { + return (sapp_desc){ + ... + icon = icon_desc + }; + } + + ...or later by calling sapp_set_icon(): + + sapp_set_icon(&icon_desc); + + Some window icon caveats: + + - once the window icon has been updated, there's no way to go back to + the platform's default icon, this is because some platforms (Linux + and HTML5) don't switch the icon visual back to the default even if + the custom icon is deleted or removed + - on HTML5, if the sokol_app.h icon doesn't show up in the browser + tab, check that there's no traditional favicon 'link' element + is defined in the page's index.html, sokol_app.h will only + append a new favicon link element, but not delete any manually + defined favicon in the page + + For an example and test of the window icon feature, check out the the + 'icon-sapp' sample on the sokol-samples git repository. + ONSCREEN KEYBOARD ================= On some platforms which don't provide a physical keyboard, sokol-app @@ -890,6 +990,7 @@ distribution. */ #define SOKOL_APP_INCLUDED (1) +#include <stddef.h> // size_t #include <stdint.h> #include <stdbool.h> @@ -910,12 +1011,22 @@ extern "C" { #endif +/* misc constants */ enum { SAPP_MAX_TOUCHPOINTS = 8, SAPP_MAX_MOUSEBUTTONS = 3, SAPP_MAX_KEYCODES = 512, + SAPP_MAX_ICONIMAGES = 8, }; +/* + sapp_event_type + + The type of event that's passed to the event handler callback + in the sapp_event.type field. These are not just "traditional" + input events, but also notify the application about state changes + or other user-invoked actions. +*/ typedef enum sapp_event_type { SAPP_EVENTTYPE_INVALID, SAPP_EVENTTYPE_KEY_DOWN, @@ -944,7 +1055,14 @@ typedef enum sapp_event_type { _SAPP_EVENTTYPE_FORCE_U32 = 0x7FFFFFFF } sapp_event_type; -/* key codes are the same names and values as GLFW */ +/* + sapp_keycode + + The 'virtual keycode' of a KEY_DOWN or KEY_UP event in the + struct field sapp_event.key_code. + + Note that the keycode values are identical with GLFW. +*/ typedef enum sapp_keycode { SAPP_KEYCODE_INVALID = 0, SAPP_KEYCODE_SPACE = 32, @@ -1069,6 +1187,15 @@ typedef enum sapp_keycode { SAPP_KEYCODE_MENU = 348, } sapp_keycode; +/* + sapp_touchpoint + + Describes a single touchpoint in a multitouch event (TOUCHES_BEGAN, + TOUCHES_MOVED, TOUCHES_ENDED). + + Touch points are stored in the nested array sapp_event.touches[], + and the number of touches is stored in sapp_event.num_touches. +*/ typedef struct sapp_touchpoint { uintptr_t identifier; float pos_x; @@ -1076,6 +1203,12 @@ typedef struct sapp_touchpoint { bool changed; } sapp_touchpoint; +/* + sapp_mousebutton + + The currently pressed mouse button in the events MOUSE_DOWN + and MOUSE_UP, stored in the struct field sapp_event.mouse_button. +*/ typedef enum sapp_mousebutton { SAPP_MOUSEBUTTON_LEFT = 0x0, SAPP_MOUSEBUTTON_RIGHT = 0x1, @@ -1083,6 +1216,10 @@ typedef enum sapp_mousebutton { SAPP_MOUSEBUTTON_INVALID = 0x100, } sapp_mousebutton; +/* + These are currently pressed modifier keys which are passed in the + struct field sapp_event.modifiers. +*/ enum { SAPP_MODIFIER_SHIFT = 0x1, SAPP_MODIFIER_CTRL = 0x2, @@ -1090,68 +1227,140 @@ enum { SAPP_MODIFIER_SUPER = 0x8 }; +/* + sapp_event + + This is an all-in-one event struct passed to the event handler + user callback function. Note that it depends on the event + type what struct fields actually contain useful values, so you + should first check the event type before reading other struct + fields. +*/ typedef struct sapp_event { - uint64_t frame_count; - sapp_event_type type; - sapp_keycode key_code; - uint32_t char_code; - bool key_repeat; - uint32_t modifiers; - sapp_mousebutton mouse_button; - float mouse_x; - float mouse_y; - float mouse_dx; - float mouse_dy; - float scroll_x; - float scroll_y; - int num_touches; - sapp_touchpoint touches[SAPP_MAX_TOUCHPOINTS]; - int window_width; + uint64_t frame_count; // current frame counter, always valid, useful for checking if two events were issued in the same frame + sapp_event_type type; // the event type, always valid + sapp_keycode key_code; // the virtual key code, only valid in KEY_UP, KEY_DOWN + uint32_t char_code; // the UTF-32 character code, only valid in CHAR events + bool key_repeat; // true if this is a key-repeat event, valid in KEY_UP, KEY_DOWN and CHAR + uint32_t modifiers; // current modifier keys, valid in all key-, char- and mouse-events + sapp_mousebutton mouse_button; // mouse button that was pressed or released, valid in MOUSE_DOWN, MOUSE_UP + float mouse_x; // current horizontal mouse position in pixels, always valid except during mouse lock + float mouse_y; // current vertical mouse position in pixels, always valid except during mouse lock + float mouse_dx; // relative horizontal mouse movement since last frame, always valid + float mouse_dy; // relative vertical mouse movement since last frame, always valid + float scroll_x; // horizontal mouse wheel scroll distance, valid in MOUSE_SCROLL events + float scroll_y; // vertical mouse wheel scroll distance, valid in MOUSE_SCROLL events + int num_touches; // number of valid items in the touches[] array + sapp_touchpoint touches[SAPP_MAX_TOUCHPOINTS]; // current touch points, valid in TOUCHES_BEGIN, TOUCHES_MOVED, TOUCHES_ENDED + int window_width; // current window- and framebuffer sizes in pixels, always valid int window_height; - int framebuffer_width; - int framebuffer_height; + int framebuffer_width; // = window_width * dpi_scale + int framebuffer_height; // = window_height * dpi_scale } sapp_event; +/* + sg_range + + A general pointer/size-pair struct and constructor macros for passing binary blobs + into sokol_app.h. +*/ +typedef struct sapp_range { + const void* ptr; + size_t size; +} sapp_range; +// disabling this for every includer isn't great, but the warnings are also quite pointless +#if defined(_MSC_VER) +#pragma warning(disable:4221) /* /W4 only: nonstandard extension used: 'x': cannot be initialized using address of automatic variable 'y' */ +#pragma warning(disable:4204) /* VS2015: nonstandard extension used: non-constant aggregate initializer */ +#endif +#if defined(__cplusplus) +#define SAPP_RANGE(x) sapp_range{ &x, sizeof(x) } +#else +#define SAPP_RANGE(x) (sapp_range){ &x, sizeof(x) } +#endif + +/* + sapp_image_desc + + This is used to describe image data to sokol_app.h (at first, window + icons, later maybe cursor images). + + Note that the actual image pixel format depends on the use case: + + - window icon pixels are RGBA8 + - cursor images are ??? (FIXME) +*/ +typedef struct sapp_image_desc { + int width; + int height; + sapp_range pixels; +} sapp_image_desc; + +/* + sapp_icon_desc + + An icon description structure for use in sapp_desc.icon and + sapp_set_icon(). + + When setting a custom image, the application can provide a number of + candidates differing in size, and sokol_app.h will pick the image(s) + closest to the size expected by the platform's window system. + + To set sokol-app's default icon, set .sokol_default to true. + + Otherwise provide candidate images of different sizes in the + images[] array. + + If both the sokol_default flag is set to true, any image candidates + will be ignored and the sokol_app.h default icon will be set. +*/ +typedef struct sapp_icon_desc { + bool sokol_default; + sapp_image_desc images[SAPP_MAX_ICONIMAGES]; +} sapp_icon_desc; + + typedef struct sapp_desc { - void (*init_cb)(void); /* these are the user-provided callbacks without user data */ + void (*init_cb)(void); // these are the user-provided callbacks without user data void (*frame_cb)(void); void (*cleanup_cb)(void); void (*event_cb)(const sapp_event*); void (*fail_cb)(const char*); - void* user_data; /* these are the user-provided callbacks with user data */ + void* user_data; // these are the user-provided callbacks with user data void (*init_userdata_cb)(void*); void (*frame_userdata_cb)(void*); void (*cleanup_userdata_cb)(void*); void (*event_userdata_cb)(const sapp_event*, void*); void (*fail_userdata_cb)(const char*, void*); - int width; /* the preferred width of the window / canvas */ - int height; /* the preferred height of the window / canvas */ - int sample_count; /* MSAA sample count */ - int swap_interval; /* the preferred swap interval (ignored on some platforms) */ - bool high_dpi; /* whether the rendering canvas is full-resolution on HighDPI displays */ - bool fullscreen; /* whether the window should be created in fullscreen mode */ - bool alpha; /* whether the framebuffer should have an alpha channel (ignored on some platforms) */ - const char* window_title; /* the window title as UTF-8 encoded string */ - bool user_cursor; /* if true, user is expected to manage cursor image in SAPP_EVENTTYPE_UPDATE_CURSOR */ - bool enable_clipboard; /* enable clipboard access, default is false */ - int clipboard_size; /* max size of clipboard content in bytes */ - bool enable_dragndrop; /* enable file dropping (drag'n'drop), default is false */ - int max_dropped_files; /* max number of dropped files to process (default: 1) */ - int max_dropped_file_path_length; /* max length in bytes of a dropped UTF-8 file path (default: 2048) */ + int width; // the preferred width of the window / canvas + int height; // the preferred height of the window / canvas + int sample_count; // MSAA sample count + int swap_interval; // the preferred swap interval (ignored on some platforms) + bool high_dpi; // whether the rendering canvas is full-resolution on HighDPI displays + bool fullscreen; // whether the window should be created in fullscreen mode + bool alpha; // whether the framebuffer should have an alpha channel (ignored on some platforms) + const char* window_title; // the window title as UTF-8 encoded string + bool user_cursor; // if true, user is expected to manage cursor image in SAPP_EVENTTYPE_UPDATE_CURSOR + bool enable_clipboard; // enable clipboard access, default is false + int clipboard_size; // max size of clipboard content in bytes + bool enable_dragndrop; // enable file dropping (drag'n'drop), default is false + int max_dropped_files; // max number of dropped files to process (default: 1) + int max_dropped_file_path_length; // max length in bytes of a dropped UTF-8 file path (default: 2048) + sapp_icon_desc icon; // the initial window icon to set /* backend-specific options */ - bool gl_force_gles2; /* if true, setup GLES2/WebGL even if GLES3/WebGL2 is available */ - bool win32_console_utf8; /* if true, set the output console codepage to UTF-8 */ - bool win32_console_create; /* if true, attach stdout/stderr to a new console window */ - bool win32_console_attach; /* if true, attach stdout/stderr to parent process */ - const char* html5_canvas_name; /* the name (id) of the HTML5 canvas element, default is "canvas" */ - bool html5_canvas_resize; /* if true, the HTML5 canvas size is set to sapp_desc.width/height, otherwise canvas size is tracked */ - bool html5_preserve_drawing_buffer; /* HTML5 only: whether to preserve default framebuffer content between frames */ - bool html5_premultiplied_alpha; /* HTML5 only: whether the rendered pixels use premultiplied alpha convention */ - bool html5_ask_leave_site; /* initial state of the internal html5_ask_leave_site flag (see sapp_html5_ask_leave_site()) */ - bool ios_keyboard_resizes_canvas; /* if true, showing the iOS keyboard shrinks the canvas */ + bool gl_force_gles2; // if true, setup GLES2/WebGL even if GLES3/WebGL2 is available + bool win32_console_utf8; // if true, set the output console codepage to UTF-8 + bool win32_console_create; // if true, attach stdout/stderr to a new console window + bool win32_console_attach; // if true, attach stdout/stderr to parent process + const char* html5_canvas_name; // the name (id) of the HTML5 canvas element, default is "canvas" + bool html5_canvas_resize; // if true, the HTML5 canvas size is set to sapp_desc.width/height, otherwise canvas size is tracked + bool html5_preserve_drawing_buffer; // HTML5 only: whether to preserve default framebuffer content between frames + bool html5_premultiplied_alpha; // HTML5 only: whether the rendered pixels use premultiplied alpha convention + bool html5_ask_leave_site; // initial state of the internal html5_ask_leave_site flag (see sapp_html5_ask_leave_site()) + bool ios_keyboard_resizes_canvas; // if true, showing the iOS keyboard shrinks the canvas } sapp_desc; /* HTML5 specific: request and response structs for @@ -1240,6 +1449,8 @@ SOKOL_APP_API_DECL void sapp_set_clipboard_string(const char* str); SOKOL_APP_API_DECL const char* sapp_get_clipboard_string(void); /* set the window title (only on desktop platforms) */ SOKOL_APP_API_DECL void sapp_set_window_title(const char* str); +/* set the window icon (only on Windows and Linux) */ +SOKOL_APP_API_DECL void sapp_set_icon(const sapp_icon_desc* icon_desc); /* gets the total number of dropped files (after an SAPP_EVENTTYPE_FILES_DROPPED event) */ SOKOL_APP_API_DECL int sapp_get_num_dropped_files(void); /* gets the dropped file paths */ @@ -1703,6 +1914,8 @@ typedef struct { typedef struct { HWND hwnd; HDC dc; + HICON big_icon; + HICON small_icon; UINT orig_codepage; LONG mouse_locked_x, mouse_locked_y; bool is_win10_or_greater; @@ -1933,6 +2146,7 @@ typedef struct { Atom WM_STATE; Atom NET_WM_NAME; Atom NET_WM_ICON_NAME; + Atom NET_WM_ICON; Atom NET_WM_STATE; Atom NET_WM_STATE_FULLSCREEN; _sapp_xi_t xi; @@ -2051,6 +2265,8 @@ typedef struct { _sapp_mouse_t mouse; _sapp_clipboard_t clipboard; _sapp_drop_t drop; + sapp_icon_desc default_icon_desc; + uint32_t* default_icon_pixels; #if defined(_SAPP_MACOS) _sapp_macos_t macos; #elif defined(_SAPP_IOS) @@ -2241,6 +2457,9 @@ _SOKOL_PRIVATE void _sapp_discard_state(void) { SOKOL_ASSERT(_sapp.drop.buffer); SOKOL_FREE((void*)_sapp.drop.buffer); } + if (_sapp.default_icon_pixels) { + SOKOL_FREE((void*)_sapp.default_icon_pixels); + } _SAPP_CLEAR(_sapp_t, _sapp); } @@ -2289,6 +2508,169 @@ _SOKOL_PRIVATE void _sapp_frame(void) { _sapp.frame_count++; } +_SOKOL_PRIVATE bool _sapp_image_validate(const sapp_image_desc* desc) { + SOKOL_ASSERT(desc->width > 0); + SOKOL_ASSERT(desc->height > 0); + SOKOL_ASSERT(desc->pixels.ptr != 0); + SOKOL_ASSERT(desc->pixels.size > 0); + const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); + if (wh_size != desc->pixels.size) { + _sapp_fail("Image data size mismatch (must be width*height*4 bytes)\n"); + return false; + } + return true; +} + +_SOKOL_PRIVATE int _sapp_image_bestmatch(const sapp_image_desc* desc, int num_images, int width, int height) { + int least_diff = 0x7FFFFFFF; + int least_index = 0; + for (int i = 0; i < num_images; i++) { + int diff = (desc->width * desc->height) - (width * height); + if (diff < 0) { + diff = -diff; + } + if (diff < least_diff) { + least_diff = diff; + least_index = i; + } + } + return least_index; +} + +_SOKOL_PRIVATE int _sapp_icon_num_images(const sapp_icon_desc* desc) { + int index = 0; + for (; index < SAPP_MAX_ICONIMAGES; index++) { + if (0 == desc->images[index].pixels.ptr) { + break; + } + } + return index; +} + +_SOKOL_PRIVATE bool _sapp_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { + SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &desc->images[i]; + if (!_sapp_image_validate(img_desc)) { + return false; + } + } + return true; +} + +_SOKOL_PRIVATE void _sapp_setup_default_icon(void) { + SOKOL_ASSERT(0 == _sapp.default_icon_pixels); + + const int num_icons = 3; + const int icon_sizes[3] = { 16, 32, 64 }; // must be multiple of 8! + + // allocate a pixel buffer for all icon pixels + int all_num_pixels = 0; + for (int i = 0; i < num_icons; i++) { + all_num_pixels += icon_sizes[i] * icon_sizes[i]; + } + _sapp.default_icon_pixels = (uint32_t*) SOKOL_CALLOC((size_t)all_num_pixels, sizeof(uint32_t)); + + // initialize default_icon_desc struct + uint32_t* dst = _sapp.default_icon_pixels; + const uint32_t* dst_end = dst + all_num_pixels; + (void)dst_end; // silence unused warning in release mode + for (int i = 0; i < num_icons; i++) { + const int dim = (int) icon_sizes[i]; + const int num_pixels = dim * dim; + sapp_image_desc* img_desc = &_sapp.default_icon_desc.images[i]; + img_desc->width = dim; + img_desc->height = dim; + img_desc->pixels.ptr = dst; + img_desc->pixels.size = (size_t)num_pixels * sizeof(uint32_t); + dst += num_pixels; + } + SOKOL_ASSERT(dst == dst_end); + + // Amstrad CPC font 'S' + const uint8_t tile[8] = { + 0x3C, + 0x66, + 0x60, + 0x3C, + 0x06, + 0x66, + 0x3C, + 0x00, + }; + // rainbow colors + const uint32_t colors[8] = { + 0xFF4370FF, + 0xFF26A7FF, + 0xFF58EEFF, + 0xFF57E1D4, + 0xFF65CC9C, + 0xFF6ABB66, + 0xFFF5A542, + 0xFFC2577E, + }; + dst = _sapp.default_icon_pixels; + const uint32_t blank = 0x00FFFFFF; + const uint32_t shadow = 0xFF000000; + for (int i = 0; i < num_icons; i++) { + const int dim = icon_sizes[i]; + SOKOL_ASSERT((dim % 8) == 0); + const int scale = dim / 8; + for (int ty = 0, y = 0; ty < 8; ty++) { + const uint32_t color = colors[ty]; + for (int sy = 0; sy < scale; sy++, y++) { + uint8_t bits = tile[ty]; + for (int tx = 0, x = 0; tx < 8; tx++, bits<<=1) { + uint32_t pixel = (0 == (bits & 0x80)) ? blank : color; + for (int sx = 0; sx < scale; sx++, x++) { + SOKOL_ASSERT(dst < dst_end); + *dst++ = pixel; + } + } + } + } + } + SOKOL_ASSERT(dst == dst_end); + + // right shadow + dst = _sapp.default_icon_pixels; + for (int i = 0; i < num_icons; i++) { + const int dim = icon_sizes[i]; + for (int y = 0; y < dim; y++) { + uint32_t prev_color = blank; + for (int x = 0; x < dim; x++) { + const int dst_index = y * dim + x; + const uint32_t cur_color = dst[dst_index]; + if ((cur_color == blank) && (prev_color != blank)) { + dst[dst_index] = shadow; + } + prev_color = cur_color; + } + } + dst += dim * dim; + } + SOKOL_ASSERT(dst == dst_end); + + // bottom shadow + dst = _sapp.default_icon_pixels; + for (int i = 0; i < num_icons; i++) { + const int dim = icon_sizes[i]; + for (int x = 0; x < dim; x++) { + uint32_t prev_color = blank; + for (int y = 0; y < dim; y++) { + const int dst_index = y * dim + x; + const uint32_t cur_color = dst[dst_index]; + if ((cur_color == blank) && (prev_color != blank)) { + dst[dst_index] = shadow; + } + prev_color = cur_color; + } + } + dst += dim * dim; + } + SOKOL_ASSERT(dst == dst_end); +} + /*== MacOS/iOS ===============================================================*/ #if defined(_SAPP_APPLE) @@ -3815,6 +4197,38 @@ _SOKOL_PRIVATE void _sapp_emsc_update_mouse_lock_state(void) { } } +/* JS helper functions to update browser tab favicon */ +EM_JS(void, sapp_js_clear_favicon, (void), { + var link = document.getElementById('sokol-app-favicon'); + if (link) { + document.head.removeChild(link); + } +}); + +EM_JS(void, sapp_js_set_favicon, (int w, int h, const uint8_t* pixels), { + var canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + var img_data = ctx.createImageData(w, h); + img_data.data.set(HEAPU8.subarray(pixels, pixels + w*h*4)); + ctx.putImageData(img_data, 0, 0); + var new_link = document.createElement('link'); + new_link.id = 'sokol-app-favicon'; + new_link.rel = 'shortcut icon'; + new_link.href = canvas.toDataURL(); + document.head.appendChild(new_link); +}); + +_SOKOL_PRIVATE void _sapp_emsc_set_icon(const sapp_icon_desc* icon_desc, int num_images) { + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + sapp_js_clear_favicon(); + // find the best matching image candidate for 16x16 pixels + int img_index = _sapp_image_bestmatch(icon_desc->images, num_images, 16, 16); + const sapp_image_desc* img_desc = &icon_desc->images[img_index]; + sapp_js_set_favicon(img_desc->width, img_desc->height, (const uint8_t*) img_desc->pixels.ptr); +} + #if defined(SOKOL_WGPU) _SOKOL_PRIVATE void _sapp_emsc_wgpu_surfaces_create(void); _SOKOL_PRIVATE void _sapp_emsc_wgpu_surfaces_discard(void); @@ -4578,6 +4992,7 @@ _SOKOL_PRIVATE void _sapp_emsc_run(const sapp_desc* desc) { #endif _sapp.valid = true; _sapp_emsc_register_eventhandlers(); + sapp_set_icon(&desc->icon); /* start the frame loop */ emscripten_request_animation_frame_loop(_sapp_emsc_frame, 0); @@ -5810,6 +6225,17 @@ _SOKOL_PRIVATE void _sapp_win32_destroy_window(void) { UnregisterClassW(L"SOKOLAPP", GetModuleHandleW(NULL)); } +_SOKOL_PRIVATE void _sapp_win32_destroy_icons(void) { + if (_sapp.win32.big_icon) { + DestroyIcon(_sapp.win32.big_icon); + _sapp.win32.big_icon = 0; + } + if (_sapp.win32.small_icon) { + DestroyIcon(_sapp.win32.small_icon); + _sapp.win32.small_icon = 0; + } +} + _SOKOL_PRIVATE void _sapp_win32_init_console(void) { if (_sapp.desc.win32_console_create || _sapp.desc.win32_console_attach) { BOOL con_valid = FALSE; @@ -5972,6 +6398,85 @@ _SOKOL_PRIVATE void _sapp_win32_update_window_title(void) { SetWindowTextW(_sapp.win32.hwnd, _sapp.window_title_wide); } +_SOKOL_PRIVATE HICON _sapp_win32_create_icon_from_image(const sapp_image_desc* desc) { + BITMAPV5HEADER bi; + memset(&bi, 0, sizeof(bi)); + bi.bV5Size = sizeof(bi); + bi.bV5Width = desc->width; + bi.bV5Height = -desc->height; // NOTE the '-' here to indicate that origin is top-left + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00FF0000; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x000000FF; + bi.bV5AlphaMask = 0xFF000000; + + uint8_t* target = 0; + const uint8_t* source = (const uint8_t*)desc->pixels.ptr; + + HDC dc = GetDC(NULL); + HBITMAP color = CreateDIBSection(dc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, (void**)&target, NULL, (DWORD)0); + ReleaseDC(NULL, dc); + if (0 == color) { + return NULL; + } + SOKOL_ASSERT(target); + + HBITMAP mask = CreateBitmap(desc->width, desc->height, 1, 1, NULL); + if (0 == mask) { + DeleteObject(color); + return NULL; + } + + for (int i = 0; i < (desc->width*desc->height); i++) { + target[0] = source[2]; + target[1] = source[1]; + target[2] = source[0]; + target[3] = source[3]; + target += 4; + source += 4; + } + + ICONINFO icon_info; + memset(&icon_info, 0, sizeof(icon_info)); + icon_info.fIcon = true; + icon_info.xHotspot = 0; + icon_info.yHotspot = 0; + icon_info.hbmMask = mask; + icon_info.hbmColor = color; + HICON icon_handle = CreateIconIndirect(&icon_info); + DeleteObject(color); + DeleteObject(mask); + + return icon_handle; +} + +_SOKOL_PRIVATE void _sapp_win32_set_icon(const sapp_icon_desc* icon_desc, int num_images) { + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + + int big_img_index = _sapp_image_bestmatch(icon_desc->images, num_images, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); + int sml_img_index = _sapp_image_bestmatch(icon_desc->images, num_images, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); + HICON big_icon = _sapp_win32_create_icon_from_image(&icon_desc->images[big_img_index]); + HICON sml_icon = _sapp_win32_create_icon_from_image(&icon_desc->images[sml_img_index]); + + // if icon creation or lookup has failed for some reason, leave the currently set icon untouched + if (0 != big_icon) { + SendMessage(_sapp.win32.hwnd, WM_SETICON, ICON_BIG, (LPARAM) big_icon); + if (0 != _sapp.win32.big_icon) { + DestroyIcon(_sapp.win32.big_icon); + } + _sapp.win32.big_icon = big_icon; + } + if (0 != sml_icon) { + SendMessage(_sapp.win32.hwnd, WM_SETICON, ICON_SMALL, (LPARAM) sml_icon); + if (0 != _sapp.win32.small_icon) { + DestroyIcon(_sapp.win32.small_icon); + } + _sapp.win32.small_icon = sml_icon; + } +} + /* don't laugh, but this seems to be the easiest and most robust way to check if we're running on Win10 @@ -5995,6 +6500,7 @@ _SOKOL_PRIVATE void _sapp_win32_run(const sapp_desc* desc) { _sapp_win32_uwp_utf8_to_wide(_sapp.window_title, _sapp.window_title_wide, sizeof(_sapp.window_title_wide)); _sapp_win32_init_dpi(); _sapp_win32_create_window(); + sapp_set_icon(&desc->icon); #if defined(SOKOL_D3D11) _sapp_d3d11_create_device_and_swapchain(); _sapp_d3d11_create_default_render_target(); @@ -6050,6 +6556,7 @@ _SOKOL_PRIVATE void _sapp_win32_run(const sapp_desc* desc) { _sapp_wgl_shutdown(); #endif _sapp_win32_destroy_window(); + _sapp_win32_destroy_icons(); _sapp_win32_restore_console(); _sapp_discard_state(); } @@ -8627,6 +9134,7 @@ _SOKOL_PRIVATE void _sapp_x11_init_extensions(void) { _sapp.x11.WM_STATE = XInternAtom(_sapp.x11.display, "WM_STATE", False); _sapp.x11.NET_WM_NAME = XInternAtom(_sapp.x11.display, "_NET_WM_NAME", False); _sapp.x11.NET_WM_ICON_NAME = XInternAtom(_sapp.x11.display, "_NET_WM_ICON_NAME", False); + _sapp.x11.NET_WM_ICON = XInternAtom(_sapp.x11.display, "_NET_WM_ICON", False); _sapp.x11.NET_WM_STATE = XInternAtom(_sapp.x11.display, "_NET_WM_STATE", False); _sapp.x11.NET_WM_STATE_FULLSCREEN = XInternAtom(_sapp.x11.display, "_NET_WM_STATE_FULLSCREEN", False); if (_sapp.drop.enabled) { @@ -9079,6 +9587,39 @@ _SOKOL_PRIVATE void _sapp_x11_update_window_title(void) { XFlush(_sapp.x11.display); } +_SOKOL_PRIVATE void _sapp_x11_set_icon(const sapp_icon_desc* icon_desc, int num_images) { + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + int long_count = 0; + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &icon_desc->images[i]; + long_count += 2 + (img_desc->width * img_desc->height); + } + long* icon_data = (long*) SOKOL_CALLOC((size_t)long_count, sizeof(long)); + SOKOL_ASSERT(icon_data); + long* dst = icon_data; + for (int img_index = 0; img_index < num_images; img_index++) { + const sapp_image_desc* img_desc = &icon_desc->images[img_index]; + const uint8_t* src = (const uint8_t*) img_desc->pixels.ptr; + *dst++ = img_desc->width; + *dst++ = img_desc->height; + const int num_pixels = img_desc->width * img_desc->height; + for (int pixel_index = 0; pixel_index < num_pixels; pixel_index++) { + *dst++ = (src[pixel_index * 4 + 0] << 16) | + (src[pixel_index * 4 + 1] << 8) | + (src[pixel_index * 4 + 2] << 0) | + (src[pixel_index * 4 + 3] << 24); + } + } + XChangeProperty(_sapp.x11.display, _sapp.x11.window, + _sapp.x11.NET_WM_ICON, + XA_CARDINAL, 32, + PropModeReplace, + (unsigned char*)icon_data, + long_count); + SOKOL_FREE(icon_data); + XFlush(_sapp.x11.display); +} + _SOKOL_PRIVATE void _sapp_x11_create_window(Visual* visual, int depth) { _sapp.x11.colormap = XCreateColormap(_sapp.x11.display, _sapp.x11.root, visual, AllocNone); XSetWindowAttributes wa; @@ -9827,6 +10368,7 @@ _SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc* desc) { _sapp_glx_choose_visual(&visual, &depth); _sapp_x11_create_window(visual, depth); _sapp_glx_create_context(); + sapp_set_icon(&desc->icon); _sapp.valid = true; _sapp_x11_show_window(); if (_sapp.fullscreen) { @@ -10113,6 +10655,32 @@ SOKOL_API_IMPL void sapp_set_window_title(const char* title) { #endif } +SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { + SOKOL_ASSERT(desc); + if (desc->sokol_default) { + if (0 == _sapp.default_icon_pixels) { + _sapp_setup_default_icon(); + } + SOKOL_ASSERT(0 != _sapp.default_icon_pixels); + desc = &_sapp.default_icon_desc; + } + const int num_images = _sapp_icon_num_images(desc); + if (num_images == 0) { + return; + } + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + if (!_sapp_validate_icon_desc(desc, num_images)) { + return; + } + #if defined(_SAPP_WIN32) + _sapp_win32_set_icon(desc, num_images); + #elif defined(_SAPP_LINUX) + _sapp_x11_set_icon(desc, num_images); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_emsc_set_icon(desc, num_images); + #endif +} + SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { SOKOL_ASSERT(_sapp.drop.enabled); return _sapp.drop.num_files; |