wayland: add xdg_toplevel_icon_v1 support

Closes: #3859.
This commit is contained in:
Enn3Developer 2025-06-22 12:40:05 +02:00 committed by GitHub
parent 552c7a6252
commit fe1eab07ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 227 additions and 32 deletions

View file

@ -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<Icon>);
/// Set the IME cursor editing area, where the `position` is the top left corner of that area

View file

@ -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<u32>, 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<Buffer, CreateBufferError> {
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)
}

View file

@ -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<Mutex<SlotPool>>,
/// 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<XdgActivationState>,
/// Xdg toplevel icon manager
pub xdg_toplevel_icon_manager: Option<XdgToplevelIconManagerState>,
/// The pool where images are allocated (used for window icons and custom cursors)
pub image_pool: Arc<Mutex<SlotPool>>,
/// Relative pointer.
pub relative_pointer: Option<RelativePointerState>,
@ -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(),

View file

@ -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,

View file

@ -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;

View file

@ -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<Self, ToplevelIconError> {
let icon = match icon.cast_ref::<RgbaIcon>() {
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<WinitState>,
) -> Result<Self, BindError> {
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<XdgToplevelIconManagerV1, GlobalData, WinitState> for XdgToplevelIconManagerState {
fn event(
_state: &mut WinitState,
_proxy: &XdgToplevelIconManagerV1,
_event: <XdgToplevelIconManagerV1 as Proxy>::Event,
_data: &GlobalData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
// No events.
}
}
impl Dispatch<XdgToplevelIconV1, GlobalData, WinitState> for XdgToplevelIconManagerState {
fn event(
_state: &mut WinitState,
_proxy: &XdgToplevelIconV1,
_event: <XdgToplevelIconV1 as Proxy>::Event,
_data: &GlobalData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
// No events.
}
}
delegate_dispatch!(WinitState: [XdgToplevelIconManagerV1: GlobalData] => XdgToplevelIconManagerState);
delegate_dispatch!(WinitState: [XdgToplevelIconV1: GlobalData] => XdgToplevelIconManagerState);

View file

@ -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<winit_core::icon::Icon>) {}
fn set_window_icon(&self, window_icon: Option<winit_core::icon::Icon>) {
self.window_state.lock().unwrap().set_window_icon(window_icon)
}
#[inline]
fn set_ime_cursor_area(&self, position: Position, size: Size) {

View file

@ -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<WinitState>;
@ -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<Mutex<SlotPool>>,
/// The last received configure.
pub last_configure: Option<WindowConfigure>,
@ -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<XdgToplevelIconManagerV1>,
/// The current window toplevel icon
toplevel_icon: Option<ToplevelIcon>,
/// A shared pool where to allocate images (used for window icons and custom cursors)
image_pool: Arc<Mutex<SlotPool>>,
/// 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<winit_core::icon::Icon>) {
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) {

View file

@ -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