diff --git a/docs/README-wayland.md b/docs/README-wayland.md index aa0f2ccf0..ec3a24deb 100644 --- a/docs/README-wayland.md +++ b/docs/README-wayland.md @@ -46,12 +46,12 @@ encounter limitations or behavior that is different from other windowing systems ### The application icon can't be set via ```SDL_SetWindowIcon()``` -- Wayland doesn't support programmatically setting the application icon. To provide a custom icon for your application, - you must create an associated desktop entry file, aka a `.desktop` file, that points to the icon image. Please see the - [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/) for more information - on the format of this file. Note that if your application manually sets the application ID via the `SDL_APP_ID` hint - string, the desktop entry file name should match the application ID. For example, if your application ID is set - to `org.my_org.sdl_app`, the desktop entry file should be named `org.my_org.sdl_app.desktop`. +- Wayland requires compositor support for the `xdg-toplevel-icon-v1` protocol to set window icons programmatically. + Otherwise, the launcher icon from the associated desktop entry file, aka a `.desktop` file, will typically be used. + Please see the [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/) for + more information on the format of this file. Note that if your application manually sets the application ID via the + `SDL_APP_ID` hint string, the desktop entry file name should match the application ID. For example, if your + application ID is set to `org.my_org.sdl_app`, the desktop entry file should be named `org.my_org.sdl_app.desktop`. ## Using custom Wayland windowing protocols with SDL windows diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index dfa045e40..a3898c4da 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -63,6 +63,7 @@ #include "xdg-foreign-unstable-v2-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" #include "xdg-shell-client-protocol.h" +#include "xdg-toplevel-icon-v1-client-protocol.h" #ifdef HAVE_LIBDECOR_H #include @@ -549,6 +550,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->SetWindowModalFor = Wayland_SetWindowModalFor; device->SetWindowOpacity = Wayland_SetWindowOpacity; device->SetWindowTitle = Wayland_SetWindowTitle; + device->SetWindowIcon = Wayland_SetWindowIcon; device->GetWindowSizeInPixels = Wayland_GetWindowSizeInPixels; device->GetDisplayForWindow = Wayland_GetDisplayForWindow; device->DestroyWindow = Wayland_DestroyWindow; @@ -1191,6 +1193,8 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint d->xdg_wm_dialog_v1 = wl_registry_bind(d->registry, id, &xdg_wm_dialog_v1_interface, 1); } else if (SDL_strcmp(interface, "wp_alpha_modifier_v1") == 0) { d->wp_alpha_modifier_v1 = wl_registry_bind(d->registry, id, &wp_alpha_modifier_v1_interface, 1); + } else if (SDL_strcmp(interface, "xdg_toplevel_icon_manager_v1") == 0) { + d->xdg_toplevel_icon_manager_v1 = wl_registry_bind(d->registry, id, &xdg_toplevel_icon_manager_v1_interface, 1); } else if (SDL_strcmp(interface, "kde_output_order_v1") == 0) { d->kde_output_order = wl_registry_bind(d->registry, id, &kde_output_order_v1_interface, 1); kde_output_order_v1_add_listener(d->kde_output_order, &kde_output_order_listener, d); @@ -1460,6 +1464,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this) data->wp_alpha_modifier_v1 = NULL; } + if (data->xdg_toplevel_icon_manager_v1) { + xdg_toplevel_icon_manager_v1_destroy(data->xdg_toplevel_icon_manager_v1); + data->xdg_toplevel_icon_manager_v1 = NULL; + } + if (data->kde_output_order) { Wayland_FlushOutputOrder(data); kde_output_order_v1_destroy(data->kde_output_order); diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h index 426b56661..411670669 100644 --- a/src/video/wayland/SDL_waylandvideo.h +++ b/src/video/wayland/SDL_waylandvideo.h @@ -81,6 +81,7 @@ struct SDL_VideoData struct zxdg_exporter_v2 *zxdg_exporter_v2; struct xdg_wm_dialog_v1 *xdg_wm_dialog_v1; struct wp_alpha_modifier_v1 *wp_alpha_modifier_v1; + struct xdg_toplevel_icon_manager_v1 *xdg_toplevel_icon_manager_v1; struct kde_output_order_v1 *kde_output_order; struct frog_color_management_factory_v1 *frog_color_management_factory_v1; struct zwp_tablet_manager_v2 *tablet_manager; diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 8c766722b..4596a3078 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -42,6 +42,7 @@ #include "xdg-foreign-unstable-v2-client-protocol.h" #include "xdg-dialog-v1-client-protocol.h" #include "frog-color-management-v1-client-protocol.h" +#include "xdg-toplevel-icon-v1-client-protocol.h" #ifdef HAVE_LIBDECOR_H #include @@ -1722,6 +1723,12 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) zxdg_exported_v2_add_listener(data->exported, &exported_v2_listener, data); } + if (c->xdg_toplevel_icon_manager_v1 && data->xdg_toplevel_icon_v1) { + xdg_toplevel_icon_manager_v1_set_icon(_this->internal->xdg_toplevel_icon_manager_v1, + libdecor_frame_get_xdg_toplevel(data->shell_surface.libdecor.frame), + data->xdg_toplevel_icon_v1); + } + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, libdecor_frame_get_xdg_surface(data->shell_surface.libdecor.frame)); SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, libdecor_frame_get_xdg_toplevel(data->shell_surface.libdecor.frame)); } @@ -1802,6 +1809,12 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) zxdg_exported_v2_add_listener(data->exported, &exported_v2_listener, data); } + if (c->xdg_toplevel_icon_manager_v1 && data->xdg_toplevel_icon_v1) { + xdg_toplevel_icon_manager_v1_set_icon(_this->internal->xdg_toplevel_icon_manager_v1, + data->shell_surface.xdg.roleobj.toplevel, + data->xdg_toplevel_icon_v1); + } + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, data->shell_surface.xdg.roleobj.toplevel); } } @@ -2666,6 +2679,51 @@ void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) } } +bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon) +{ + SDL_WindowData *wind = window->internal; + struct xdg_toplevel *toplevel = NULL; + + if (!_this->internal->xdg_toplevel_icon_manager_v1) { + return SDL_SetError("wayland: cannot set icon; xdg_toplevel_icon_v1 protocol not supported"); + } + + if (icon->w != icon->h) { + return SDL_SetError("wayland: icon width and height must be equal, got %ix%i", icon->w, icon->h); + } + + if (wind->xdg_toplevel_icon_v1) { + xdg_toplevel_icon_v1_destroy(wind->xdg_toplevel_icon_v1); + wind->xdg_toplevel_icon_v1 = NULL; + } + + // TODO: Add high-DPI icon support + Wayland_ReleaseSHMBuffer(&wind->icon); + if (Wayland_AllocSHMBuffer(icon->w, icon->h, &wind->icon) != 0) { + return SDL_SetError("wayland: failed to allocate SHM buffer for the icon"); + } + + SDL_PremultiplyAlpha(icon->w, icon->h, icon->format, icon->pixels, icon->pitch, SDL_PIXELFORMAT_ARGB8888, wind->icon.shm_data, icon->w * 4, SDL_TRUE); + + wind->xdg_toplevel_icon_v1 = xdg_toplevel_icon_manager_v1_create_icon(_this->internal->xdg_toplevel_icon_manager_v1); + xdg_toplevel_icon_v1_add_buffer(wind->xdg_toplevel_icon_v1, wind->icon.wl_buffer, 1); + +#ifdef HAVE_LIBDECOR_H + if (wind->shell_surface_type == WAYLAND_SURFACE_LIBDECOR && wind->shell_surface.libdecor.frame) { + toplevel = libdecor_frame_get_xdg_toplevel(wind->shell_surface.libdecor.frame); + } else +#endif + if (wind->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL && wind->shell_surface.xdg.roleobj.toplevel) { + toplevel = wind->shell_surface.xdg.roleobj.toplevel; + } + + if (toplevel) { + xdg_toplevel_icon_manager_v1_set_icon(_this->internal->xdg_toplevel_icon_manager_v1, toplevel, wind->xdg_toplevel_icon_v1); + } + + return true; +} + bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) { SDL_WindowData *wind = window->internal; @@ -2803,6 +2861,12 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) Wayland_RemoveWindowDataFromExternalList(wind); } + if (wind->xdg_toplevel_icon_v1) { + xdg_toplevel_icon_v1_destroy(wind->xdg_toplevel_icon_v1); + } + + Wayland_ReleaseSHMBuffer(&wind->icon); + SDL_free(wind); WAYLAND_wl_display_flush(data->display); } diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index 38494a9b8..278884f2d 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -28,6 +28,7 @@ #include "../../events/SDL_touch_c.h" #include "SDL_waylandvideo.h" +#include "SDL_waylandshmbuffer.h" struct SDL_WaylandInput; @@ -98,6 +99,7 @@ struct SDL_WindowData struct zxdg_exported_v2 *exported; struct xdg_dialog_v1 *xdg_dialog_v1; struct wp_alpha_modifier_surface_v1 *wp_alpha_modifier_surface_v1; + struct xdg_toplevel_icon_v1 *xdg_toplevel_icon_v1; struct frog_color_managed_surface *frog_color_managed_surface; SDL_AtomicInt swap_interval_ready; @@ -110,6 +112,8 @@ struct SDL_WindowData char *app_id; float windowed_scale_factor; + struct Wayland_SHMBuffer icon; + struct { float x; @@ -202,6 +206,7 @@ extern void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y); extern void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); extern bool Wayland_SuspendScreenSaver(SDL_VideoDevice *_this); +extern bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon); extern bool Wayland_SetWindowHitTest(SDL_Window *window, bool enabled); extern bool Wayland_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation); diff --git a/wayland-protocols/xdg-toplevel-icon-v1.xml b/wayland-protocols/xdg-toplevel-icon-v1.xml new file mode 100644 index 000000000..4270d6940 --- /dev/null +++ b/wayland-protocols/xdg-toplevel-icon-v1.xml @@ -0,0 +1,203 @@ + + + + + Copyright © 2023-2024 Matthias Klumpp + Copyright © 2024 David Edmundson + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to set icons for their toplevel surfaces + either via the XDG icon stock (using an icon name), or from pixel data. + + A toplevel icon represents the individual toplevel (unlike the application + or launcher icon, which represents the application as a whole), and may be + shown in window switchers, window overviews and taskbars that list + individual windows. + + This document adheres to RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + This interface allows clients to create toplevel window icons and set + them on toplevel windows to be displayed to the user. + + + + + Destroy the toplevel icon manager. + This does not destroy objects created with the manager. + + + + + + Creates a new icon object. This icon can then be attached to a + xdg_toplevel via the 'set_icon' request. + + + + + + + This request assigns the icon 'icon' to 'toplevel', or clears the + toplevel icon if 'icon' was null. + This state is double-buffered and is applied on the next + wl_surface.commit of the toplevel. + + After making this call, the xdg_toplevel_icon_v1 provided as 'icon' + can be destroyed by the client without 'toplevel' losing its icon. + The xdg_toplevel_icon_v1 is immutable from this point, and any + future attempts to change it must raise the + 'xdg_toplevel_icon_v1.immutable' protocol error. + + The compositor must set the toplevel icon from either the pixel data + the icon provides, or by loading a stock icon using the icon name. + See the description of 'xdg_toplevel_icon_v1' for details. + + If 'icon' is set to null, the icon of the respective toplevel is reset + to its default icon (usually the icon of the application, derived from + its desktop-entry file, or a placeholder icon). + If this request is passed an icon with no pixel buffers or icon name + assigned, the icon must be reset just like if 'icon' was null. + + + + + + + + This event indicates an icon size the compositor prefers to be + available if the client has scalable icons and can render to any size. + + When the 'xdg_toplevel_icon_manager_v1' object is created, the + compositor may send one or more 'icon_size' events to describe the list + of preferred icon sizes. If the compositor has no size preference, it + may not send any 'icon_size' event, and it is up to the client to + decide a suitable icon size. + + A sequence of 'icon_size' events must be finished with a 'done' event. + If the compositor has no size preferences, it must still send the + 'done' event, without any preceding 'icon_size' events. + + + + + + + This event is sent after all 'icon_size' events have been sent. + + + + + + + This interface defines a toplevel icon. + An icon can have a name, and multiple buffers. + In order to be applied, the icon must have either a name, or at least + one buffer assigned. Applying an empty icon (with no buffer or name) to + a toplevel should reset its icon to the default icon. + + It is up to compositor policy whether to prefer using a buffer or loading + an icon via its name. See 'set_name' and 'add_buffer' for details. + + + + + + + + + + + Destroys the 'xdg_toplevel_icon_v1' object. + The icon must still remain set on every toplevel it was assigned to, + until the toplevel icon is reset explicitly. + + + + + + This request assigns an icon name to this icon. + Any previously set name is overridden. + + The compositor must resolve 'icon_name' according to the lookup rules + described in the XDG icon theme specification[1] using the + environment's current icon theme. + + If the compositor does not support icon names or cannot resolve + 'icon_name' according to the XDG icon theme specification it must + fall back to using pixel buffer data instead. + + If this request is made after the icon has been assigned to a toplevel + via 'set_icon', a 'immutable' error must be raised. + + [1]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + + + + + + + This request adds pixel data supplied as wl_buffer to the icon. + + The client should add pixel data for all icon sizes and scales that + it can provide, or which are explicitly requested by the compositor + via 'icon_size' events on xdg_toplevel_icon_manager_v1. + + The wl_buffer supplying pixel data as 'buffer' must be backed by wl_shm + and must be a square (width and height being equal). + If any of these buffer requirements are not fulfilled, a 'invalid_buffer' + error must be raised. + + If this icon instance already has a buffer of the same size and scale + from a previous 'add_buffer' request, data from the last request + overrides the preexisting pixel data. + + The wl_buffer must be kept alive for as long as the xdg_toplevel_icon + it is associated with is not destroyed, otherwise a 'no_buffer' error + is raised. The buffer contents must not be modified after it was + assigned to the icon. + + If this request is made after the icon has been assigned to a toplevel + via 'set_icon', a 'immutable' error must be raised. + + + + + +