From fe1eab07ae4886ce0cc1a0e892b23945abceca38 Mon Sep 17 00:00:00 2001 From: Enn3Developer <45467622+Enn3Developer@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:40:05 +0200 Subject: [PATCH] wayland: add xdg_toplevel_icon_v1 support Closes: #3859. --- winit-core/src/window.rs | 6 +- winit-wayland/src/lib.rs | 26 +++++ winit-wayland/src/state.rs | 17 ++- winit-wayland/src/types/cursor.rs | 29 ++--- winit-wayland/src/types/mod.rs | 1 + .../src/types/xdg_toplevel_icon_manager.rs | 108 ++++++++++++++++++ winit-wayland/src/window/mod.rs | 6 +- winit-wayland/src/window/state.rs | 65 ++++++++++- winit/src/changelog/unreleased.md | 1 + 9 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 winit-wayland/src/types/xdg_toplevel_icon_manager.rs diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 742ae2db..3b021cb2 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1041,18 +1041,20 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Sets the window icon. /// - /// On Windows and X11, this is typically the small icon in the top-left + /// On Windows, Wayland and X11, this is typically the small icon in the top-left /// corner of the titlebar. /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Wayland / macOS / Orbital:** Unsupported. + /// - **iOS / Android / Web / / macOS / Orbital:** Unsupported. /// /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. /// /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. /// That said, it's usually in the same ballpark as on Windows. + /// + /// - **Wayland:** The compositor needs to implement `xdg_toplevel_icon`. fn set_window_icon(&self, window_icon: Option); /// Set the IME cursor editing area, where the `position` is the top left corner of that area diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 7e9c745d..1a35a3de 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -19,6 +19,8 @@ use std::ptr::NonNull; use dpi::{LogicalSize, PhysicalSize}; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::Proxy; +use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool}; +use wayland_client::protocol::wl_shm::Format; use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop; use winit_core::window::{ ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId, @@ -138,3 +140,27 @@ fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> Phy let height = size.height as f64 * scale_factor; (width.round(), height.round()).into() } + +/// Converts an image buffer to a Wayland buffer (`wl_buffer`) +fn image_to_buffer( + width: i32, + height: i32, + data: &[u8], + format: Format, + pool: &mut SlotPool, +) -> Result { + let (buffer, canvas) = pool.create_buffer(width, height, 4 * width, format)?; + + for (canvas_chunk, rgba) in canvas.chunks_exact_mut(4).zip(data.chunks_exact(4)) { + // Alpha in buffer is premultiplied. + let alpha = rgba[3] as f32 / 255.; + let r = (rgba[0] as f32 * alpha) as u32; + let g = (rgba[1] as f32 * alpha) as u32; + let b = (rgba[2] as f32 * alpha) as u32; + let color = ((rgba[3] as u32) << 24) + (r << 16) + (g << 8) + b; + let array: &mut [u8; 4] = canvas_chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + } + + Ok(buffer) +} diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index f41cf3d9..9f8a4f49 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -32,6 +32,7 @@ use crate::types::kwin_blur::KWinBlurManager; use crate::types::wp_fractional_scaling::FractionalScalingManager; use crate::types::wp_viewporter::ViewporterState; use crate::types::xdg_activation::XdgActivationState; +use crate::types::xdg_toplevel_icon_manager::XdgToplevelIconManagerState; use crate::window::{WindowRequests, WindowState}; use crate::WindowId; @@ -56,9 +57,6 @@ pub struct WinitState { /// The shm for software buffers, such as cursors. pub shm: Shm, - /// The pool where custom cursors are allocated. - pub custom_cursor_pool: Arc>, - /// The XDG shell that is used for windows. pub xdg_shell: XdgShell, @@ -93,6 +91,12 @@ pub struct WinitState { /// Xdg activation. pub xdg_activation: Option, + /// Xdg toplevel icon manager + pub xdg_toplevel_icon_manager: Option, + + /// The pool where images are allocated (used for window icons and custom cursors) + pub image_pool: Arc>, + /// Relative pointer. pub relative_pointer: Option, @@ -158,7 +162,7 @@ impl WinitState { }; let shm = Shm::bind(globals, queue_handle).map_err(|err| os_error!(err))?; - let custom_cursor_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); + let image_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); Ok(Self { registry_state, @@ -167,10 +171,13 @@ impl WinitState { output_state, seat_state, shm, - custom_cursor_pool, xdg_shell: XdgShell::bind(globals, queue_handle).map_err(|err| os_error!(err))?, xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(), + xdg_toplevel_icon_manager: XdgToplevelIconManagerState::bind(globals, queue_handle) + .ok(), + + image_pool, windows: Default::default(), window_requests: Default::default(), diff --git a/winit-wayland/src/types/cursor.rs b/winit-wayland/src/types/cursor.rs index 1e89bc6f..9b6e1de2 100644 --- a/winit-wayland/src/types/cursor.rs +++ b/winit-wayland/src/types/cursor.rs @@ -3,6 +3,8 @@ use sctk::reexports::client::protocol::wl_shm::Format; use sctk::shm::slot::{Buffer, SlotPool}; use winit_core::cursor::{CursorImage, CustomCursorProvider}; +use crate::image_to_buffer; + // Wrap in our own type to not impl trait on global type. #[derive(Debug)] pub struct WaylandCustomCursor(pub(crate) CursorImage); @@ -36,25 +38,14 @@ pub struct CustomCursor { impl CustomCursor { pub(crate) fn new(pool: &mut SlotPool, image: &WaylandCustomCursor) -> Self { let image = &image.0; - let (buffer, canvas) = pool - .create_buffer( - image.width() as i32, - image.height() as i32, - 4 * (image.width() as i32), - Format::Argb8888, - ) - .unwrap(); - - for (canvas_chunk, rgba) in canvas.chunks_exact_mut(4).zip(image.buffer().chunks_exact(4)) { - // Alpha in buffer is premultiplied. - let alpha = rgba[3] as f32 / 255.; - let r = (rgba[0] as f32 * alpha) as u32; - let g = (rgba[1] as f32 * alpha) as u32; - let b = (rgba[2] as f32 * alpha) as u32; - let color = ((rgba[3] as u32) << 24) + (r << 16) + (g << 8) + b; - let array: &mut [u8; 4] = canvas_chunk.try_into().unwrap(); - *array = color.to_le_bytes(); - } + let buffer = image_to_buffer( + image.width() as i32, + image.height() as i32, + image.buffer(), + Format::Argb8888, + pool, + ) + .unwrap(); CustomCursor { buffer, diff --git a/winit-wayland/src/types/mod.rs b/winit-wayland/src/types/mod.rs index 77e67f48..e27a98ff 100644 --- a/winit-wayland/src/types/mod.rs +++ b/winit-wayland/src/types/mod.rs @@ -5,3 +5,4 @@ pub mod kwin_blur; pub mod wp_fractional_scaling; pub mod wp_viewporter; pub mod xdg_activation; +pub mod xdg_toplevel_icon_manager; diff --git a/winit-wayland/src/types/xdg_toplevel_icon_manager.rs b/winit-wayland/src/types/xdg_toplevel_icon_manager.rs new file mode 100644 index 00000000..df692c30 --- /dev/null +++ b/winit-wayland/src/types/xdg_toplevel_icon_manager.rs @@ -0,0 +1,108 @@ +//! Handling of xdg toplevel icon manager, which is used for icon setting requests. + +use std::fmt; +use std::fmt::Formatter; + +use sctk::globals::GlobalData; +use sctk::shm::slot::{Buffer, SlotPool}; +use wayland_client::globals::{BindError, GlobalList}; +use wayland_client::protocol::wl_shm::Format; +use wayland_client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_manager_v1::XdgToplevelIconManagerV1; +use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_v1::XdgToplevelIconV1; +use winit_core::icon::{Icon, RgbaIcon}; + +use crate::image_to_buffer; +use crate::state::WinitState; + +#[derive(Debug)] +pub struct XdgToplevelIconManagerState { + xdg_toplevel_icon_manager: XdgToplevelIconManagerV1, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum ToplevelIconError { + /// The icon's unsupported + Unsupported, +} + +#[derive(Debug)] +pub struct ToplevelIcon { + buffer: Buffer, +} + +impl ToplevelIcon { + pub fn new(icon: Icon, pool: &mut SlotPool) -> Result { + let icon = match icon.cast_ref::() { + Some(icon) => icon, + None => return Err(ToplevelIconError::Unsupported), + }; + + let buffer = image_to_buffer( + icon.width() as i32, + icon.height() as i32, + icon.buffer(), + Format::Argb8888, + pool, + ) + .unwrap(); + + Ok(Self { buffer }) + } + + pub fn add_buffer(&self, xdg_toplevel_icon: &XdgToplevelIconV1) { + xdg_toplevel_icon.add_buffer(self.buffer.wl_buffer(), 1); + } +} + +impl fmt::Display for ToplevelIconError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ToplevelIconError::Unsupported => write!(f, "this icon is unsupported on Wayland"), + } + } +} + +impl XdgToplevelIconManagerState { + pub fn bind( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let xdg_toplevel_icon_manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { xdg_toplevel_icon_manager }) + } + + pub fn global(&self) -> &XdgToplevelIconManagerV1 { + &self.xdg_toplevel_icon_manager + } +} + +impl Dispatch for XdgToplevelIconManagerState { + fn event( + _state: &mut WinitState, + _proxy: &XdgToplevelIconManagerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + // No events. + } +} + +impl Dispatch for XdgToplevelIconManagerState { + fn event( + _state: &mut WinitState, + _proxy: &XdgToplevelIconV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + // No events. + } +} + +delegate_dispatch!(WinitState: [XdgToplevelIconManagerV1: GlobalData] => XdgToplevelIconManagerState); +delegate_dispatch!(WinitState: [XdgToplevelIconV1: GlobalData] => XdgToplevelIconManagerState); diff --git a/winit-wayland/src/window/mod.rs b/winit-wayland/src/window/mod.rs index df75eea7..d5b1312e 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -114,6 +114,8 @@ impl Window { attributes.preferred_theme, ); + window_state.set_window_icon(attributes.window_icon); + // Set transparency hint. window_state.set_transparent(attributes.transparent); @@ -501,7 +503,9 @@ impl CoreWindow for Window { fn set_window_level(&self, _level: WindowLevel) {} - fn set_window_icon(&self, _window_icon: Option) {} + fn set_window_icon(&self, window_icon: Option) { + self.window_state.lock().unwrap().set_window_icon(window_icon) + } #[inline] fn set_ime_cursor_area(&self, position: Position, size: Size) { diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 9f69b022..8d87d665 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -7,6 +7,7 @@ use std::time::Duration; use ahash::HashSet; use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Size}; use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt}; +use sctk::globals::GlobalData; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_shm::WlShm; @@ -27,6 +28,7 @@ use sctk::shm::slot::SlotPool; use sctk::shm::Shm; use sctk::subcompositor::SubcompositorState; use tracing::{info, warn}; +use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_manager_v1::XdgToplevelIconManagerV1; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor}; use winit_core::error::{NotSupportedError, RequestError}; @@ -40,6 +42,7 @@ use crate::seat::{ use crate::state::{WindowCompositorUpdate, WinitState}; use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor}; use crate::types::kwin_blur::KWinBlurManager; +use crate::types::xdg_toplevel_icon_manager::ToplevelIcon; #[cfg(feature = "sctk-adwaita")] pub type WinitFrame = sctk_adwaita::AdwaitaFrame; @@ -58,9 +61,6 @@ pub struct WindowState { /// The `Shm` to set cursor. pub shm: WlShm, - // A shared pool where to allocate custom cursors. - custom_cursor_pool: Arc>, - /// The last received configure. pub last_configure: Option, @@ -84,6 +84,15 @@ pub struct WindowState { /// The current window title. title: String, + /// Xdg toplevel icon manager to request icon setting. + xdg_toplevel_icon_manager: Option, + + /// The current window toplevel icon + toplevel_icon: Option, + + /// A shared pool where to allocate images (used for window icons and custom cursors) + image_pool: Arc>, + /// Whether the frame is resizable. resizable: bool, @@ -180,7 +189,14 @@ impl WindowState { .as_ref() .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); + let xdg_toplevel_icon_manager = winit_state + .xdg_toplevel_icon_manager + .as_ref() + .map(|toplevel_icon_manager_state| toplevel_icon_manager_state.global().clone()); + Self { + toplevel_icon: None, + xdg_toplevel_icon_manager, blur: None, blur_manager: winit_state.kwin_blur_manager.clone(), compositor, @@ -206,7 +222,7 @@ impl WindowState { resizable: true, scale_factor: 1., shm: winit_state.shm.wl_shm().clone(), - custom_cursor_pool: winit_state.custom_cursor_pool.clone(), + image_pool: winit_state.image_pool.clone(), size: initial_size.to_logical(1.), stateless_size: initial_size.to_logical(1.), initial_size: Some(initial_size), @@ -711,7 +727,7 @@ impl WindowState { }; let cursor = { - let mut pool = self.custom_cursor_pool.lock().unwrap(); + let mut pool = self.image_pool.lock().unwrap(); CustomCursor::new(&mut pool, cursor) }; @@ -1069,6 +1085,45 @@ impl WindowState { self.title = title; } + /// Set the window's icon + pub fn set_window_icon(&mut self, window_icon: Option) { + let xdg_toplevel_icon_manager = match self.xdg_toplevel_icon_manager.as_ref() { + Some(xdg_toplevel_icon_manager) => xdg_toplevel_icon_manager, + None => { + warn!("`xdg_toplevel_icon_manager_v1` is not supported"); + return; + }, + }; + + let (toplevel_icon, xdg_toplevel_icon) = match window_icon { + Some(icon) => { + let mut image_pool = self.image_pool.lock().unwrap(); + let toplevel_icon = match ToplevelIcon::new(icon, &mut image_pool) { + Ok(toplevel_icon) => toplevel_icon, + Err(error) => { + warn!("Error setting window icon: {error}"); + return; + }, + }; + + let xdg_toplevel_icon = + xdg_toplevel_icon_manager.create_icon(&self.queue_handle, GlobalData); + + toplevel_icon.add_buffer(&xdg_toplevel_icon); + + (Some(toplevel_icon), Some(xdg_toplevel_icon)) + }, + None => (None, None), + }; + + xdg_toplevel_icon_manager.set_icon(self.window.xdg_toplevel(), xdg_toplevel_icon.as_ref()); + self.toplevel_icon = toplevel_icon; + + if let Some(xdg_toplevel_icon) = xdg_toplevel_icon { + xdg_toplevel_icon.destroy(); + } + } + /// Mark the window as transparent. #[inline] pub fn set_transparent(&mut self, transparent: bool) { diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index d382032a..84db1a3d 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -81,6 +81,7 @@ changelog entry. - `keyboard::ModifiersKey` to track which modifier is exactly pressed. - `ActivationToken::as_raw` to get a ref to raw token. - Each platform now has corresponding `WindowAttributes` struct instead of trait extension. +- On Wayland, added implementation for `Window::set_window_icon` ### Changed