diff --git a/satellite/src/lib.rs b/satellite/src/lib.rs index 7043250..8ba83e7 100644 --- a/satellite/src/lib.rs +++ b/satellite/src/lib.rs @@ -1,6 +1,6 @@ mod clientside; mod server; -mod xstate; +pub mod xstate; use crate::server::{PendingSurfaceState, ServerState}; use crate::xstate::XState; diff --git a/satellite/src/server/event.rs b/satellite/src/server/event.rs index 5229264..a78e434 100644 --- a/satellite/src/server/event.rs +++ b/satellite/src/server/event.rs @@ -158,12 +158,12 @@ impl SurfaceData { let width = if pending.width > 0 { pending.width as _ } else { - window.dims.width + window.attrs.dims.width }; let height = if pending.height > 0 { pending.height as _ } else { - window.dims.height + window.attrs.dims.height }; debug!( "configuring {:?}: {}x{}, {width}x{height}", @@ -178,7 +178,7 @@ impl SurfaceData { height: height as _, }, ); - window.dims = WindowDims { + window.attrs.dims = WindowDims { x: pending.x as _, y: pending.y as _, width, diff --git a/satellite/src/server/mod.rs b/satellite/src/server/mod.rs index 5b2d8df..55aef6f 100644 --- a/satellite/src/server/mod.rs +++ b/satellite/src/server/mod.rs @@ -68,19 +68,24 @@ where u32::from(wenum).try_into().unwrap() } +#[derive(Default, Debug)] +pub struct WindowAttributes { + pub override_redirect: bool, + pub popup_for: Option, + pub dims: WindowDims, + pub size_hints: Option, + pub title: Option, + pub class: Option, + pub group: Option, +} + #[derive(Debug)] struct WindowData { window: x::Window, + surface_id: u32, surface_key: Option, mapped: bool, - surface_id: u32, - popup_for: Option, - dims: WindowDims, - size_hints: Option, - override_redirect: bool, - title: Option, - class: Option, - group: Option, + attrs: WindowAttributes, } impl WindowData { @@ -92,16 +97,15 @@ impl WindowData { ) -> Self { Self { window, + surface_id: 0, surface_key: None, mapped: false, - popup_for: parent, - surface_id: 0, - dims, - size_hints: None, - override_redirect, - title: None, - class: None, - group: None + attrs: WindowAttributes { + override_redirect, + dims, + popup_for: parent, + ..Default::default() + }, } } } @@ -491,7 +495,7 @@ impl ServerState { pub fn set_win_title(&mut self, window: x::Window, name: WmName) { let win = self.windows.get_mut(&window).unwrap(); - let new_title = match &mut win.title { + let new_title = match &mut win.attrs.title { Some(w) => { if matches!(w, WmName::NetWmName(_)) && matches!(name, WmName::WmName(_)) { debug!("skipping setting window name to {name:?} because a _NET_WM_NAME title is already set"); @@ -501,7 +505,7 @@ impl ServerState { Some(w) } } - None => Some(win.title.insert(name)), + None => Some(win.attrs.title.insert(name)), }; let Some(title) = new_title else { @@ -518,7 +522,7 @@ impl ServerState { pub fn set_win_class(&mut self, window: x::Window, class: String) { let win = self.windows.get_mut(&window).unwrap(); - let class = win.class.insert(class); + let class = win.attrs.class.insert(class); if let Some(key) = win.surface_key { let surface: &SurfaceData = self.objects[key].as_ref(); if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role { @@ -529,13 +533,13 @@ impl ServerState { pub fn set_win_hints(&mut self, window: x::Window, hints: WmHints) { let win = self.windows.get_mut(&window).unwrap(); - win.group = hints.window_group; + win.attrs.group = hints.window_group; } pub fn set_size_hints(&mut self, window: x::Window, hints: WmNormalHints) { let win = self.windows.get_mut(&window).unwrap(); - if win.size_hints.is_none() || *win.size_hints.as_ref().unwrap() != hints { + if win.attrs.size_hints.is_none() || *win.attrs.size_hints.as_ref().unwrap() != hints { debug!("setting {window:?} hints {hints:?}"); if let Some(surface) = win.surface_key { let surface: &SurfaceData = self.objects[surface].as_ref(); @@ -548,7 +552,7 @@ impl ServerState { } } } - win.size_hints = Some(hints); + win.attrs.size_hints = Some(hints); } } @@ -577,7 +581,7 @@ impl ServerState { pub fn reconfigure_window(&mut self, event: x::ConfigureNotifyEvent) { let win = self.windows.get_mut(&event.window()).unwrap(); - win.dims = WindowDims { + win.attrs.dims = WindowDims { x: event.x(), y: event.y(), width: event.width(), @@ -693,21 +697,21 @@ impl ServerState { .get_xdg_surface(client, &self.qh, surface_key); let window_data = self.windows.get_mut(&window).unwrap(); - if window_data.override_redirect { + if window_data.attrs.override_redirect { // Override redirect is hard to convert to Wayland! // We will just make them be popups for the last focused toplevel. if let Some(win) = self.last_focused_toplevel { - window_data.popup_for = Some(win) + window_data.attrs.popup_for = Some(win) } } let window = self.windows.get(&window).unwrap(); - let role = if let Some(parent) = window.popup_for { + let role = if let Some(parent) = window.attrs.popup_for { debug!( "creating popup ({:?}) {:?} {:?} {:?} {surface_key:?}", window.window, parent, - window.dims, + window.attrs.dims, surface.client.id() ); @@ -716,15 +720,15 @@ impl ServerState { self.objects[parent_window.surface_key.unwrap()].as_ref(); let positioner = self.xdg_wm_base.create_positioner(&self.qh, ()); - positioner.set_size(window.dims.width as _, window.dims.height as _); - positioner.set_offset(window.dims.x as i32, window.dims.y as i32); + positioner.set_size(window.attrs.dims.width as _, window.attrs.dims.height as _); + positioner.set_offset(window.attrs.dims.x as i32, window.attrs.dims.y as i32); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); positioner.set_anchor_rect( 0, 0, - parent_window.dims.width as _, - parent_window.dims.height as _, + parent_window.attrs.dims.width as _, + parent_window.attrs.dims.height as _, ); let popup = xdg_surface.get_popup( Some(&parent_surface.xdg().unwrap().surface), @@ -772,7 +776,7 @@ impl ServerState { debug!("creating toplevel for {:?}", window.window); let toplevel = xdg.get_toplevel(&self.qh, surface_key); - if let Some(hints) = &window.size_hints { + if let Some(hints) = &window.attrs.size_hints { if let Some(min) = &hints.min_size { toplevel.set_min_size(min.width, min.height); } @@ -781,18 +785,20 @@ impl ServerState { } } - let group = window.group.and_then(|win| self.windows.get(&win)); + let group = window.attrs.group.and_then(|win| self.windows.get(&win)); if let Some(class) = window + .attrs .class .as_ref() - .or(group.and_then(|g| g.class.as_ref())) + .or(group.and_then(|g| g.attrs.class.as_ref())) { toplevel.set_app_id(class.to_string()); } if let Some(title) = window + .attrs .title .as_ref() - .or(group.and_then(|g| g.title.as_ref())) + .or(group.and_then(|g| g.attrs.title.as_ref())) { toplevel.set_title(title.name().to_string()); } diff --git a/satellite/src/server/tests.rs b/satellite/src/server/tests.rs index e9c4612..689ce5a 100644 --- a/satellite/src/server/tests.rs +++ b/satellite/src/server/tests.rs @@ -118,7 +118,7 @@ impl Compositor { } } -#[derive(Debug)] +#[derive(Debug, Default)] struct WindowData { mapped: bool, fullscreen: bool, @@ -135,7 +135,7 @@ impl FakeXConnection { fn window(&mut self, window: Window) -> &mut WindowData { self.windows .get_mut(&window) - .expect("Unknown window: {window:?}") + .expect(&format!("Unknown window: {window:?}")) } } diff --git a/satellite/src/xstate.rs b/satellite/src/xstate.rs index 6af4a30..d698c43 100644 --- a/satellite/src/xstate.rs +++ b/satellite/src/xstate.rs @@ -1,3 +1,4 @@ +use crate::server::WindowAttributes; use bitflags::bitflags; use log::{debug, trace, warn}; use std::ffi::CString; @@ -12,6 +13,39 @@ pub struct XState { pub atoms: Atoms, } +/// Essentially a trait alias. +trait PropertyResolver { + type Output; + fn resolve(self, reply: x::GetPropertyReply) -> Self::Output; +} +impl PropertyResolver for T +where + T: FnOnce(x::GetPropertyReply) -> Output, +{ + type Output = Output; + fn resolve(self, reply: x::GetPropertyReply) -> Self::Output { + (self)(reply) + } +} + +struct PropertyCookieWrapper<'a, F: PropertyResolver> { + connection: &'a xcb::Connection, + cookie: x::GetPropertyCookie, + resolver: F, +} + +impl PropertyCookieWrapper<'_, F> { + /// Get the result from our property cookie. + fn resolve(self) -> Option { + let reply = self.connection.wait_for_reply(self.cookie).unwrap(); + if reply.r#type() == x::ATOM_NONE { + None + } else { + Some(self.resolver.resolve(reply)) + } + } +} + #[derive(Debug)] pub enum WmName { WmName(String), @@ -107,11 +141,7 @@ impl XState { self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[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.connection .send_and_check_request(&x::ChangeProperty { @@ -140,20 +170,6 @@ impl XState { match event { xcb::Event::X(x::Event::CreateNotify(e)) => { debug!("new window: {:?}", e); - match self - .connection - .send_and_check_request(&x::ChangeWindowAttributes { - window: e.window(), - value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], - }) { - // This can sometimes fail if the window was created and then immediately - // destroyed. - Ok(()) | Err(xcb::ProtocolError::X(x::Error::Window(_), _)) => {} - Err(other) => { - panic!("error subscribing to property change on new window: {other:?}") - } - } - let parent = e.parent(); let parent = if parent.is_none() || parent == self.root { None @@ -162,6 +178,22 @@ impl XState { }; server_state.new_window(e.window(), e.override_redirect(), (&e).into(), parent); } + xcb::Event::X(x::Event::ReparentNotify(e)) => { + debug!("reparent event: {e:?}"); + if e.parent() == self.root { + let attrs = self.get_window_attributes(e.window()); + server_state.new_window( + e.window(), + attrs.override_redirect, + attrs.dims, + None, + ); + self.handle_window_attributes(server_state, e.window(), attrs); + } else { + debug!("destroying window since its parent is no longer root!"); + server_state.destroy_window(e.window()); + } + } xcb::Event::X(x::Event::MapRequest(e)) => { debug!("requested to map {:?}", e.window()); self.connection @@ -169,7 +201,15 @@ impl XState { .unwrap(); } xcb::Event::X(x::Event::MapNotify(e)) => { + let attrs = self.get_window_attributes(e.window()); + self.handle_window_attributes(server_state, e.window(), attrs); server_state.map_window(e.window()); + self.connection + .send_and_check_request(&x::ChangeWindowAttributes { + window: e.window(), + value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], + }) + .unwrap(); } xcb::Event::X(x::Event::ConfigureNotify(e)) => { server_state.reconfigure_window(e); @@ -177,6 +217,17 @@ impl XState { xcb::Event::X(x::Event::UnmapNotify(e)) => { trace!("unmap event: {:?}", e.event()); server_state.unmap_window(e.window()); + match self + .connection + .send_and_check_request(&x::ChangeWindowAttributes { + window: e.window(), + value_list: &[x::Cw::EventMask(x::EventMask::empty())], + }) { + // Window error may occur if the window has been destroyed, + // which is fine + Ok(_) | Err(xcb::ProtocolError::X(x::Error::Window(_), _)) => {} + Err(other) => panic!("Error removing event mask from window: {other:?}"), + } } xcb::Event::X(x::Event::DestroyNotify(e)) => { debug!("destroying window {:?}", e.window()); @@ -252,6 +303,173 @@ impl XState { } } + fn get_window_attributes(&self, window: x::Window) -> WindowAttributes { + let geometry = self.connection.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(window), + }); + let attrs = self + .connection + .send_request(&x::GetWindowAttributes { window }); + + let name = self.get_net_wm_name(window); + 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 geometry = self.connection.wait_for_reply(geometry).unwrap(); + let attrs = self.connection.wait_for_reply(attrs).unwrap(); + let title = name + .resolve() + .or_else(|| self.get_wm_name(window).resolve()); + let class = class.resolve(); + let wm_hints = wm_hints.resolve(); + let size_hints = size_hints.resolve(); + + WindowAttributes { + override_redirect: attrs.override_redirect(), + popup_for: None, + dims: WindowDims { + x: geometry.x(), + y: geometry.y(), + width: geometry.width(), + height: geometry.height(), + }, + title, + class, + group: wm_hints.map(|h| h.window_group).flatten(), + size_hints, + } + } + + fn handle_window_attributes( + &self, + server_state: &mut super::RealServerState, + window: x::Window, + attrs: WindowAttributes, + ) { + if let Some(name) = attrs.title { + debug!("setting {window:?} title to {name:?}"); + server_state.set_win_title(window, name); + } + if let Some(class) = attrs.class { + debug!("setting {window:?} class to {class}"); + server_state.set_win_class(window, class); + } + if let Some(hints) = attrs.size_hints { + debug!("{window:?} size hints: {hints:?}"); + server_state.set_size_hints(window, hints); + } + } + + fn get_property_cookie( + &self, + window: x::Window, + property: x::Atom, + ty: x::Atom, + long_length: u32, + ) -> x::GetPropertyCookie { + self.connection.send_request(&x::GetProperty { + delete: false, + window, + property, + r#type: ty, + long_offset: 0, + long_length, + }) + } + + fn get_wm_class( + &self, + window: x::Window, + ) -> PropertyCookieWrapper> { + let cookie = self.get_property_cookie(window, x::ATOM_WM_CLASS, x::ATOM_STRING, 256); + let resolver = move |reply: x::GetPropertyReply| { + let data: &[u8] = reply.value(); + // wm class is instance + class - ignore instance + let class_start = data.iter().copied().position(|b| b == 0u8).unwrap() + 1; + let data = data[class_start..].to_vec(); + let class = CString::from_vec_with_nul(data).unwrap(); + debug!("{:?} class: {class:?}", window); + class.to_string_lossy().to_string() + }; + PropertyCookieWrapper { + connection: &self.connection, + cookie, + resolver, + } + } + + fn get_wm_name( + &self, + window: x::Window, + ) -> PropertyCookieWrapper> { + let cookie = self.get_property_cookie(window, x::ATOM_WM_NAME, x::ATOM_STRING, 256); + let resolver = |reply: x::GetPropertyReply| { + let data: &[u8] = reply.value(); + WmName::WmName(String::from_utf8(data.to_vec()).unwrap()) + }; + + PropertyCookieWrapper { + connection: &self.connection, + cookie, + resolver, + } + } + + fn get_net_wm_name( + &self, + window: x::Window, + ) -> PropertyCookieWrapper> { + let cookie = + self.get_property_cookie(window, self.atoms.net_wm_name, self.atoms.utf8_string, 256); + let resolver = |reply: x::GetPropertyReply| { + let data: &[u8] = reply.value(); + WmName::NetWmName(String::from_utf8(data.to_vec()).unwrap()) + }; + + PropertyCookieWrapper { + connection: &self.connection, + cookie, + resolver, + } + } + + fn get_wm_hints( + &self, + window: x::Window, + ) -> PropertyCookieWrapper> { + let cookie = self.get_property_cookie(window, x::ATOM_WM_HINTS, x::ATOM_WM_HINTS, 9); + let resolver = |reply: x::GetPropertyReply| { + let data: &[u32] = reply.value(); + let hints = WmHints::from(data); + debug!("wm hints: {hints:?}"); + hints + }; + PropertyCookieWrapper { + connection: &self.connection, + cookie, + resolver, + } + } + + fn get_wm_size_hints( + &self, + window: x::Window, + ) -> PropertyCookieWrapper> { + let cookie = + self.get_property_cookie(window, x::ATOM_WM_NORMAL_HINTS, x::ATOM_WM_SIZE_HINTS, 9); + let resolver = |reply: x::GetPropertyReply| { + let data: &[u32] = reply.value(); + WmNormalHints::from(data) + }; + + PropertyCookieWrapper { + connection: &self.connection, + cookie, + resolver, + } + } + fn handle_property_change( &self, event: x::PropertyNotifyEvent, @@ -276,19 +494,12 @@ impl XState { }; match event.atom() { - x if x == self.atoms.wm_hints => { - let prop = get_prop(self.atoms.wm_hints, 9).unwrap(); - let data: &[u32] = prop.value(); - let hints = WmHints::from(data); - debug!("wm hints: {hints:?}"); - server_state.set_win_hints(event.window(), hints); + x if x == x::ATOM_WM_HINTS => { + let hints = self.get_wm_hints(window).resolve().unwrap(); + server_state.set_win_hints(window, hints); } x if x == x::ATOM_WM_NORMAL_HINTS => { - let Ok(prop) = get_prop(x::ATOM_WM_SIZE_HINTS, 9) else { - return; - }; - let data: &[u32] = prop.value(); - let hints = WmNormalHints::from(data); + let hints = self.get_wm_size_hints(window).resolve().unwrap(); server_state.set_size_hints(window, hints); } x if x == x::ATOM_WM_NAME || x == self.atoms.net_wm_name => { @@ -309,14 +520,8 @@ impl XState { server_state.set_win_title(window, name); } x if x == x::ATOM_WM_CLASS => { - let prop = get_prop(x::ATOM_STRING, 256).unwrap(); - let data: &[u8] = prop.value(); - // wm class is instance + class - ignore instance - let class_start = data.iter().copied().position(|b| b == 0u8).unwrap() + 1; - let data = data[class_start..].to_vec(); - let class = CString::from_vec_with_nul(data).unwrap(); - debug!("{:?} class: {class:?}", window); - server_state.set_win_class(window, class.to_string_lossy().to_string()); + let class = self.get_wm_class(window).resolve().unwrap(); + server_state.set_win_class(window, class); } _ => { if log::log_enabled!(log::Level::Debug) { @@ -342,7 +547,6 @@ xcb::atoms_struct! { pub wm_protocols => b"WM_PROTOCOLS" only_if_exists = false, pub wm_delete_window => b"WM_DELETE_WINDOW" only_if_exists = false, pub wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false, - pub wm_hints => b"WM_HINTS" only_if_exists = false, pub wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false, pub net_wm_name => b"_NET_WM_NAME" only_if_exists = false, pub wm_pid => b"_NET_WM_PID" only_if_exists = false, @@ -506,8 +710,8 @@ impl super::XConnection for Arc { .wait_for_reply(self.send_request(&x::GetProperty { delete: false, window, - property: atoms.wm_hints, - r#type: atoms.wm_hints, + property: x::ATOM_WM_HINTS, + r#type: x::ATOM_WM_HINTS, long_offset: 0, long_length: 8, })) diff --git a/satellite/tests/integration.rs b/satellite/tests/integration.rs index ecafe1d..dcc5b1d 100644 --- a/satellite/tests/integration.rs +++ b/satellite/tests/integration.rs @@ -12,6 +12,7 @@ use wayland_protocols::xdg::shell::server::xdg_toplevel; use wayland_server::Resource; use xcb::{x, Xid}; use xwayland_satellite as xwls; +use xwayland_satellite::xstate::WmSizeHintsFlags; #[derive(Default)] struct TestDataInner { @@ -87,6 +88,7 @@ impl Fixture { env_logger::builder() .is_test(true) .filter_level(log::LevelFilter::Debug) + .parse_default_env() .init(); }); @@ -127,7 +129,7 @@ impl Fixture { // Give Xwayland time to do its thing let mut ready = our_data.display.lock().unwrap().is_some(); - while !ready && start.elapsed() < Duration::from_millis(3000) { + while !ready && start.elapsed() < Duration::from_millis(2000) { let n = poll(&mut f, 100).unwrap(); if n > 0 { testwl.dispatch(); @@ -162,58 +164,13 @@ impl Fixture { } } - fn create_and_map_window( + fn configure_and_verify_new_toplevel( &mut self, - connection: &xcb::Connection, - override_redirect: bool, - x: i16, - y: i16, - width: u16, - height: u16, - ) -> (x::Window, testwl::SurfaceId) { - let screen = connection.get_setup().roots().next().unwrap(); - let wid = connection.generate_id(); - let req = x::CreateWindow { - depth: x::COPY_FROM_PARENT as _, - wid, - parent: screen.root(), - x, - y, - width, - height, - border_width: 0, - class: x::WindowClass::InputOutput, - visual: screen.root_visual(), - value_list: &[ - x::Cw::BackPixel(screen.white_pixel()), - x::Cw::OverrideRedirect(override_redirect), - ], - }; - connection.send_and_check_request(&req).unwrap(); - - let req = x::MapWindow { window: wid }; - connection.send_and_check_request(&req).unwrap(); - self.wait_and_dispatch(); - - let id = self - .testwl - .last_created_surface_id() - .expect("No surface created for window"); - - (wid, id) - } - - fn create_toplevel( - &mut self, - connection: &xcb::Connection, - width: u16, - height: u16, - ) -> (x::Window, testwl::SurfaceId) { - let (window, surface) = self.create_and_map_window(connection, false, 0, 0, width, height); - let data = self - .testwl - .get_surface_data(surface) - .expect("No surface data"); + connection: &mut Connection, + window: x::Window, + surface: testwl::SurfaceId, + ) { + let data = self.testwl.get_surface_data(surface).unwrap(); assert!( matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))), "surface role was wrong: {:?}", @@ -233,8 +190,6 @@ impl Fixture { assert_eq!(geometry.y(), 0); assert_eq!(geometry.width(), 100); assert_eq!(geometry.height(), 100); - - (window, surface) } /// Triggers a Wayland side toplevel Close event and processes the corresponding @@ -273,8 +228,6 @@ xcb::atoms_struct! { struct Atoms { wm_protocols => b"WM_PROTOCOLS", wm_delete_window => b"WM_DELETE_WINDOW", - wm_class => b"WM_CLASS", - wm_name => b"WM_NAME", } } @@ -282,6 +235,8 @@ struct Connection { inner: xcb::Connection, pollfd: PollFd<'static>, atoms: Atoms, + root: x::Window, + visual: u32, } impl std::ops::Deref for Connection { @@ -297,14 +252,71 @@ impl Connection { let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) }; let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN); let atoms = Atoms::intern_all(&inner).unwrap(); + let screen = inner.get_setup().roots().next().unwrap(); + let root = screen.root(); + let visual = screen.root_visual(); Self { inner, pollfd, atoms, + root, + visual, } } + fn new_window( + &self, + parent: x::Window, + x: i16, + y: i16, + width: u16, + height: u16, + override_redirect: bool, + ) -> x::Window { + let wid = self.inner.generate_id(); + let req = x::CreateWindow { + depth: 0, + wid, + parent, + x, + y, + width, + height, + border_width: 0, + class: x::WindowClass::InputOutput, + visual: self.visual, + value_list: &[x::Cw::OverrideRedirect(override_redirect)], + }; + self.inner + .send_and_check_request(&req) + .expect("creating window failed"); + + wid + } + + fn map_window(&self, window: x::Window) { + self.send_and_check_request(&x::MapWindow { window }) + .unwrap(); + } + + fn set_property( + &self, + window: x::Window, + r#type: x::Atom, + property: x::Atom, + data: &[P], + ) { + self.send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window, + r#type, + property, + data, + }) + .unwrap(); + } + #[track_caller] fn await_event(&mut self) { assert!( @@ -318,39 +330,85 @@ impl Connection { fn toplevel_flow() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); - let (window, surface) = f.create_toplevel(&connection.inner, 200, 200); + let window = connection.new_window(connection.root, 0, 0, 200, 200, false); - connection - .inner - .send_and_check_request(&x::ChangeProperty { - mode: x::PropMode::Replace, - window, - r#type: x::ATOM_STRING, - property: connection.atoms.wm_name, - data: c"window".to_bytes(), - }) - .unwrap(); - - connection - .inner - .send_and_check_request(&x::ChangeProperty { - mode: x::PropMode::Replace, - window, - r#type: x::ATOM_STRING, - property: connection.atoms.wm_class, - data: &[ - c"instance".to_bytes_with_nul(), - c"class".to_bytes_with_nul(), - ] - .concat(), - }) - .unwrap(); + // Pre-map properties + connection.set_property( + window, + x::ATOM_STRING, + x::ATOM_WM_NAME, + c"window".to_bytes(), + ); + connection.set_property( + window, + x::ATOM_STRING, + x::ATOM_WM_CLASS, + &[ + c"instance".to_bytes_with_nul(), + c"class".to_bytes_with_nul(), + ] + .concat(), + ); + let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits(); + connection.set_property( + window, + x::ATOM_WM_SIZE_HINTS, + x::ATOM_WM_NORMAL_HINTS, + &[flags, 0, 0, 0, 0, 50, 100, 300, 400], + ); + connection.map_window(window); f.wait_and_dispatch(); + let surface = f + .testwl + .last_created_surface_id() + .expect("No surface created!"); + f.configure_and_verify_new_toplevel(&mut connection, window, surface); + let data = f.testwl.get_surface_data(surface).unwrap(); assert_eq!(data.toplevel().title, Some("window".into())); assert_eq!(data.toplevel().app_id, Some("class".into())); + assert_eq!( + data.toplevel().min_size, + Some(testwl::Vec2 { x: 50, y: 100 }) + ); + assert_eq!( + data.toplevel().max_size, + Some(testwl::Vec2 { x: 300, y: 400 }) + ); + + // Post map properties + connection.set_property( + window, + x::ATOM_STRING, + x::ATOM_WM_NAME, + c"bindow".to_bytes(), + ); + connection.set_property( + window, + x::ATOM_STRING, + x::ATOM_WM_CLASS, + &[c"f".to_bytes_with_nul(), c"ssalc".to_bytes_with_nul()].concat(), + ); + connection.set_property( + window, + x::ATOM_WM_SIZE_HINTS, + x::ATOM_WM_NORMAL_HINTS, + &[flags, 1, 2, 3, 4, 25, 50, 150, 200], + ); + f.wait_and_dispatch(); + let data = f.testwl.get_surface_data(surface).unwrap(); + assert_eq!(data.toplevel().title, Some("bindow".into())); + assert_eq!(data.toplevel().app_id, Some("ssalc".into())); + assert_eq!( + data.toplevel().min_size, + Some(testwl::Vec2 { x: 25, y: 50 }) + ); + assert_eq!( + data.toplevel().max_size, + Some(testwl::Vec2 { x: 150, y: 200 }) + ); f.close_toplevel(&mut connection, window, surface); @@ -361,3 +419,27 @@ fn toplevel_flow() { let data = f.testwl.get_surface_data(surface).expect("No surface data"); assert!(!data.toplevel().toplevel.is_alive()); } + +#[test] +fn reparent() { + let mut f = Fixture::new(); + let connection = Connection::new(&f.display); + + let parent = connection.new_window(connection.root, 0, 0, 1, 1, false); + let child = connection.new_window(parent, 0, 0, 20, 20, false); + + connection + .send_and_check_request(&x::ReparentWindow { + window: child, + parent: connection.root, + x: 0, + y: 0, + }) + .unwrap(); + + connection + .send_and_check_request(&x::MapWindow { window: child }) + .unwrap(); + + f.wait_and_dispatch(); +}