diff --git a/compositor/src/handlers/layer_shell.rs b/compositor/src/handlers/layer_shell.rs index 4f6fc96..ce8848e 100644 --- a/compositor/src/handlers/layer_shell.rs +++ b/compositor/src/handlers/layer_shell.rs @@ -159,9 +159,12 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { .layer_for_surface(&surface, WindowSurfaceType::ALL) .is_some_and(|focused| focused == layer) }); - if !state.session_lock.active() + state.reconcile_layer_keyboard_focus(); + if pointer_on_layer + && !state.session_lock.active() && !state.input_capture.captures(1) - && (policy.keyboard_interactivity == KeyboardInteractivity::Exclusive || pointer_on_layer) + && state.layer_keyboard_eligible(&layer) + && state.exclusive_keyboard_layer().is_none() && matches!(policy.layer, Layer::Top | Layer::Overlay) && !layer_has_keyboard_focus(state, &layer) { @@ -170,8 +173,6 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { Some(layer.wl_surface().clone()), smithay::utils::SERIAL_COUNTER.next_serial(), ); - } else if !layer.can_receive_keyboard_focus() && layer_has_keyboard_focus(state, &layer) { - state.restore_keyboard_focus(); } if let Err(error) = state.display_handle.flush_clients() { @@ -179,13 +180,99 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { } } +impl Ferese { + /// Layer-map membership tracks the role and output, not buffer mapping. + /// Use the renderer state updated by on_commit_buffer_handler, as for windows. + pub(crate) fn layer_keyboard_eligible(&self, layer: &DesktopLayerSurface) -> bool { + layer.layer_surface().alive() + && layer.can_receive_keyboard_focus() + && smithay::backend::renderer::utils::with_renderer_surface_state(layer.wl_surface(), |state| { + state.buffer().is_some() + }) + .unwrap_or(false) + && self.space.outputs().any(|output| { + layer_map_for_output(output) + .layers() + .any(|candidate| candidate == layer) + }) + } + + fn keyboard_layer_root(&self) -> Option { + let surface = self.seat.get_keyboard()?.current_focus()?; + let root = self + .popups + .find_popup(&surface) + .and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok()) + .unwrap_or(surface); + (smithay::wayland::compositor::get_role(&root) == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE)) + .then_some(root) + } + + pub(crate) fn keyboard_focused_layer(&self) -> Option { + self.space + .layer_for_surface(&self.keyboard_layer_root()?, WindowSurfaceType::ALL) + } + + pub(crate) fn exclusive_keyboard_layer(&self) -> Option { + // Overlay outranks Top. Keep the current owner among peers; the protocol + // leaves ordering within one layer to the compositor. + let focused = self.keyboard_focused_layer(); + for priority in [Layer::Overlay, Layer::Top] { + if let Some(layer) = focused.as_ref().filter(|layer| { + layer.layer() == priority + && layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive + && self.layer_keyboard_eligible(layer) + }) { + return Some(layer.clone()); + } + for output in self.space.outputs() { + let layers = layer_map_for_output(output) + .layers_on(priority) + .rev() + .cloned() + .collect::>(); + if let Some(layer) = layers.into_iter().find(|layer| { + layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive + && self.layer_keyboard_eligible(layer) + }) { + return Some(layer.clone()); + } + } + } + None + } + + pub(crate) fn reconcile_layer_keyboard_focus(&mut self) { + if self.session_lock.active() || self.input_capture.captures(1) { + return; + } + let keyboard = self.seat.get_keyboard().expect("seat has a keyboard"); + if let Some(root) = self.keyboard_layer_root().filter(|_| { + !self + .keyboard_focused_layer() + .is_some_and(|layer| self.layer_keyboard_eligible(&layer)) + }) { + // A popup grab cannot keep an ineligible layer alive as input owner. + for (popup, _) in smithay::desktop::PopupManager::popups_for_surface(&root) { + let _ = smithay::desktop::PopupManager::dismiss_popup(&root, &popup); + } + keyboard.unset_grab(self); + self.restore_keyboard_focus(); + } else if !keyboard.is_grabbed() + && let Some(layer) = self.exclusive_keyboard_layer() + && !layer_has_keyboard_focus(self, &layer) + { + keyboard.set_focus( + self, + Some(layer.wl_surface().clone()), + smithay::utils::SERIAL_COUNTER.next_serial(), + ); + } + } +} + fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool { - state - .seat - .get_keyboard() - .and_then(|keyboard| keyboard.current_focus()) - .and_then(|surface| state.space.layer_for_surface(&surface, WindowSurfaceType::ALL)) - .is_some_and(|focused| focused == *layer) + state.keyboard_focused_layer().is_some_and(|focused| focused == *layer) } #[cfg(test)] diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 170e3fa..f27aeaf 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -998,9 +998,13 @@ impl Ferese { } let keyboard = self.seat.get_keyboard().expect("seat has a keyboard"); self.focus_output_at(position); + if let Some(layer) = self.exclusive_keyboard_layer() { + keyboard.set_focus(self, Some(layer.wl_surface().clone()), serial); + return; + } if let Some((layer, _, _)) = self.layer_under(position) { - if layer.can_receive_keyboard_focus() { + if self.layer_keyboard_eligible(&layer) { keyboard.set_focus(self, Some(layer.wl_surface().clone()), serial); } else if raise && keyboard.current_focus().is_some_and(|surface| { diff --git a/compositor/src/input/layer_focus_tests.rs b/compositor/src/input/layer_focus_tests.rs index 913ea42..bd7def5 100644 --- a/compositor/src/input/layer_focus_tests.rs +++ b/compositor/src/input/layer_focus_tests.rs @@ -12,6 +12,17 @@ use std::os::unix::net::UnixStream; use std::sync::Arc; fn layer(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, output: &Output) -> (LayerSurface, UnixStream) { + layer_with_policy(state, events, output, 0, 2) +} + +fn layer_with_policy( + state: &mut Ferese, + events: &mut EventLoop<'static, Ferese>, + output: &Output, + interactivity: u32, + level: u32, +) -> (LayerSurface, UnixStream) { + let initial_focus = state.seat.get_keyboard().unwrap().current_focus(); let (server, mut wire) = UnixStream::pair().unwrap(); let client = state .display_handle @@ -31,13 +42,19 @@ fn layer(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, output: &O &mut wire, layers.id().protocol_id(), 0, - &[3, 2, 0, 2, 4, u32::from_ne_bytes(*b"bar\0")], + &[3, 2, 0, level, 4, u32::from_ne_bytes(*b"bar\0")], None, ); request(&mut wire, 3, 0, &[64, 48], None); request(&mut wire, 3, 1, &[1 | 4], None); // Top left. + request(&mut wire, 3, 4, &[interactivity], None); request(&mut wire, 2, 6, &[], None); dispatch(events, state); + assert_eq!( + state.seat.get_keyboard().unwrap().current_focus(), + initial_focus, + "an initially unconfigured layer must not acquire focus" + ); wire.set_nonblocking(true).unwrap(); let mut bytes = Vec::new(); @@ -57,6 +74,12 @@ fn layer(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, output: &O offset += (header >> 16) as usize; } request(&mut wire, 3, 6, &[serial.expect("layer configure")], None); + dispatch(events, state); + assert_eq!( + state.seat.get_keyboard().unwrap().current_focus(), + initial_focus, + "acknowledging configure without mapping must not acquire focus" + ); let mut file = tempfile::tempfile().unwrap(); file.write_all(&0xff112233u32.to_ne_bytes().repeat(64 * 48)).unwrap(); request( @@ -221,3 +244,225 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() { "enabling a panel away from the pointer must not steal window focus" ); } + +fn fixture() -> (Ferese, EventLoop<'static, Ferese>, Output) { + let mut events = EventLoop::try_new().unwrap(); + let mut config = crate::config::Config::default().runtime_config().unwrap(); + config.wallpaper.path = None; + config.animations_enabled = false; + let mut state = Ferese::new( + &mut events, + smithay::reexports::wayland_server::Display::new().unwrap(), + config, + ) + .unwrap(); + let output = Output::new( + "layers".into(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + output.change_current_state( + Some(Mode { + size: (800, 600).into(), + refresh: 60_000, + }), + None, + None, + None, + ); + state.space.map_output(&output, (0, 0)); + state.register_output(&output, "layers".into()); + (state, events, output) +} + +fn commit_policy(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream, keyboard: u32) { + request(wire, 3, 4, &[keyboard], None); + request(wire, 2, 6, &[], None); + dispatch(events, state); +} + +#[test] +fn null_buffer_commit_revokes_layer_focus_without_destroying_role() { + if !crate::startup_tests::private_runtime( + "input::layer_focus_tests::null_buffer_commit_revokes_layer_focus_without_destroying_role", + ) { + return; + } + let (mut state, mut events, output) = fixture(); + let (window, _window_wire) = window(&mut state, &mut events, 0xff112233); + let id = state.windows.ids()[&window]; + assert!(state.activate_managed_window(id)); + let fallback = Some(window.toplevel().unwrap().wl_surface().clone()); + let keyboard = state.seat.get_keyboard().unwrap(); + for interactivity in [1, 2] { + let (panel, mut wire) = layer(&mut state, &mut events, &output); + commit_policy(&mut state, &mut events, &mut wire, interactivity); + state.focus_window_at((20., 20.).into(), SERIAL_COUNTER.next_serial(), true); + assert_eq!(keyboard.current_focus(), Some(panel.wl_surface().clone())); + request(&mut wire, 2, 1, &[0, 0, 0], None); // wl_surface.attach(NULL). + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + assert!(panel.layer_surface().alive(), "the layer role survives unmapping"); + assert!( + state + .space + .layer_for_surface(panel.wl_surface(), smithay::desktop::WindowSurfaceType::TOPLEVEL) + .is_some() + ); + assert!( + smithay::backend::renderer::utils::with_renderer_surface_state(panel.wl_surface(), |s| s + .buffer() + .is_none()) + .unwrap() + ); + assert_eq!( + keyboard.current_focus(), + fallback, + "null commit must revoke keyboard focus for policy {interactivity}" + ); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + assert_eq!( + keyboard.current_focus(), + fallback, + "unmapped commits must not reclaim focus" + ); + // Reconfigure with a changed size before mapping again. This follows + // the protocol's bufferless-commit/configure/ack sequence, including + // with Smithay 0.7 which retains configure state on null commits. + request(&mut wire, 3, 0, &[65, 48], None); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + ack_latest_configure(&mut wire); + request(&mut wire, 2, 1, &[5, 0, 0], None); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + if interactivity == 2 { + assert_eq!( + keyboard.current_focus(), + fallback, + "on-demand remap requires interaction" + ); + state.focus_window_at((20., 20.).into(), SERIAL_COUNTER.next_serial(), true); + } + assert_eq!( + keyboard.current_focus(), + Some(panel.wl_surface().clone()), + "remapped layer remains focusable" + ); + request(&mut wire, 3, 7, &[], None); // Destroy the role. + dispatch(&mut events, &mut state); + assert_eq!(keyboard.current_focus(), fallback, "destruction restores window focus"); + } +} + +fn ack_latest_configure(wire: &mut UnixStream) { + wire.set_nonblocking(true).unwrap(); + let mut bytes = Vec::new(); + assert_eq!( + wire.read_to_end(&mut bytes).unwrap_err().kind(), + std::io::ErrorKind::WouldBlock + ); + wire.set_nonblocking(false).unwrap(); + let mut serial = None; + let mut offset = 0; + while offset < bytes.len() { + let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + if object == 3 && header & 0xffff == 0 { + serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap())); + } + offset += (header >> 16) as usize; + } + request(wire, 3, 6, &[serial.expect("layer configure")], None); +} + +#[test] +fn exclusive_layer_priority_and_output_removal_restore_eligible_focus() { + if !crate::startup_tests::private_runtime( + "input::layer_focus_tests::exclusive_layer_priority_and_output_removal_restore_eligible_focus", + ) { + return; + } + let (mut state, mut events, output) = fixture(); + let (window, _window_wire) = window(&mut state, &mut events, 0xff112233); + assert!(state.activate_managed_window(state.windows.ids()[&window])); + let fallback = Some(window.toplevel().unwrap().wl_surface().clone()); + let keyboard = state.seat.get_keyboard().unwrap(); + let (top, mut top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2); + assert_eq!(keyboard.current_focus(), Some(top.wl_surface().clone())); + let (overlay, mut overlay_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 3); + assert_eq!(keyboard.current_focus(), Some(overlay.wl_surface().clone())); + commit_policy(&mut state, &mut events, &mut top_wire, 1); + assert_eq!( + keyboard.current_focus(), + Some(overlay.wl_surface().clone()), + "Top commits cannot steal from an exclusive Overlay" + ); + request(&mut overlay_wire, 2, 1, &[0, 0, 0], None); + request(&mut overlay_wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + assert_eq!( + keyboard.current_focus(), + Some(top.wl_surface().clone()), + "unmapping falls back to another eligible exclusive layer" + ); + commit_policy(&mut state, &mut events, &mut top_wire, 0); + assert_eq!( + keyboard.current_focus(), + fallback, + "disabling keyboard interaction restores the window" + ); + commit_policy(&mut state, &mut events, &mut top_wire, 1); + assert_eq!(keyboard.current_focus(), Some(top.wl_surface().clone())); + state.unregister_output(&output); + assert_eq!( + keyboard.current_focus(), + None, + "removed outputs cannot retain layer focus" + ); +} + +#[test] +fn layer_mapping_commits_do_not_override_session_lock() { + use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_manager_v1::ExtSessionLockManagerV1; + if !crate::startup_tests::private_runtime( + "input::layer_focus_tests::layer_mapping_commits_do_not_override_session_lock", + ) { + return; + } + let (mut state, mut events, output) = fixture(); + let (panel, mut wire) = layer_with_policy(&mut state, &mut events, &output, 1, 3); + let client = panel.wl_surface().client().unwrap(); + let manager = client + .create_resource::(&state.display_handle, 1, ()) + .unwrap(); + request(&mut wire, manager.id().protocol_id(), 1, &[6], None); + dispatch(&mut events, &mut state); + let keyboard = state.seat.get_keyboard().unwrap(); + assert_eq!(keyboard.current_focus(), None); + commit_policy(&mut state, &mut events, &mut wire, 1); + assert_eq!( + keyboard.current_focus(), + None, + "an exclusive layer cannot focus during lock acquisition" + ); + request(&mut wire, 2, 1, &[0, 0, 0], None); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + request(&mut wire, 2, 1, &[5, 0, 0], None); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + assert_eq!(keyboard.current_focus(), None, "layer remapping cannot bypass the lock"); + request(&mut wire, 3, 7, &[], None); + dispatch(&mut events, &mut state); + assert_eq!( + keyboard.current_focus(), + None, + "layer destruction preserves lock-owned focus policy" + ); +} diff --git a/compositor/src/state/navigation.rs b/compositor/src/state/navigation.rs index 57907db..f6f0b81 100644 --- a/compositor/src/state/navigation.rs +++ b/compositor/src/state/navigation.rs @@ -713,10 +713,14 @@ impl Ferese { return; } let surface = self - .focused_window - .and_then(|focused| self.windows.window(focused)) - .and_then(|window| window.toplevel()) - .map(|toplevel| toplevel.wl_surface().clone()); + .exclusive_keyboard_layer() + .map(|layer| layer.wl_surface().clone()) + .or_else(|| { + self.focused_window + .and_then(|focused| self.windows.window(focused)) + .and_then(|window| window.toplevel()) + .map(|toplevel| toplevel.wl_surface().clone()) + }); self.seat.get_keyboard().expect("seat has a keyboard").set_focus( self, diff --git a/compositor/src/state/reconciliation.rs b/compositor/src/state/reconciliation.rs index e48cf13..2870b01 100644 --- a/compositor/src/state/reconciliation.rs +++ b/compositor/src/state/reconciliation.rs @@ -178,6 +178,7 @@ impl Ferese { if changes.focus || self.session_lock.active() { self.restore_keyboard_focus(); } + self.reconcile_layer_keyboard_focus(); self.refresh_lock_outputs(); self.refresh_idle_inhibition(); let batch = self.desktop_transition.take().unwrap();