diff --git a/src/server/mod.rs b/src/server/mod.rs index cf0780f..7e5faac 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -93,6 +93,7 @@ struct WindowAttributes { class: Option, group: Option, decorations: Option, + transient_for: Option, } #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] @@ -930,6 +931,14 @@ impl ServerState { } } + pub fn set_transient_for(&mut self, window: x::Window, parent: x::Window) { + let Some(win) = self.windows.get_mut(&window) else { + return; + }; + + win.attrs.transient_for = Some(parent); + } + pub fn activate_window(&mut self, window: x::Window) { let Some(activation_state) = self.activation_state.as_ref() else { return; @@ -1168,14 +1177,16 @@ impl ServerState { .xdg_wm_base .get_xdg_surface(&surface.client, &self.qh, surface_key); - let window = self.windows.get(&window).unwrap(); + // Temporarily remove to placate borrow checker + let window_data = self.windows.remove(&window).unwrap(); + let mut popup_for = None; - if window.attrs.is_popup { + if window_data.attrs.is_popup { popup_for = self.last_hovered.or(self.last_focused_toplevel); } let mut fullscreen = false; - let (width, height) = (window.attrs.dims.width, window.attrs.dims.height); + let (width, height) = (window_data.attrs.dims.width, window_data.attrs.dims.height); for (key, _) in &self.output_keys { let output: &Output = self.objects[key].as_ref(); if output.dimensions.width == width as i32 && output.dimensions.height == height as i32 @@ -1188,11 +1199,12 @@ impl ServerState { let initial_scale; let role = if let Some(parent) = popup_for { let data; - (initial_scale, data) = self.create_popup(window, surface_key, xdg_surface, parent); + (initial_scale, data) = + self.create_popup(&window_data, surface_key, xdg_surface, parent); SurfaceRole::Popup(Some(data)) } else { initial_scale = 1.0; - let data = self.create_toplevel(window, surface_key, xdg_surface, fullscreen); + let data = self.create_toplevel(&window_data, surface_key, xdg_surface, fullscreen); SurfaceRole::Toplevel(Some(data)) }; @@ -1206,15 +1218,17 @@ impl ServerState { assert_eq!( new_role_type, old_role_type, "Surface for {:?} already had a role: {:?}", - window.window, role + window_data.window, role ); } surface.client.commit(); + // Reinsert + self.windows.insert(window, window_data); } fn create_toplevel( - &self, + &mut self, window: &WindowData, surface_key: ObjectKey, xdg: XdgSurface, @@ -1273,6 +1287,37 @@ impl ServerState { activation_state.activate::(&surface.client, token); } + if let Some(parent) = window.attrs.transient_for { + // TODO: handle transient_for window not being mapped/not a toplevel + 'b: { + let Some(parent_data) = self.windows.get_mut(&parent) else { + warn!( + "Window {:?} is marked transient for unknown window {:?}", + window.window, parent + ); + break 'b; + }; + + let Some(key) = parent_data.surface_key else { + warn!("Parent window {parent:?} missing surface key."); + break 'b; + }; + + let Some::<&SurfaceData>(surface) = self.objects.get(key).map(|o| o.as_ref()) + else { + warn!("Parent window {parent:?} surface is stale"); + break 'b; + }; + + let Some(SurfaceRole::Toplevel(Some(parent_toplevel))) = &surface.role else { + warn!("Surface {:?} (for window {parent:?}) was not an active toplevel, not setting as parent", surface.client.id()); + break 'b; + }; + + toplevel.set_parent(Some(&parent_toplevel.toplevel)); + } + } + ToplevelData { xdg: XdgSurfaceData { surface: xdg, diff --git a/src/server/tests.rs b/src/server/tests.rs index 51af4c4..0269cea 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -2183,6 +2183,40 @@ fn subpopup_positioning() { assert_eq!(dims.y, 50); } +#[test] +fn transient_for_toplevel() { + let (mut f, comp) = TestFixture::new_with_compositor(); + let toplevel = unsafe { Window::new(1) }; + let (_, toplevel_id) = f.create_toplevel(&comp, toplevel); + + let sub_toplevel = unsafe { Window::new(2) }; + let (buffer, surface) = comp.create_surface(); + f.new_window( + sub_toplevel, + false, + WindowData { + mapped: true, + dims: WindowDims { + width: 50, + height: 50, + ..Default::default() + }, + fullscreen: false, + }, + ); + + f.satellite.set_transient_for(sub_toplevel, toplevel); + f.map_window(&comp, sub_toplevel, &surface.obj, &buffer); + f.run(); + let id = f.check_new_surface(); + let toplevel_data = f.testwl.get_surface_data(toplevel_id).unwrap(); + let sub_data = f.testwl.get_surface_data(id).unwrap(); + assert_eq!( + sub_data.toplevel().parent, + Some(toplevel_data.toplevel().toplevel.clone()) + ); +} + /// See Pointer::handle_event for an explanation. #[test] fn popup_pointer_motion_workaround() {} diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index c390f30..6b1cab4 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -533,10 +533,14 @@ impl XState { 1, |reply: x::GetPropertyReply| reply.value::().first().copied(), ) - .resolve()?; + .resolve()? + .flatten(); let is_popup = self.guess_is_popup(window, motif_hints, transient_for.is_some())?; server_state.set_popup(window, is_popup); + if let Some(parent) = transient_for.and_then(|t| (!is_popup).then_some(t)) { + server_state.set_transient_for(window, parent); + } Ok(()) } diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 42ba65a..b71c43d 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -137,6 +137,7 @@ pub enum SurfaceRole { pub struct Toplevel { pub xdg: XdgSurfaceData, pub toplevel: XdgToplevel, + pub parent: Option, pub min_size: Option, pub max_size: Option, pub states: Vec, @@ -1224,6 +1225,13 @@ impl Dispatch for State { }; toplevel.app_id = app_id.into(); } + xdg_toplevel::Request::SetParent { parent } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + toplevel.parent = parent; + } other => todo!("unhandled request {other:?}"), } } @@ -1247,6 +1255,7 @@ impl Dispatch for State { let t = Toplevel { xdg: XdgSurfaceData::new(resource.clone()), toplevel, + parent: None, min_size: None, max_size: None, states: Vec::new(),