From 5d5a685a8004fe8ceaf3dc5b3e9b431b32603e4b Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Thu, 28 Mar 2024 10:26:16 -0400
Subject: [PATCH] wayland: Add support for setting window icons via the
xdg-toplevel-icon-v1 protocol
---
docs/README-wayland.md | 12 +-
src/video/wayland/SDL_waylandvideo.c | 9 +
src/video/wayland/SDL_waylandvideo.h | 1 +
src/video/wayland/SDL_waylandwindow.c | 64 +++++++
src/video/wayland/SDL_waylandwindow.h | 5 +
wayland-protocols/xdg-toplevel-icon-v1.xml | 203 +++++++++++++++++++++
6 files changed, 288 insertions(+), 6 deletions(-)
create mode 100644 wayland-protocols/xdg-toplevel-icon-v1.xml
diff --git a/docs/README-wayland.md b/docs/README-wayland.md
index aa0f2ccf09bf1..ec3a24debbc63 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 dfa045e40d67d..a3898c4da8015 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 <libdecor.h>
@@ -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 426b566619ea0..411670669a642 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 8c766722b5293..4596a3078ef03 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 <libdecor.h>
@@ -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 38494a9b8abda..278884f2df634 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 0000000000000..4270d69408d99
--- /dev/null
+++ b/wayland-protocols/xdg-toplevel-icon-v1.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="xdg_toplevel_icon_v1">
+
+ <copyright>
+ 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.
+ </copyright>
+
+ <description summary="protocol to assign icons to toplevels">
+ 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.
+ </description>
+
+ <interface name="xdg_toplevel_icon_manager_v1" version="1">
+ <description summary="interface to manage toplevel icons">
+ This interface allows clients to create toplevel window icons and set
+ them on toplevel windows to be displayed to the user.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the toplevel icon manager">
+ Destroy the toplevel icon manager.
+ This does not destroy objects created with the manager.
+ </description>
+ </request>
+
+ <request name="create_icon">
+ <description summary="create a new icon instance">
+ Creates a new icon object. This icon can then be attached to a
+ xdg_toplevel via the 'set_icon' request.
+ </description>
+ <arg name="id" type="new_id" interface="xdg_toplevel_icon_v1"/>
+ </request>
+
+ <request name="set_icon">
+ <description summary="set an icon on a toplevel window">
+ 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.
+ </description>
+ <arg name="toplevel" type="object" interface="xdg_toplevel" summary="the toplevel to act on"/>
+ <arg name="icon" type="object" interface="xdg_toplevel_icon_v1" allow-null="true"/>
+ </request>
+
+ <event name="icon_size">
+ <description summary="describes a supported & preferred icon size">
+ 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.
+ </description>
+ <arg name="size" type="int"
+ summary="the edge size of the square icon in surface-local coordinates, e.g. 64"/>
+ </event>
+
+ <event name="done">
+ <description summary="all information has been sent">
+ This event is sent after all 'icon_size' events have been sent.
+ </description>
+ </event>
+ </interface>
+
+ <interface name="xdg_toplevel_icon_v1" version="1">
+ <description summary="a toplevel window icon">
+ 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.
+ </description>
+
+ <enum name="error">
+ <entry name="invalid_buffer"
+ summary="the provided buffer does not satisfy requirements"
+ value="1"/>
+ <entry name="immutable"
+ summary="the icon has already been assigned to a toplevel and must not be changed"
+ value="2"/>
+ <entry name="no_buffer"
+ summary="the provided buffer has been destroyed before the toplevel icon"
+ value="3"/>
+ </enum>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the icon object">
+ 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.
+ </description>
+ </request>
+
+ <request name="set_name">
+ <description summary="set an icon name">
+ 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
+ </description>
+ <arg name="icon_name" type="string"/>
+ </request>
+
+ <request name="add_buffer">
+ <description summary="add icon data from a pixel buffer">
+ 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.
+ </description>
+ <arg name="buffer" type="object" interface="wl_buffer"/>
+ <arg name="scale" type="int"
+ summary="the scaling factor of the icon, e.g. 1"/>
+ </request>
+ </interface>
+</protocol>