diff --git a/src/clientside/mod.rs b/src/clientside/mod.rs index 207db4b..cfe8a1f 100644 --- a/src/clientside/mod.rs +++ b/src/clientside/mod.rs @@ -19,6 +19,8 @@ use wayland_protocols::wp::relative_pointer::zv1::client::{ zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, zwp_relative_pointer_v1::ZwpRelativePointerV1, }; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1; use wayland_protocols::{ wp::{ linux_dmabuf::zv1::client::{ @@ -140,6 +142,8 @@ delegate_noop!(Globals: ZxdgOutputManagerV1); delegate_noop!(Globals: ZwpPointerConstraintsV1); delegate_noop!(Globals: ZwpTabletManagerV2); delegate_noop!(Globals: XdgActivationV1); +delegate_noop!(Globals: ZxdgDecorationManagerV1); +delegate_noop!(Globals: ignore ZxdgToplevelDecorationV1); impl Dispatch for Globals { fn event( diff --git a/src/server/mod.rs b/src/server/mod.rs index 780890f..0850e3f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -6,7 +6,7 @@ mod tests; use self::event::*; use crate::clientside::*; -use crate::xstate::{WindowDims, WmHints, WmName, WmNormalHints}; +use crate::xstate::{Decorations, WindowDims, WmHints, WmName, WmNormalHints}; use crate::{X11Selection, XConnection}; use log::{debug, warn}; use rustix::event::{poll, PollFd, PollFlags}; @@ -22,6 +22,10 @@ use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::rc::{Rc, Weak}; use wayland_client::{globals::Global, protocol as client, Proxy}; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::{ + self, ZxdgToplevelDecorationV1, +}; use wayland_protocols::{ wp::{ linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, @@ -87,6 +91,7 @@ pub struct WindowAttributes { pub title: Option, pub class: Option, pub group: Option, + pub decorations: Option, } #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] @@ -244,6 +249,7 @@ struct ToplevelData { toplevel: XdgToplevel, xdg: XdgSurfaceData, fullscreen: bool, + decoration: Option, } #[derive(Debug)] @@ -509,6 +515,7 @@ pub struct ServerState { activation_state: Option, global_output_offset: GlobalOutputOffset, global_offset_updated: bool, + decoration_manager: Option, } impl ServerState { @@ -542,6 +549,14 @@ impl ServerState { }) .ok(); + let decoration_manager = clientside + .global_list + .bind::(&qh, 1..=1, ()) + .inspect_err(|e| { + warn!("Could not bind xdg decoration ({e:?}). Windows might not have decorations.") + }) + .ok(); + dh.create_global::(1, ()); clientside .global_list @@ -578,6 +593,7 @@ impl ServerState { }, }, global_offset_updated: false, + decoration_manager, } } @@ -717,6 +733,35 @@ impl ServerState { } } + pub fn set_win_decorations(&mut self, window: x::Window, decorations: Decorations) { + if self.decoration_manager.is_none() { + return; + }; + + let Some(win) = self.windows.get_mut(&window) else { + debug!("not setting decorations for unknown window {window:?}"); + return; + }; + + if win.attrs.decorations != Some(decorations) { + debug!("setting {window:?} decorations {decorations:?}"); + if let Some(key) = win.surface_key { + if let Some(object) = self.objects.get(key) { + let surface: &SurfaceData = object.as_ref(); + if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role { + data.decoration + .as_ref() + .unwrap() + .set_mode(decorations.into()); + } + } else { + warn!("could not set decorations on {window:?}: stale surface") + } + } + win.attrs.decorations = Some(decorations); + } + } + pub fn set_window_serial(&mut self, window: x::Window, serial: [u32; 2]) { let Some(win) = self.windows.get_mut(&window) else { warn!("Tried to set serial for unknown window {window:?}"); @@ -1224,6 +1269,17 @@ impl ServerState { toplevel.set_fullscreen(None); } + let decoration = self.decoration_manager.as_ref().map(|decoration_manager| { + let decoration = decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, ()); + decoration.set_mode( + window + .attrs + .decorations + .map_or(zxdg_toplevel_decoration_v1::Mode::ServerSide, From::from), + ); + decoration + }); + let surface: &SurfaceData = self.objects[surface_key].as_ref(); if let (Some(activation_state), Some(token)) = ( self.activation_state.as_ref(), @@ -1240,6 +1296,7 @@ impl ServerState { }, toplevel, fullscreen: false, + decoration, } } diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index c280ac0..74b45fa 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -1,5 +1,6 @@ mod selection; use selection::{Selection, SelectionData}; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; use crate::{server::WindowAttributes, XConnection}; use bitflags::bitflags; @@ -246,7 +247,11 @@ impl XState { self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[self.wm_window]); self.set_root_property(self.atoms.active_win, x::ATOM_WINDOW, &[x::Window::none()]); - self.set_root_property(self.atoms.supported, x::ATOM_ATOM, &[self.atoms.active_win]); + self.set_root_property( + self.atoms.supported, + x::ATOM_ATOM, + &[self.atoms.active_win, self.atoms.motif_wm_hints], + ); self.connection .send_and_check_request(&x::ChangeProperty { @@ -491,6 +496,7 @@ impl XState { let class = self.get_wm_class(window); let wm_hints = self.get_wm_hints(window); let size_hints = self.get_wm_size_hints(window); + let motif_wm_hints = self.get_motif_wm_hints(window); let geometry = self.connection.wait_for_reply(geometry)?; debug!("{window:?} geometry: {geometry:?}"); @@ -503,6 +509,7 @@ impl XState { let class = class.resolve()?; let wm_hints = wm_hints.resolve()?; let size_hints = size_hints.resolve()?; + let motif_wm_hints = motif_wm_hints.resolve()?; Ok(WindowAttributes { override_redirect: attrs.override_redirect(), @@ -517,6 +524,7 @@ impl XState { class, group: wm_hints.and_then(|h| h.window_group), size_hints, + decorations: motif_wm_hints.and_then(|h| h.decorations), }) } @@ -535,6 +543,9 @@ impl XState { if let Some(hints) = attrs.size_hints { server_state.set_size_hints(window, hints); } + if let Some(decorations) = attrs.decorations { + server_state.set_win_decorations(window, decorations); + } } fn get_property_cookie( @@ -660,6 +671,28 @@ impl XState { } } + fn get_motif_wm_hints( + &self, + window: x::Window, + ) -> PropertyCookieWrapper> { + let cookie = self.get_property_cookie( + window, + self.atoms.motif_wm_hints, + self.atoms.motif_wm_hints, + 5, + ); + let resolver = |reply: x::GetPropertyReply| { + let data: &[u32] = reply.value(); + MotifWmHints::from(data) + }; + + PropertyCookieWrapper { + connection: &self.connection, + cookie, + resolver, + } + } + fn get_pid(&self, window: x::Window) -> Option { let Some(pid) = self .connection @@ -718,6 +751,13 @@ impl XState { unwrap_or_skip_bad_window!(self.get_wm_class(window).resolve()).unwrap(); server_state.set_win_class(window, class); } + x if x == self.atoms.motif_wm_hints => { + let motif_hints = + unwrap_or_skip_bad_window!(self.get_motif_wm_hints(window).resolve()).unwrap(); + if let Some(decorations) = motif_hints.decorations { + server_state.set_win_decorations(window, decorations); + } + } _ => { if !self.handle_selection_property_change(&event) && log::log_enabled!(log::Level::Debug) @@ -750,6 +790,7 @@ xcb::atoms_struct! { active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false, client_list => b"_NET_CLIENT_LIST" only_if_exists = false, supported => b"_NET_SUPPORTED" only_if_exists = false, + motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false, utf8_string => b"UTF8_STRING" only_if_exists = false, clipboard => b"CLIPBOARD" only_if_exists = false, targets => b"TARGETS" only_if_exists = false, @@ -795,6 +836,12 @@ bitflags! { } } +bitflags! { + pub struct MotifWmHintsFlags: u32 { + const Decorations = 2; + } +} + #[derive(Debug, PartialEq, Eq)] pub struct WinSize { pub width: i32, @@ -849,6 +896,52 @@ impl From<&[u32]> for WmHints { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Decorations { + Client = 0, + Server = 1, +} + +impl TryFrom for Decorations { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Client), + 1 => Ok(Self::Server), + _ => Err(()), + } + } +} + +impl From for zxdg_toplevel_decoration_v1::Mode { + fn from(value: Decorations) -> Self { + match value { + Decorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, + Decorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, + } + } +} + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct MotifWmHints { + pub decorations: Option, +} + +impl From<&[u32]> for MotifWmHints { + fn from(value: &[u32]) -> Self { + let mut ret = Self::default(); + + let flags = MotifWmHintsFlags::from_bits_truncate(value[0]); + + if flags.contains(MotifWmHintsFlags::Decorations) { + ret.decorations = value[2].try_into().ok(); + } + + ret + } +} + #[derive(Debug, Clone, Copy)] pub enum SetState { Remove, diff --git a/tests/integration.rs b/tests/integration.rs index e0dcdd5..74d8193 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -10,7 +10,9 @@ use std::sync::{ }; use std::thread::JoinHandle; use std::time::{Duration, Instant}; -use wayland_protocols::xdg::shell::server::xdg_toplevel; +use wayland_protocols::xdg::{ + decoration::zv1::server::zxdg_toplevel_decoration_v1, shell::server::xdg_toplevel, +}; use wayland_server::Resource; use xcb::{x, Xid}; use xwayland_satellite as xwls; @@ -317,6 +319,7 @@ xcb::atoms_struct! { multiple => b"MULTIPLE", wm_state => b"WM_STATE", wm_check => b"_NET_SUPPORTING_WM_CHECK", + motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false, mime1 => b"text/plain" only_if_exists = false, mime2 => b"blah/blah" only_if_exists = false, incr => b"INCR", @@ -1519,3 +1522,57 @@ fn negative_output_coordinates() { assert_eq!(ptr_reply.win_x(), 30); assert_eq!(ptr_reply.win_y(), 40); } + +#[test] +fn xdg_decorations() { + let mut f = Fixture::new(); + let mut connection = Connection::new(&f.display); + + let window = connection.new_window(connection.root, 0, 0, 20, 20, false); + let surface = f.map_as_toplevel(&mut connection, window); + let data = f.testwl.get_surface_data(surface).unwrap(); + // The default decoration mode in x11 is SDD + assert_eq!( + data.toplevel() + .decoration + .as_ref() + .and_then(|(_, decoration)| *decoration), + Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) + ); + + // CSD + connection.set_property( + window, + connection.atoms.motif_wm_hints, + connection.atoms.motif_wm_hints, + &[2u32, 0, 0, 0, 0], + ); + std::thread::sleep(std::time::Duration::from_millis(1)); + f.testwl.dispatch(); + let data = f.testwl.get_surface_data(surface).unwrap(); + assert_eq!( + data.toplevel() + .decoration + .as_ref() + .and_then(|(_, decoration)| *decoration), + Some(zxdg_toplevel_decoration_v1::Mode::ClientSide) + ); + + // SSD + connection.set_property( + window, + connection.atoms.motif_wm_hints, + connection.atoms.motif_wm_hints, + &[2u32, 0, 1, 0, 0], + ); + std::thread::sleep(std::time::Duration::from_millis(1)); + f.testwl.dispatch(); + let data = f.testwl.get_surface_data(surface).unwrap(); + assert_eq!( + data.toplevel() + .decoration + .as_ref() + .and_then(|(_, decoration)| *decoration), + Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) + ); +} diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 4957073..014b52b 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -27,6 +27,10 @@ use wayland_protocols::{ xdg_activation_token_v1::{self, XdgActivationTokenV1}, xdg_activation_v1::{self, XdgActivationV1}, }, + decoration::zv1::server::{ + zxdg_decoration_manager_v1::{self, ZxdgDecorationManagerV1}, + zxdg_toplevel_decoration_v1::{self, ZxdgToplevelDecorationV1}, + }, shell::server::{ xdg_popup::{self, XdgPopup}, xdg_positioner::{self, XdgPositioner}, @@ -62,7 +66,7 @@ use wayland_server::{ wl_shm_pool::WlShmPool, wl_surface::WlSurface, }, - Client, Dispatch, Display, DisplayHandle, GlobalDispatch, Resource, + Client, Dispatch, Display, DisplayHandle, GlobalDispatch, Resource, WEnum, }; use wl_drm::server::wl_drm::WlDrm; @@ -123,6 +127,10 @@ pub struct Toplevel { pub closed: bool, pub title: Option, pub app_id: Option, + pub decoration: Option<( + ZxdgToplevelDecorationV1, + Option, + )>, } #[derive(Debug, PartialEq, Eq)] @@ -383,6 +391,7 @@ impl Server { dh.create_global::(3, ()); dh.create_global::(1, ()); dh.create_global::(1, ()); + dh.create_global::(1, ()); global_noop!(ZwpLinuxDmabufV1); global_noop!(ZwpRelativePointerManagerV1); global_noop!(WpViewporter); @@ -733,6 +742,7 @@ simple_global_dispatch!(WlCompositor); simple_global_dispatch!(XdgWmBase); simple_global_dispatch!(ZxdgOutputManagerV1); simple_global_dispatch!(ZwpTabletManagerV2); +simple_global_dispatch!(ZxdgDecorationManagerV1); impl Dispatch for State { fn request( @@ -1219,6 +1229,7 @@ impl Dispatch for State { closed: false, title: None, app_id: None, + decoration: None, }; let data = state.surfaces.get_mut(surface_id).unwrap(); data.role = Some(SurfaceRole::Toplevel(t)); @@ -1676,3 +1687,105 @@ impl Dispatch> for State { } } } + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + resource: &ZxdgDecorationManagerV1, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + zxdg_decoration_manager_v1::Request::GetToplevelDecoration { id, toplevel } => { + let surface_id = *toplevel.data::().unwrap(); + let data = state.surfaces.get_mut(&surface_id).unwrap(); + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + if toplevel.decoration.is_some() { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::AlreadyConstructed, + "Toplevel already has an decoration object", + ); + return; + } + toplevel.decoration = Some((data_init.init(id, surface_id), None)); + } + zxdg_decoration_manager_v1::Request::Destroy => {} + _ => todo!(), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + resource: &ZxdgToplevelDecorationV1, + request: ::Request, + surface_id: &SurfaceId, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + zxdg_toplevel_decoration_v1::Request::SetMode { mode } => { + let WEnum::Value(mode) = mode else { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::InvalidMode, + "Invalid decoration mode", + ); + return; + }; + if let Some(data) = state.surfaces.get_mut(surface_id) { + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + *toplevel + .decoration + .as_mut() + .map(|(_, decoration)| decoration) + .unwrap() = Some(mode); + } else { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::Orphaned, + "Toplevel was destroyed", + ); + } + } + zxdg_toplevel_decoration_v1::Request::UnsetMode => { + if let Some(data) = state.surfaces.get_mut(surface_id) { + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + *toplevel + .decoration + .as_mut() + .map(|(_, decoration)| decoration) + .unwrap() = None; + } else { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::Orphaned, + "Toplevel was destroyed", + ); + } + } + zxdg_toplevel_decoration_v1::Request::Destroy => { + if let Some(data) = state.surfaces.get_mut(surface_id) { + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + toplevel.decoration = None; + } else { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::Orphaned, + "Toplevel was destroyed", + ); + } + } + _ => unreachable!(), + } + } +}