diff --git a/src/lib.rs b/src/lib.rs index 5c861ea0..d3bf003a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ pub trait XConnection: Sized + 'static { fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState) -> bool; fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool); fn focus_window(&mut self, window: x::Window, output_name: Option); + fn send_take_focus(&mut self, window: x::Window); fn close_window(&mut self, window: x::Window); fn unmap_window(&mut self, window: x::Window); fn raise_to_top(&mut self, window: x::Window); diff --git a/src/server/event.rs b/src/server/event.rs index c75b9f19..4fcf9230 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -438,6 +438,7 @@ impl SurfaceEvents { window, output_name: None, is_popup: true, + has_take_focus: window_data.attrs.has_take_focus, }); } } @@ -864,10 +865,13 @@ impl Event for client::wl_keyboard::Event { serial, )); let output_name = get_output_name(output, &state.world); + let window_data = data.get::<&WindowData>(); + let has_take_focus = window_data.as_ref().is_some_and(|d| d.attrs.has_take_focus); state.to_focus = Some(FocusData { window: *window, output_name, is_popup: false, + has_take_focus, }); keyboard.enter(serial, surface, keys); } diff --git a/src/server/mod.rs b/src/server/mod.rs index 379df771..a436331b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -101,9 +101,10 @@ where #[derive(Default, Debug)] struct WindowAttributes { - acquire_input_via_wm: bool, + accepts_input: bool, has_take_focus: bool, role: WindowRole, + override_redirect: bool, dims: WindowDims, size_hints: Option, title: Option, @@ -114,9 +115,8 @@ struct WindowAttributes { } impl WindowAttributes { - /// AKA "Passive" input model fn require_wm_focus(&self) -> bool { - self.acquire_input_via_wm && !self.has_take_focus + !self.override_redirect && (self.has_take_focus || self.accepts_input) } } @@ -140,6 +140,7 @@ impl WindowData { mapped: false, attrs: WindowAttributes { role: WindowRole::new_basic(override_redirect), + override_redirect, dims, ..Default::default() }, @@ -398,6 +399,7 @@ struct FocusData { window: x::Window, output_name: Option, is_popup: bool, + has_take_focus: bool, } #[derive(Copy, Clone, Default)] @@ -422,6 +424,9 @@ impl XConnection for NoConnection { fn focus_window(&mut self, _: x::Window, _: Option) { debug!("could not focus window without XWayland initialized"); } + fn send_take_focus(&mut self, _: x::Window) { + debug!("could not send take focus without XWayland initialized"); + } fn close_window(&mut self, _: x::Window) { debug!("could not close window without XWayland initialized"); } @@ -742,15 +747,20 @@ impl ServerState { window, output_name, is_popup, + has_take_focus, }) = self.to_focus.take() { debug!( - "focusing {} {window:?}", + "focusing (take_focus={has_take_focus:?}) {} {window:?}", if is_popup { "popup" } else { "window" } ); - self.connection.focus_window(window, output_name); - if !is_popup { - self.last_focused_toplevel = Some(window); + if has_take_focus { + self.connection.send_take_focus(window); + } else { + self.connection.focus_window(window, output_name); + if !is_popup { + self.last_focused_toplevel = Some(window); + } } } else if self.unfocus { self.connection.focus_window(x::WINDOW_NONE, None); @@ -888,9 +898,24 @@ impl InnerServerState { self.windows.insert(window, id); } + pub fn set_override_redirect(&mut self, window: x::Window, override_redirect: bool) { + let Some(id) = self.windows.get(&window).copied() else { + debug!( + "not setting override_redirect {override_redirect} for unknown window {window:?}" + ); + return; + }; + + self.world + .get::<&mut WindowData>(id) + .unwrap() + .attrs + .override_redirect = override_redirect; + } + pub fn set_window_role(&mut self, window: x::Window, role: WindowRole) { let Some(id) = self.windows.get(&window).copied() else { - debug!("not setting popup for unknown window {window:?}"); + debug!("not setting role {role:?} for unknown window {window:?}"); return; }; @@ -969,7 +994,7 @@ impl InnerServerState { let attrs = &mut self.world.get::<&mut WindowData>(id).unwrap().attrs; attrs.group = hints.window_group; - attrs.acquire_input_via_wm = hints.acquire_input_via_wm; + attrs.accepts_input = hints.accepts_input; } pub fn set_take_focus(&mut self, window: x::Window, has_take_focus: bool) { diff --git a/src/server/tests.rs b/src/server/tests.rs index 0651bb2a..4e1735ee 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -160,6 +160,7 @@ struct WindowData { #[derive(Default)] struct FakeXConnection { focused_window: Option, + send_take_focus_window: Option, windows: HashMap, set_window_dims_counter: usize, } @@ -234,6 +235,15 @@ impl super::XConnection for FakeXConnection { self.focused_window = window.into(); } + #[track_caller] + fn send_take_focus(&mut self, window: x::Window) { + assert!( + self.windows.contains_key(&window), + "Unknown window: {window:?}" + ); + self.send_take_focus_window = window.into(); + } + fn raise_to_top(&mut self, window: Window) { assert!( self.windows.contains_key(&window), @@ -858,6 +868,55 @@ impl TestFixture { (surface, popup_id) } + #[track_caller] + fn setup_popup_focus_case( + &mut self, + comp: &Compositor, + override_redirect: bool, + accepts_input: bool, + take_focus: bool, + ) -> (Window, Window) { + let win_toplevel = Window::new(1); + self.create_toplevel(comp, win_toplevel); + + let win_popup = Window::new(2); + let (buffer, surface) = comp.create_surface(); + let data = WindowData { + mapped: true, + dims: WindowDims { + x: 10, + y: 20, + width: 100, + height: 50, + }, + fullscreen: false, + }; + self.new_window(win_popup, override_redirect, data); + if !override_redirect { + self.satellite + .set_window_role(win_popup, crate::xstate::WindowRole::Popup); + } + + self.satellite.set_win_hints( + win_popup, + super::WmHints { + window_group: None, + accepts_input, + }, + ); + if take_focus { + self.satellite.set_take_focus(win_popup, true); + } + self.map_window(comp, win_popup, &surface.obj, &buffer); + self.run(); + + let popup_id = self.check_new_surface(); + self.testwl.configure_popup(popup_id); + self.run(); + + (win_toplevel, win_popup) + } + #[track_caller] fn assert_window_dimensions( &self, @@ -1342,7 +1401,7 @@ fn window_group_properties() { win, super::WmHints { window_group: Some(prop_win), - acquire_input_via_wm: false, + accepts_input: false, }, ); f.satellite.map_window(win); @@ -1647,103 +1706,51 @@ fn override_redirect_choose_hover_window() { } #[test] -fn popup_no_focus_without_input_hint() { - let (mut f, comp) = TestFixture::new_with_compositor(); +fn popup_override_redirect_never_focused_nor_offered() { + for accepts_input in [true, false] { + for take_focus in [false, true] { + let (mut f, comp) = TestFixture::new_with_compositor(); + + let (win_toplevel, _) = + f.setup_popup_focus_case(&comp, true, accepts_input, take_focus); + assert_eq!( + f.connection().focused_window, + Some(win_toplevel), + "accepts_input={accepts_input:?} take_focus={take_focus}" + ); + assert_eq!( + f.connection().send_take_focus_window, + None, + "accepts_input={accepts_input:?} take_focus={take_focus}" + ); + } + } +} - let win_toplevel = Window::new(1); - let (_, toplevel_id) = f.create_toplevel(&comp, win_toplevel); +#[test] +fn popup_send_take_focus_when_advertised() { + for accepts_input in [true, false] { + let (mut f, comp) = TestFixture::new_with_compositor(); - // A popup without acquire_input_via_wm should not receive focus. - let win_popup = Window::new(2); - f.create_popup( - &comp, - PopupBuilder::new(win_popup, win_toplevel, toplevel_id), - ); - assert_eq!(f.connection().focused_window, Some(win_toplevel)); + let (win_toplevel, win_popup) = f.setup_popup_focus_case(&comp, false, accepts_input, true); + assert_eq!(f.connection().focused_window, Some(win_toplevel)); + assert_eq!(f.connection().send_take_focus_window, Some(win_popup)); + } } #[test] fn popup_focus_on_map_with_input_hint() { let (mut f, comp) = TestFixture::new_with_compositor(); - let win_toplevel = Window::new(1); - let (_, toplevel_id) = f.create_toplevel(&comp, win_toplevel); - - // A popup with acquire_input_via_wm should receive X11 keyboard focus - // at map time, matching what a real X11 WM does for windows with - // WM_HINTS input=True. - let win_popup = Window::new(2); - let (buffer, surface) = comp.create_surface(); - let dims = WindowDims { - x: 10, - y: 20, - width: 100, - height: 50, - }; - let data = WindowData { - mapped: true, - dims, - fullscreen: false, - }; - f.new_window(win_popup, true, data); - f.satellite.set_win_hints( - win_popup, - super::WmHints { - window_group: None, - acquire_input_via_wm: true, - }, - ); - f.map_window(&comp, win_popup, &surface.obj, &buffer); - f.run(); - - let popup_id = f.check_new_surface(); - assert_ne!(popup_id, toplevel_id); - - f.testwl.configure_popup(popup_id); - f.run(); - - // Focus should have been given directly to the popup at map time. + let (_, win_popup) = f.setup_popup_focus_case(&comp, false, true, false); assert_eq!(f.connection().focused_window, Some(win_popup)); } #[test] -fn popup_no_focus_input_hint_wm_take_focus() { +fn popup_no_focus_without_input_hint() { let (mut f, comp) = TestFixture::new_with_compositor(); - let win_toplevel = Window::new(1); - let (_, toplevel_id) = f.create_toplevel(&comp, win_toplevel); - - let win_popup = Window::new(2); - let (buffer, surface) = comp.create_surface(); - let dims = WindowDims { - x: 10, - y: 20, - width: 100, - height: 50, - }; - let data = WindowData { - mapped: true, - dims, - fullscreen: false, - }; - f.new_window(win_popup, true, data); - f.satellite.set_win_hints( - win_popup, - super::WmHints { - window_group: None, - acquire_input_via_wm: true, - }, - ); - f.satellite.set_take_focus(win_popup, true); - f.map_window(&comp, win_popup, &surface.obj, &buffer); - f.run(); - - let popup_id = f.check_new_surface(); - assert_ne!(popup_id, toplevel_id); - - f.testwl.configure_popup(popup_id); - f.run(); - + let (win_toplevel, _) = f.setup_popup_focus_case(&comp, false, false, false); assert_eq!(f.connection().focused_window, Some(win_toplevel)); } diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index efcb13de..7a7e02d2 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -653,6 +653,7 @@ impl XState { let transient_for = self.get_transient_for(window)?; let override_redirect = self.get_override_redirect(window)?; + server_state.set_override_redirect(window, override_redirect); let window_types = self .get_net_wm_window_types(window)? @@ -1010,7 +1011,7 @@ impl From<&[u32]> for WmNormalHints { #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct WmHints { pub window_group: Option, - pub acquire_input_via_wm: bool, + pub accepts_input: bool, } impl From<&[u32]> for WmHints { @@ -1022,8 +1023,11 @@ impl From<&[u32]> for WmHints { let window = x::Window::new(value[8]); ret.window_group = Some(window); } - if flags.contains(WmHintsFlags::Input) { - ret.acquire_input_via_wm = value[1] == 1; + + // if input hint isn't present, set it to true anyways in case the client is misbehaving + ret.accepts_input = true; + if flags.contains(WmHintsFlags::Input) && value[1] == 0 { + ret.accepts_input = false; } ret @@ -1429,6 +1433,22 @@ impl XConnection for RealConnection { } } + fn send_take_focus(&mut self, window: x::Window) { + let resource_id = self.atoms.wm_take_focus.resource_id(); + let data = [resource_id, x::CURRENT_TIME, 0, 0, 0]; + let event = &x::ClientMessageEvent::new( + window, + self.atoms.wm_protocols, + x::ClientMessageData::Data32(data), + ); + unwrap_or_skip_bad_window_ret!(self.connection.send_and_check_request(&x::SendEvent { + destination: x::SendEventDest::Window(window), + propagate: false, + event_mask: x::EventMask::empty(), + event, + })); + } + fn close_window(&mut self, window: x::Window) { let cookie = self.connection.send_request(&x::GetProperty { window,