From 5429c18ad0a0a490393a65c4beb47444fd2d934d Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 9 Jul 2026 09:46:57 +0200 Subject: [PATCH 1/5] inital implementation of dialogs --- src/shell/xdg/dialog.rs | 329 ++++++++++++++++++++++++++++++++++ src/shell/xdg/mod.rs | 136 ++++++++++---- src/shell/xdg/popup.rs | 8 +- src/shell/xdg/window/inner.rs | 107 +++++------ src/shell/xdg/window/mod.rs | 14 ++ 5 files changed, 502 insertions(+), 92 deletions(-) create mode 100644 src/shell/xdg/dialog.rs diff --git a/src/shell/xdg/dialog.rs b/src/shell/xdg/dialog.rs new file mode 100644 index 0000000000..59b62bb361 --- /dev/null +++ b/src/shell/xdg/dialog.rs @@ -0,0 +1,329 @@ +use crate::reexports::client::{protocol::wl_compositor::WlCompositor, Proxy, QueueHandle}; +use crate::reexports::client::{protocol::wl_surface, Connection, Dispatch}; +use crate::reexports::protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use crate::shell::xdg::window::inner::{ + determine_decoration_mode, determine_window_state, determine_wm_capabilities, WindowInner, +}; +use crate::shell::xdg::window::WindowConfigure; +use crate::shell::xdg::Dispatch2; +use crate::shell::xdg::WindowDecorations; +use crate::shell::WaylandSurface; +use crate::{ + compositor::{Surface, SurfaceData}, + globals::ProvidesBoundGlobal, +}; +use crate::{error::GlobalError, shell::xdg::XdgShellSurface}; +use std::num::NonZeroU32; +use std::sync::{Arc, Mutex, Weak}; +use wayland_protocols::xdg::{ + decoration::zv1::client::zxdg_toplevel_decoration_v1::{self, Mode}, + dialog::v1::client::xdg_dialog_v1::XdgDialogV1, + shell::client::xdg_wm_base, +}; +use wayland_protocols::xdg::{dialog::v1::client::xdg_dialog_v1, shell::client::xdg_surface}; +use wayland_protocols::xdg::{dialog::v1::client::xdg_wm_dialog_v1, shell::client::xdg_toplevel}; + +/// Handler for toplevel operations on a [`Dialog`] +pub trait DialogHandler: Sized { + /// Request to close a window. + /// + /// This request does not destroy the window. You must drop all [`Window`] handles to destroy the window. + /// This request may be sent either by the compositor or by some other mechanism (such as client side decorations). + fn request_close(&mut self, conn: &Connection, qh: &QueueHandle, window: &Dialog); + + /// Apply a suggested surface change. + /// + /// When this function is called, the compositor is requesting the window's size or state to change. + /// + /// Internally this function is called when the underlying `xdg_surface` is configured. Any extension + /// protocols that interface with xdg-shell are able to be notified that the surface's configure sequence + /// is complete by using this function. + /// + /// # Double buffering + /// + /// Configure events in Wayland are considered to be double buffered and the state of the window does not + /// change until committed. + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + window: &Dialog, + configure: WindowConfigure, + serial: u32, + ); +} + +#[derive(Debug, Clone)] +pub struct Dialog { + inner: Arc, +} + +#[derive(Debug)] +pub struct DialogData(pub(crate) Weak); + +#[derive(Debug)] +pub(crate) struct DialogInner { + pub xdg_dialog: XdgDialogV1, + pub window: WindowInner, +} + +impl Dialog { + pub fn new( + parent: &xdg_toplevel::XdgToplevel, + qh: &QueueHandle, + // TODO: is 6 correct? + compositor: &impl ProvidesBoundGlobal, + wm: &GLOBAL, + decoration_manager: Option<&ZxdgDecorationManagerV1>, + decorations: WindowDecorations, + ) -> Result + where + D: Dispatch> + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + 'static, + GLOBAL: ProvidesBoundGlobal + + ProvidesBoundGlobal, + { + let surface = Surface::new(compositor, qh)?; + let dialog = Self::from_surface(surface, parent, qh, wm, decoration_manager, decorations)?; + dialog.wl_surface().commit(); + Ok(dialog) + } + + pub fn from_surface( + surface: impl Into, + parent: &xdg_toplevel::XdgToplevel, + qh: &QueueHandle, + wm_base: &GLOBAL, + decoration_manager: Option<&ZxdgDecorationManagerV1>, + decorations: WindowDecorations, + ) -> Result + where + D: Dispatch + + Dispatch + + Dispatch + + Dispatch + + 'static, + GLOBAL: ProvidesBoundGlobal + + ProvidesBoundGlobal, + { + let surface = surface.into(); + let wm_dialog: xdg_wm_dialog_v1::XdgWmDialogV1 = wm_base.bound_global()?; + let wm_base: xdg_wm_base::XdgWmBase = wm_base.bound_global()?; + + // Freeze the queue during the creation of the Arc to avoid a race between events on the + // new objects being processed and the Weak in the PopupData becoming usable. + let freeze = qh.freeze(); + + let inner = Arc::new_cyclic(|weak| { + let xdg_surface = + wm_base.get_xdg_surface(surface.wl_surface(), qh, DialogData(weak.clone())); + let surface = XdgShellSurface { surface, xdg_surface }; + let xdg_toplevel = surface.xdg_surface.get_toplevel(qh, DialogData(weak.clone())); + xdg_toplevel.set_parent(Some(parent)); + let xdg_dialog = wm_dialog.get_xdg_dialog(&xdg_toplevel, qh, DialogData(weak.clone())); + + let toplevel_decoration = crate::shell::xdg::XdgShell::toplevel_decoration( + decoration_manager, + &xdg_toplevel, + decorations, + DialogData(weak.clone()), + qh, + ); + + DialogInner { + xdg_dialog, + window: WindowInner { + xdg_surface: surface, + xdg_toplevel, + toplevel_decoration, + pending_configure: Mutex::new(Default::default()), + }, + } + }); + drop(freeze); + let dialog = Dialog { inner }; + Ok(dialog) + } + + pub fn from_xdg_toplevel(toplevel: &xdg_toplevel::XdgToplevel) -> Option { + toplevel.data::().and_then(|data| data.dialog()) + } + + pub fn from_xdg_surface(surface: &xdg_surface::XdgSurface) -> Option { + surface.data::().and_then(|data| data.dialog()) + } + + pub fn from_toplevel_decoration( + decoration: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + ) -> Option { + decoration.data::().and_then(|data| data.dialog()) + } + + pub fn xdg_dialog(&self) -> &XdgDialogV1 { + &self.inner.xdg_dialog + } + + pub fn xdg_shell_surface(&self) -> &XdgShellSurface { + &self.inner.window.xdg_surface + } + + pub fn xdg_toplevel(&self) -> &xdg_toplevel::XdgToplevel { + &self.inner.window.xdg_toplevel + } + + pub fn xdg_surface(&self) -> &xdg_surface::XdgSurface { + self.inner.window.xdg_surface.xdg_surface() + } + + pub fn wl_surface(&self) -> &wl_surface::WlSurface { + self.inner.window.xdg_surface.wl_surface() + } + + pub fn set_modal(&self, modal: bool) { + if modal { + self.inner.xdg_dialog.set_modal(); + } else { + self.inner.xdg_dialog.unset_modal(); + } + } +} + +impl WaylandSurface for Dialog { + fn wl_surface(&self) -> &wl_surface::WlSurface { + self.wl_surface() + } +} + +impl DialogData { + /// Get a new handle to the Dialog + /// + /// This returns `None` if the dialog has been destroyed. + pub fn dialog(&self) -> Option { + let inner = self.0.upgrade()?; + Some(Dialog { inner }) + } +} + +impl Drop for DialogInner { + fn drop(&mut self) { + self.xdg_dialog.destroy(); + } +} + +impl Dispatch2 for DialogData { + fn event( + &self, + data: &mut D, + xdg_surface: &xdg_surface::XdgSurface, + event: ::Event, + conn: &Connection, + qhandle: &QueueHandle, + ) { + if let Some(dialog) = Dialog::from_xdg_surface(xdg_surface) { + match event { + xdg_surface::Event::Configure { serial } => { + xdg_surface.ack_configure(serial); + + let configure = dialog.inner.window.pending_configure.lock().unwrap().clone(); + DialogHandler::configure(data, conn, qhandle, &dialog, configure, serial) + } + _ => unreachable!(), + } + } + } +} + +impl Dispatch2 for DialogData { + fn event( + &self, + _state: &mut D, + _proxy: &XdgDialogV1, + _event: ::Event, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch2 for DialogData { + fn event( + &self, + data: &mut D, + toplevel: &xdg_toplevel::XdgToplevel, + event: ::Event, + conn: &Connection, + qhandle: &QueueHandle, + ) { + let Some(dialog) = Dialog::from_xdg_toplevel(toplevel) else { + return; + }; + + match event { + xdg_toplevel::Event::Configure { width, height, states } => { + let new_state = determine_window_state(&states); + + // XXX we do explicit convertion and sanity checking because compositor + // could pass negative values which we should ignore all together. + let width = u32::try_from(width).ok().and_then(NonZeroU32::new); + let height = u32::try_from(height).ok().and_then(NonZeroU32::new); + + let pending_configure = &mut dialog.inner.window.pending_configure.lock().unwrap(); + pending_configure.new_size = (width, height); + pending_configure.state = new_state; + } + xdg_toplevel::Event::Close => { + data.request_close(conn, qhandle, &dialog); + } + + xdg_toplevel::Event::ConfigureBounds { width, height } => { + let pending_configure = &mut dialog.inner.window.pending_configure.lock().unwrap(); + if width == 0 && height == 0 { + pending_configure.suggested_bounds = None; + } else { + pending_configure.suggested_bounds = Some((width as u32, height as u32)); + } + } + xdg_toplevel::Event::WmCapabilities { capabilities } => { + let pending_configure = &mut dialog.inner.window.pending_configure.lock().unwrap(); + pending_configure.capabilities = determine_wm_capabilities(&capabilities) + } + _ => unreachable!(), + } + } +} + +impl Dispatch2 for DialogData +where + D: DialogHandler, +{ + fn event( + &self, + _: &mut D, + decoration: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + event: zxdg_toplevel_decoration_v1::Event, + _: &Connection, + _: &QueueHandle, + ) { + if let Some(dialog) = Dialog::from_toplevel_decoration(decoration) { + match event { + zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { + wayland_client::WEnum::Value(mode) => { + let mode = determine_decoration_mode(mode); + dialog.inner.window.pending_configure.lock().unwrap().decoration_mode = + mode; + } + + wayland_client::WEnum::Unknown(unknown) => { + log::error!(target: "sctk", "unknown decoration mode 0x{:x}", unknown); + } + }, + + _ => unreachable!(), + } + } + } +} diff --git a/src/shell/xdg/mod.rs b/src/shell/xdg/mod.rs index 65a454cb39..929c1a39ec 100644 --- a/src/shell/xdg/mod.rs +++ b/src/shell/xdg/mod.rs @@ -4,14 +4,18 @@ use std::os::unix::io::OwnedFd; use std::sync::{Arc, Mutex}; +use wayland_protocols::xdg::dialog::v1::client::xdg_wm_dialog_v1; + use crate::reexports::client::globals::{BindError, GlobalList}; use crate::reexports::client::Connection; use crate::reexports::client::{protocol::wl_surface, Dispatch, Proxy, QueueHandle}; use crate::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; +use crate::reexports::protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; use crate::reexports::protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::Mode; use crate::reexports::protocols::xdg::decoration::zv1::client::{ zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, }; +use crate::reexports::protocols::xdg::dialog::v1::client::xdg_dialog_v1; use crate::reexports::protocols::xdg::shell::client::{ xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base, }; @@ -21,6 +25,7 @@ use crate::dispatch2::Dispatch2; use crate::error::GlobalError; use crate::globals::{GlobalData, ProvidesBoundGlobal}; use crate::registry::GlobalProxy; +use crate::shell::xdg::dialog::{Dialog, DialogData, DialogHandler}; use self::window::inner::WindowInner; use self::window::{ @@ -29,6 +34,7 @@ use self::window::{ use super::WaylandSurface; +pub mod dialog; pub mod fallback_frame; pub mod popup; pub mod window; @@ -36,6 +42,7 @@ pub mod window; /// The xdg shell globals. #[derive(Debug)] pub struct XdgShell { + xdg_wm_dialog_v1: xdg_wm_dialog_v1::XdgWmDialogV1, xdg_wm_base: xdg_wm_base::XdgWmBase, xdg_decoration_manager: GlobalProxy, } @@ -58,12 +65,54 @@ impl XdgShell { pub fn bind(globals: &GlobalList, qh: &QueueHandle) -> Result where State: Dispatch + + Dispatch + Dispatch + 'static, { let xdg_wm_base = globals.bind(qh, 1..=Self::API_VERSION_MAX, GlobalData)?; + let xdg_wm_dialog_v1 = globals.bind(qh, 1..=1, GlobalData)?; let xdg_decoration_manager = GlobalProxy::from(globals.bind(qh, 1..=1, GlobalData)); - Ok(Self { xdg_wm_base, xdg_decoration_manager }) + Ok(Self { xdg_wm_base, xdg_wm_dialog_v1, xdg_decoration_manager }) + } + + pub(crate) fn toplevel_decoration( + decoration_manager: Option<&ZxdgDecorationManagerV1>, + xdg_toplevel: &xdg_toplevel::XdgToplevel, + decorations: WindowDecorations, + data: D, + qh: &QueueHandle, + ) -> Option + where + D: Send + Sync + 'static, + State: Dispatch + 'static, + { + // If server side decorations are available, create the toplevel decoration. + let toplevel_decoration = decoration_manager.and_then(|decoration_manager| { + match decorations { + // Window does not want any server side decorations. + WindowDecorations::ClientOnly | WindowDecorations::None => None, + + _ => { + // Create the toplevel decoration. + let toplevel_decoration = + decoration_manager.get_toplevel_decoration(xdg_toplevel, qh, data); + + // Tell the compositor we would like a specific mode. + let mode = match decorations { + WindowDecorations::RequestServer => Some(Mode::ServerSide), + WindowDecorations::RequestClient => Some(Mode::ClientSide), + _ => None, + }; + + if let Some(mode) = mode { + toplevel_decoration.set_mode(mode); + } + + Some(toplevel_decoration) + } + } + }); + toplevel_decoration } /// Creates a new, unmapped window. @@ -109,49 +158,19 @@ impl XdgShell { let xdg_surface = XdgShellSurface { surface, xdg_surface }; let xdg_toplevel = xdg_surface.xdg_surface().get_toplevel(qh, WindowData(weak.clone())); - // If server side decorations are available, create the toplevel decoration. - let toplevel_decoration = decoration_manager.and_then(|decoration_manager| { - match decorations { - // Window does not want any server side decorations. - WindowDecorations::ClientOnly | WindowDecorations::None => None, - - _ => { - // Create the toplevel decoration. - let toplevel_decoration = decoration_manager.get_toplevel_decoration( - &xdg_toplevel, - qh, - WindowData(weak.clone()), - ); - - // Tell the compositor we would like a specific mode. - let mode = match decorations { - WindowDecorations::RequestServer => Some(Mode::ServerSide), - WindowDecorations::RequestClient => Some(Mode::ClientSide), - _ => None, - }; - - if let Some(mode) = mode { - toplevel_decoration.set_mode(mode); - } - - Some(toplevel_decoration) - } - } - }); + let toplevel_decoration = Self::toplevel_decoration( + decoration_manager, + &xdg_toplevel, + decorations, + WindowData(weak.clone()), + qh, + ); WindowInner { xdg_surface, xdg_toplevel, toplevel_decoration, - pending_configure: Mutex::new(WindowConfigure { - new_size: (None, None), - suggested_bounds: None, - // Initial configure will indicate whether there are server side decorations. - decoration_mode: DecorationMode::Client, - state: WindowState::empty(), - // XXX by default we assume that everything is supported. - capabilities: WindowManagerCapabilities::all(), - }), + pending_configure: Mutex::new(Default::default()), } }); @@ -161,6 +180,26 @@ impl XdgShell { Window(inner) } + #[must_use = "Dropping all dialog handles will destroy the dialog"] + pub fn create_dialog( + &self, + surface: impl Into, + decorations: WindowDecorations, + qh: &QueueHandle, + parent: &xdg_toplevel::XdgToplevel, + ) -> Result + where + State: Dispatch + + Dispatch + + Dispatch + + Dispatch + + DialogHandler + + 'static, + { + let decoration_manager = self.xdg_decoration_manager.get().ok(); + Dialog::from_surface(surface, parent, qh, self, decoration_manager, decorations) + } + pub fn xdg_wm_base(&self) -> &xdg_wm_base::XdgWmBase { &self.xdg_wm_base } @@ -312,6 +351,25 @@ impl ProvidesBoundGlobal } } +impl ProvidesBoundGlobal for XdgShell { + fn bound_global(&self) -> Result { + Ok(self.xdg_wm_dialog_v1.clone()) + } +} + +impl Dispatch2 for GlobalData { + fn event( + &self, + _: &mut D, + _: &xdg_wm_dialog_v1::XdgWmDialogV1, + _: xdg_wm_dialog_v1::Event, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("xdg_wm_dialog_v1 has no events") + } +} + impl Dispatch2 for GlobalData { fn event( &self, diff --git a/src/shell/xdg/popup.rs b/src/shell/xdg/popup.rs index 243dd7a001..fb0f1d590d 100644 --- a/src/shell/xdg/popup.rs +++ b/src/shell/xdg/popup.rs @@ -3,7 +3,7 @@ use crate::{ dispatch2::Dispatch2, error::GlobalError, globals::ProvidesBoundGlobal, - shell::xdg::XdgShellSurface, + shell::{xdg::XdgShellSurface, WaylandSurface}, }; use std::sync::{ atomic::{AtomicI32, AtomicU32, Ordering::Relaxed}, @@ -139,6 +139,12 @@ impl Popup { } } +impl WaylandSurface for Popup { + fn wl_surface(&self) -> &wl_surface::WlSurface { + self.wl_surface() + } +} + impl PopupData { /// Get a new handle to the Popup /// diff --git a/src/shell/xdg/window/inner.rs b/src/shell/xdg/window/inner.rs index 96a276a730..9cb4cabc42 100644 --- a/src/shell/xdg/window/inner.rs +++ b/src/shell/xdg/window/inner.rs @@ -86,6 +86,58 @@ where } } +pub(crate) fn determine_window_state(states: &[u8]) -> WindowState { + // The states are encoded as a bunch of u32 of native endian, but are encoded in an array of + // bytes. + states + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .flat_map(State::try_from) + .fold(WindowState::empty(), |mut acc, state| { + match state { + State::Maximized => acc.set(WindowState::MAXIMIZED, true), + State::Fullscreen => acc.set(WindowState::FULLSCREEN, true), + State::Resizing => acc.set(WindowState::RESIZING, true), + State::Activated => acc.set(WindowState::ACTIVATED, true), + State::TiledLeft => acc.set(WindowState::TILED_LEFT, true), + State::TiledRight => acc.set(WindowState::TILED_RIGHT, true), + State::TiledTop => acc.set(WindowState::TILED_TOP, true), + State::TiledBottom => acc.set(WindowState::TILED_BOTTOM, true), + State::Suspended => acc.set(WindowState::SUSPENDED, true), + _ => (), + } + acc + }) +} + +pub(crate) fn determine_wm_capabilities(capabilities: &[u8]) -> WindowManagerCapabilities { + capabilities + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .flat_map(WmCapabilities::try_from) + .fold(WindowManagerCapabilities::empty(), |mut acc, capability| { + match capability { + WmCapabilities::WindowMenu => acc.set(WindowManagerCapabilities::WINDOW_MENU, true), + WmCapabilities::Maximize => acc.set(WindowManagerCapabilities::MAXIMIZE, true), + WmCapabilities::Fullscreen => acc.set(WindowManagerCapabilities::FULLSCREEN, true), + WmCapabilities::Minimize => acc.set(WindowManagerCapabilities::MINIMIZE, true), + _ => (), + } + acc + }) +} + +pub(crate) fn determine_decoration_mode(mode: Mode) -> DecorationMode { + match mode { + Mode::ClientSide => DecorationMode::Client, + Mode::ServerSide => DecorationMode::Server, + + _ => unreachable!(), + } +} + impl Dispatch2 for WindowData where D: WindowHandler, @@ -101,28 +153,7 @@ where if let Some(window) = Window::from_xdg_toplevel(toplevel) { match event { xdg_toplevel::Event::Configure { width, height, states } => { - // The states are encoded as a bunch of u32 of native endian, but are encoded in an array of - // bytes. - let new_state = states - .chunks_exact(4) - .flat_map(TryInto::<[u8; 4]>::try_into) - .map(u32::from_ne_bytes) - .flat_map(State::try_from) - .fold(WindowState::empty(), |mut acc, state| { - match state { - State::Maximized => acc.set(WindowState::MAXIMIZED, true), - State::Fullscreen => acc.set(WindowState::FULLSCREEN, true), - State::Resizing => acc.set(WindowState::RESIZING, true), - State::Activated => acc.set(WindowState::ACTIVATED, true), - State::TiledLeft => acc.set(WindowState::TILED_LEFT, true), - State::TiledRight => acc.set(WindowState::TILED_RIGHT, true), - State::TiledTop => acc.set(WindowState::TILED_TOP, true), - State::TiledBottom => acc.set(WindowState::TILED_BOTTOM, true), - State::Suspended => acc.set(WindowState::SUSPENDED, true), - _ => (), - } - acc - }); + let new_state = determine_window_state(&states); // XXX we do explicit convertion and sanity checking because compositor // could pass negative values which we should ignore all together. @@ -148,29 +179,7 @@ where } xdg_toplevel::Event::WmCapabilities { capabilities } => { let pending_configure = &mut window.0.pending_configure.lock().unwrap(); - pending_configure.capabilities = capabilities - .chunks_exact(4) - .flat_map(TryInto::<[u8; 4]>::try_into) - .map(u32::from_ne_bytes) - .flat_map(WmCapabilities::try_from) - .fold(WindowManagerCapabilities::empty(), |mut acc, capability| { - match capability { - WmCapabilities::WindowMenu => { - acc.set(WindowManagerCapabilities::WINDOW_MENU, true) - } - WmCapabilities::Maximize => { - acc.set(WindowManagerCapabilities::MAXIMIZE, true) - } - WmCapabilities::Fullscreen => { - acc.set(WindowManagerCapabilities::FULLSCREEN, true) - } - WmCapabilities::Minimize => { - acc.set(WindowManagerCapabilities::MINIMIZE, true) - } - _ => (), - } - acc - }); + pending_configure.capabilities = determine_wm_capabilities(&capabilities) } _ => unreachable!(), } @@ -212,13 +221,7 @@ where match event { zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { wayland_client::WEnum::Value(mode) => { - let mode = match mode { - Mode::ClientSide => DecorationMode::Client, - Mode::ServerSide => DecorationMode::Server, - - _ => unreachable!(), - }; - + let mode = determine_decoration_mode(mode); window.0.pending_configure.lock().unwrap().decoration_mode = mode; } diff --git a/src/shell/xdg/window/mod.rs b/src/shell/xdg/window/mod.rs index 69527f8a93..a6a155e5fe 100644 --- a/src/shell/xdg/window/mod.rs +++ b/src/shell/xdg/window/mod.rs @@ -98,6 +98,20 @@ pub struct WindowConfigure { pub capabilities: WindowManagerCapabilities, } +impl Default for WindowConfigure { + fn default() -> Self { + Self { + new_size: (None, None), + suggested_bounds: None, + // Initial configure will indicate whether there are server side decorations. + decoration_mode: DecorationMode::Client, + state: WindowState::empty(), + // XXX by default we assume that everything is supported. + capabilities: WindowManagerCapabilities::all(), + } + } +} + impl WindowConfigure { /// Is [`WindowState::MAXIMIZED`] state is set. #[inline] From d1f5bbf7a193ea75feab91af3c6248e6c094f7c5 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 9 Jul 2026 13:04:05 +0200 Subject: [PATCH 2/5] do not make XdgDialogV1 mandatory --- src/shell/xdg/dialog.rs | 5 ++--- src/shell/xdg/mod.rs | 12 +++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/shell/xdg/dialog.rs b/src/shell/xdg/dialog.rs index 59b62bb361..f6d3797bb3 100644 --- a/src/shell/xdg/dialog.rs +++ b/src/shell/xdg/dialog.rs @@ -16,9 +16,8 @@ use crate::{error::GlobalError, shell::xdg::XdgShellSurface}; use std::num::NonZeroU32; use std::sync::{Arc, Mutex, Weak}; use wayland_protocols::xdg::{ - decoration::zv1::client::zxdg_toplevel_decoration_v1::{self, Mode}, - dialog::v1::client::xdg_dialog_v1::XdgDialogV1, - shell::client::xdg_wm_base, + decoration::zv1::client::zxdg_toplevel_decoration_v1, + dialog::v1::client::xdg_dialog_v1::XdgDialogV1, shell::client::xdg_wm_base, }; use wayland_protocols::xdg::{dialog::v1::client::xdg_dialog_v1, shell::client::xdg_surface}; use wayland_protocols::xdg::{dialog::v1::client::xdg_wm_dialog_v1, shell::client::xdg_toplevel}; diff --git a/src/shell/xdg/mod.rs b/src/shell/xdg/mod.rs index 929c1a39ec..7b684c3bd3 100644 --- a/src/shell/xdg/mod.rs +++ b/src/shell/xdg/mod.rs @@ -42,7 +42,7 @@ pub mod window; /// The xdg shell globals. #[derive(Debug)] pub struct XdgShell { - xdg_wm_dialog_v1: xdg_wm_dialog_v1::XdgWmDialogV1, + xdg_wm_dialog_v1: Option, xdg_wm_base: xdg_wm_base::XdgWmBase, xdg_decoration_manager: GlobalProxy, } @@ -70,7 +70,7 @@ impl XdgShell { + 'static, { let xdg_wm_base = globals.bind(qh, 1..=Self::API_VERSION_MAX, GlobalData)?; - let xdg_wm_dialog_v1 = globals.bind(qh, 1..=1, GlobalData)?; + let xdg_wm_dialog_v1 = globals.bind(qh, 1..=1, GlobalData).ok(); let xdg_decoration_manager = GlobalProxy::from(globals.bind(qh, 1..=1, GlobalData)); Ok(Self { xdg_wm_base, xdg_wm_dialog_v1, xdg_decoration_manager }) } @@ -351,12 +351,17 @@ impl ProvidesBoundGlobal } } +/// Dialog impl ProvidesBoundGlobal for XdgShell { fn bound_global(&self) -> Result { - Ok(self.xdg_wm_dialog_v1.clone()) + Ok(self + .xdg_wm_dialog_v1 + .clone() + .ok_or(GlobalError::MissingGlobal("Dialog v1 is not available"))?) } } +/// Dialog impl Dispatch2 for GlobalData { fn event( &self, @@ -370,6 +375,7 @@ impl Dispatch2 for GlobalData { } } +/// Dialog impl Dispatch2 for GlobalData { fn event( &self, From b0151779ec3f944f53be60edec07a40ddb902400 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 9 Jul 2026 13:04:28 +0200 Subject: [PATCH 3/5] fix copy paste errors --- src/shell/xdg/dialog.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shell/xdg/dialog.rs b/src/shell/xdg/dialog.rs index f6d3797bb3..faf1c5c41a 100644 --- a/src/shell/xdg/dialog.rs +++ b/src/shell/xdg/dialog.rs @@ -24,9 +24,9 @@ use wayland_protocols::xdg::{dialog::v1::client::xdg_wm_dialog_v1, shell::client /// Handler for toplevel operations on a [`Dialog`] pub trait DialogHandler: Sized { - /// Request to close a window. + /// Request to close a dialog. /// - /// This request does not destroy the window. You must drop all [`Window`] handles to destroy the window. + /// This request does not destroy the dialog. You must drop all [`Dialog`] handles to destroy the dialog. /// This request may be sent either by the compositor or by some other mechanism (such as client side decorations). fn request_close(&mut self, conn: &Connection, qh: &QueueHandle, window: &Dialog); @@ -114,7 +114,7 @@ impl Dialog { let wm_base: xdg_wm_base::XdgWmBase = wm_base.bound_global()?; // Freeze the queue during the creation of the Arc to avoid a race between events on the - // new objects being processed and the Weak in the PopupData becoming usable. + // new objects being processed and the Weak in the DialogData becoming usable. let freeze = qh.freeze(); let inner = Arc::new_cyclic(|weak| { From 98dcb9a0dd38765ceff60495444eeac7f95bd49c Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 9 Jul 2026 14:08:49 +0200 Subject: [PATCH 4/5] add an example --- examples/dialog.rs | 557 ++++++++++++++++++++++++++++++++++++++++ src/shell/xdg/dialog.rs | 6 + 2 files changed, 563 insertions(+) create mode 100644 examples/dialog.rs diff --git a/examples/dialog.rs b/examples/dialog.rs new file mode 100644 index 0000000000..15f6f7514f --- /dev/null +++ b/examples/dialog.rs @@ -0,0 +1,557 @@ +use std::collections::hash_map::RandomState; +use std::env; +use std::hash::{BuildHasher, Hasher}; + +use smithay_client_toolkit::reexports::calloop::{EventLoop, LoopHandle}; +use smithay_client_toolkit::seat::keyboard::KeyboardHandler; +use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; +use smithay_client_toolkit::shell::xdg::dialog::{Dialog, DialogHandler}; +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState, FrameCallbackData}, + delegate_registry, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + shell::{ + xdg::{ + window::{Window, WindowConfigure, WindowDecorations, WindowHandler}, + XdgShell, + }, + WaylandSurface, + }, + shm::{ + slot::{Buffer, SlotPool}, + Shm, ShmHandler, + }, +}; +use wayland_client::protocol::wl_keyboard; +use wayland_client::protocol::wl_seat; +use wayland_client::{ + globals::registry_queue_init, + protocol::{wl_output, wl_shm, wl_surface}, + Connection, QueueHandle, +}; + +#[derive(PartialEq)] +enum WindowType { + Window(Window), + Dialog(Dialog), +} + +impl WaylandSurface for WindowType { + fn wl_surface(&self) -> &wl_surface::WlSurface { + match self { + Self::Window(window) => window.wl_surface(), + Self::Dialog(dialog) => dialog.wl_surface(), + } + } +} + +struct Viewer { + window: WindowType, + width: u32, + height: u32, + buffer: Option, + first_configure: bool, + damaged: bool, +} + +struct State { + registry_state: RegistryState, + output_state: OutputState, + compositor_state: CompositorState, + shm_state: Shm, + xdg_shell_state: XdgShell, + pool: Option, + windows: Vec, + + have_dialog: bool, + loop_handle: LoopHandle<'static, State>, + seat_state: SeatState, + keyboard: Option, +} + +/// Creates a simple red window +/// When pressing any keyboard key a dialog (green window) is created +/// this window is modal to the red window so that first this dialog must be close +/// by pressing again any key +/// The modality can be changed by setting set_modal(false) on the dialog +fn main() { + env_logger::init(); + + let conn = Connection::connect_to_env().unwrap(); + + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh: QueueHandle = event_queue.handle(); + + let event_loop: EventLoop = + EventLoop::try_new().expect("Failed to initialize the event loop!"); + + let mut state = State { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + compositor_state: CompositorState::bind(&globals, &qh) + .expect("wl_compositor not available"), + shm_state: Shm::bind(&globals, &qh).expect("wl_shm not available"), + xdg_shell_state: XdgShell::bind(&globals, &qh).expect("xdg shell not available"), + + pool: None, + windows: Vec::new(), + have_dialog: false, + + loop_handle: event_loop.handle(), + seat_state: SeatState::new(&globals, &qh), + keyboard: None, + }; + + let surface = state.compositor_state.create_surface(&qh); + let window = + state.xdg_shell_state.create_window(surface, WindowDecorations::ServerDefault, &qh); + window.set_title("A wayland window"); + // GitHub does not let projects use the `org.github` domain but the `io.github` domain is fine. + window.set_app_id("io.github.smithay.client-toolkit.Dialog"); + + // In order for the window to be mapped, we need to perform an initial commit with no attached buffer. + // For more info, see WaylandSurface::commit + // + // The compositor will respond with an initial configure that we can then use to present to the window with + // the correct options. + window.commit(); + + state.windows.push(Viewer { + width: 500, + height: 500, + window: WindowType::Window(window), + first_configure: true, + damaged: true, + buffer: None, + }); + + let pool = SlotPool::new(2, &state.shm_state).expect("Failed to create pool"); + state.pool = Some(pool); + + loop { + event_queue.blocking_dispatch(&mut state).unwrap(); + + if state.windows.is_empty() { + println!("exiting example"); + break; + } + } +} + +impl State { + pub fn draw(&mut self, _conn: &Connection, qh: &QueueHandle) { + for viewer in &mut self.windows { + if viewer.first_configure || !viewer.damaged { + continue; + } + let window = &viewer.window; + let width = viewer.width; + let height = viewer.height; + let stride = viewer.width as i32 * 4; + let pool = self.pool.as_mut().unwrap(); + + let buffer = viewer.buffer.get_or_insert_with(|| { + pool.create_buffer(width as i32, height as i32, stride, wl_shm::Format::Argb8888) + .expect("create buffer") + .0 + }); + + let canvas = match pool.canvas(buffer) { + Some(canvas) => canvas, + None => { + // This should be rare, but if the compositor has not released the previous + // buffer, we need double-buffering. + let (second_buffer, canvas) = pool + .create_buffer( + viewer.width as i32, + viewer.height as i32, + stride, + wl_shm::Format::Argb8888, + ) + .expect("create buffer"); + *buffer = second_buffer; + canvas + } + }; + + // Fill the window with a random color: + { + let (r, g, b) = if matches!(viewer.window, WindowType::Window(..)) { + (0xff, 0x00, 0x00) + } else { + (0x00, 0xff, 0x00) + }; + + for argb in canvas.chunks_exact_mut(4) { + // Send pixels to the server in ARGB8888 format (this is one of the only + // formats that are guaranteed to be supported). + argb[3] = 0xff; + argb[2] = r; + argb[1] = g; + argb[0] = b; + } + } + + // Damage the entire window + window.wl_surface().damage_buffer(0, 0, viewer.width as i32, viewer.height as i32); + viewer.damaged = false; + + // Request our next frame + window.wl_surface().frame(qh, FrameCallbackData(window.wl_surface().clone())); + + // Attach and commit to present. + buffer.attach_to(window.wl_surface()).expect("buffer attach"); + window.wl_surface().commit(); + } + } +} + +impl PartialEq for WindowType { + fn eq(&self, other: &Window) -> bool { + match self { + Self::Window(window) => window == other, + _ => false, + } + } +} + +impl PartialEq for WindowType { + fn eq(&self, other: &Dialog) -> bool { + match self { + Self::Dialog(dialog) => dialog == other, + _ => false, + } + } +} + +// Trait implementations + +impl CompositorHandler for State { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_factor: i32, + ) { + // Not needed for this example. + } + + fn transform_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_transform: wl_output::Transform, + ) { + // Not needed for this example. + } + + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.draw(conn, qh); + } + + fn surface_enter( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _output: &wl_output::WlOutput, + ) { + // Not needed for this example. + } + + fn surface_leave( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _output: &wl_output::WlOutput, + ) { + // Not needed for this example. + } +} + +impl OutputHandler for State { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } +} + +impl WindowHandler for State { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, window: &Window) { + println!("Closing a Window"); + self.windows.retain(|v| v.window != *window); + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + for viewer in &mut self.windows { + if viewer.window != *window { + continue; + } + + viewer.buffer = None; + viewer.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + viewer.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + viewer.damaged = true; + + // Initiate the first draw. + viewer.first_configure = false; + } + self.draw(conn, qh); + } +} + +impl DialogHandler for State { + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + window: &smithay_client_toolkit::shell::xdg::dialog::Dialog, + configure: WindowConfigure, + serial: u32, + ) { + for viewer in &mut self.windows { + if viewer.window != *window { + continue; + } + + viewer.buffer = None; + viewer.width = configure.new_size.0.map(|v| v.get()).unwrap_or(100); + viewer.height = configure.new_size.1.map(|v| v.get()).unwrap_or(100); + viewer.damaged = true; + + // Initiate the first draw. + viewer.first_configure = false; + } + self.draw(conn, qh); + } + + fn request_close( + &mut self, + conn: &Connection, + qh: &QueueHandle, + window: &smithay_client_toolkit::shell::xdg::dialog::Dialog, + ) { + println!("Closing a Popup"); + self.windows.retain(|v| v.window != *window); + self.have_dialog = false; + } +} + +impl SeatHandler for State { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_none() { + println!("Set keyboard capability"); + let keyboard = self + .seat_state + .get_keyboard_with_repeat( + qh, + &seat, + None, + self.loop_handle.clone(), + Box::new(|_state, _wl_kbd, event| { + println!("Repeat: {:?} ", event); + }), + ) + .expect("Failed to create keyboard"); + + self.keyboard = Some(keyboard); + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _: &QueueHandle, + _: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_some() { + println!("Unset keyboard capability"); + self.keyboard.take().unwrap().release(); + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +} + +impl KeyboardHandler for State { + fn enter( + &mut self, + conn: &Connection, + qh: &QueueHandle, + keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + surface: &wl_surface::WlSurface, + serial: u32, + raw: &[u32], + keysyms: &[xkeysym::Keysym], + ) { + } + + fn leave( + &mut self, + conn: &Connection, + qh: &QueueHandle, + keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + surface: &wl_surface::WlSurface, + serial: u32, + ) { + } + + fn press_key( + &mut self, + conn: &Connection, + qh: &QueueHandle, + keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + event: smithay_client_toolkit::seat::keyboard::KeyEvent, + ) { + if self.have_dialog { + // Close the dialog + self.windows.retain(|w| matches!(w.window, WindowType::Window(..))); + self.have_dialog = false; + } else { + // Create a new dialog + self.have_dialog = true; + let surface = self.compositor_state.create_surface(qh); + if let WindowType::Window(window) = &self.windows.first().unwrap().window { + let window = window.clone(); + let parent_surface = window.xdg_toplevel(); + let dialog = self + .xdg_shell_state + .create_dialog(surface, WindowDecorations::ServerDefault, qh, parent_surface) + .unwrap(); + dialog.commit(); + dialog.set_modal(true); + self.windows.push(Viewer { + window: WindowType::Dialog(dialog), + width: 200, + height: 200, + buffer: None, + first_configure: true, + damaged: true, + }); + } + } + } + + fn release_key( + &mut self, + conn: &Connection, + qh: &QueueHandle, + keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + event: smithay_client_toolkit::seat::keyboard::KeyEvent, + ) { + } + + fn repeat_key( + &mut self, + conn: &Connection, + qh: &QueueHandle, + keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + event: smithay_client_toolkit::seat::keyboard::KeyEvent, + ) { + } + + fn update_keymap( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + _keymap: smithay_client_toolkit::seat::keyboard::Keymap<'_>, + ) { + } + + fn update_modifiers( + &mut self, + conn: &Connection, + qh: &QueueHandle, + keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + modifiers: smithay_client_toolkit::seat::keyboard::Modifiers, + raw_modifiers: smithay_client_toolkit::seat::keyboard::RawModifiers, + layout: u32, + ) { + } + + fn update_repeat_info( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, + _info: smithay_client_toolkit::seat::keyboard::RepeatInfo, + ) { + } +} + +impl ShmHandler for State { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +delegate_registry!(State); + +impl ProvidesRegistryState for State { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + registry_handlers!(OutputState); +} + +smithay_client_toolkit::delegate_dispatch2!(State); diff --git a/src/shell/xdg/dialog.rs b/src/shell/xdg/dialog.rs index faf1c5c41a..5a87e9460b 100644 --- a/src/shell/xdg/dialog.rs +++ b/src/shell/xdg/dialog.rs @@ -197,6 +197,12 @@ impl WaylandSurface for Dialog { } } +impl PartialEq for Dialog { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + impl DialogData { /// Get a new handle to the Dialog /// From 9ec449a9a5ec249d6da052d242a53fbd518fcfaa Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Tue, 14 Jul 2026 08:46:21 +0200 Subject: [PATCH 5/5] fix clippy --- src/seat/keyboard/mod.rs | 8 +++----- src/shell/xdg/mod.rs | 15 +++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/seat/keyboard/mod.rs b/src/seat/keyboard/mod.rs index 666f155c6e..804d44de66 100644 --- a/src/seat/keyboard/mod.rs +++ b/src/seat/keyboard/mod.rs @@ -457,11 +457,9 @@ impl KeyboardData { let xkb_context = self.xkb_context.lock().unwrap(); if let Some(locale) = env::var_os("LC_ALL") - .and_then(|v| if v.is_empty() { None } else { Some(v) }) - .or_else(|| env::var_os("LC_CTYPE")) - .and_then(|v| if v.is_empty() { None } else { Some(v) }) - .or_else(|| env::var_os("LANG")) - .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .filter(|v| !v.is_empty()) + .or_else(|| env::var_os("LC_CTYPE").filter(|v| !v.is_empty())) + .or_else(|| env::var_os("LANG").filter(|v| !v.is_empty())) .unwrap_or_else(|| "C".into()) .to_str() { diff --git a/src/shell/xdg/mod.rs b/src/shell/xdg/mod.rs index 7b684c3bd3..5a046300ab 100644 --- a/src/shell/xdg/mod.rs +++ b/src/shell/xdg/mod.rs @@ -9,7 +9,6 @@ use wayland_protocols::xdg::dialog::v1::client::xdg_wm_dialog_v1; use crate::reexports::client::globals::{BindError, GlobalList}; use crate::reexports::client::Connection; use crate::reexports::client::{protocol::wl_surface, Dispatch, Proxy, QueueHandle}; -use crate::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use crate::reexports::protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; use crate::reexports::protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::Mode; use crate::reexports::protocols::xdg::decoration::zv1::client::{ @@ -28,9 +27,7 @@ use crate::registry::GlobalProxy; use crate::shell::xdg::dialog::{Dialog, DialogData, DialogHandler}; use self::window::inner::WindowInner; -use self::window::{ - DecorationMode, Window, WindowConfigure, WindowData, WindowDecorations, WindowHandler, -}; +use self::window::{Window, WindowData, WindowDecorations, WindowHandler}; use super::WaylandSurface; @@ -87,7 +84,7 @@ impl XdgShell { State: Dispatch + 'static, { // If server side decorations are available, create the toplevel decoration. - let toplevel_decoration = decoration_manager.and_then(|decoration_manager| { + decoration_manager.and_then(|decoration_manager| { match decorations { // Window does not want any server side decorations. WindowDecorations::ClientOnly | WindowDecorations::None => None, @@ -111,8 +108,7 @@ impl XdgShell { Some(toplevel_decoration) } } - }); - toplevel_decoration + }) } /// Creates a new, unmapped window. @@ -354,10 +350,9 @@ impl ProvidesBoundGlobal /// Dialog impl ProvidesBoundGlobal for XdgShell { fn bound_global(&self) -> Result { - Ok(self - .xdg_wm_dialog_v1 + self.xdg_wm_dialog_v1 .clone() - .ok_or(GlobalError::MissingGlobal("Dialog v1 is not available"))?) + .ok_or(GlobalError::MissingGlobal("Dialog v1 is not available")) } }