diff --git a/compositor/src/handlers/layer_shell.rs b/compositor/src/handlers/layer_shell.rs index dd0c65a..a72be11 100644 --- a/compositor/src/handlers/layer_shell.rs +++ b/compositor/src/handlers/layer_shell.rs @@ -65,6 +65,13 @@ impl WlrLayerShellHandler for Ferese { } fn layer_destroyed(&mut self, surface: LayerSurface) { + if self + .pending_layer_focus + .as_ref() + .is_some_and(|(target, _)| target == surface.wl_surface()) + { + self.pending_layer_focus = None; + } let Some(layer) = self .space .layer_for_surface(surface.wl_surface(), WindowSurfaceType::TOPLEVEL) @@ -137,6 +144,14 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { .cloned() .collect::>(); + if !state.layer_mapped_and_visible(&layer) + && state + .pending_layer_focus + .as_ref() + .is_some_and(|(target, _)| target == surface) + { + state.pending_layer_focus = None; + } let policy = layer.cached_state(); let current = LayoutState::current(&layer); let previous = layer @@ -159,22 +174,24 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { // Configure acknowledgement and keyboard policy are independent of layout. layer.layer_surface().send_pending_configure(); - // A click can enable keyboard interaction after its button event was - // delivered. Give that layer focus without requiring a second click. - let pointer_on_layer = requested_focus - && !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed()) + // Consume the opening interaction once. Later commits, pointer hover, or + // another input target must not give a panel a fresh claim to focus. + let opening_interaction = if requested_focus && state - .seat - .get_pointer() - .and_then(|pointer| pointer.current_focus()) - .is_some_and(|surface| { - state - .space - .layer_for_surface(&surface, WindowSurfaceType::ALL) - .is_some_and(|focused| focused == layer) - }); + .pending_layer_focus + .as_ref() + .is_some_and(|(target, _)| target == surface) + { + state + .pending_layer_focus + .take() + .is_some_and(|(_, at)| at.elapsed() <= std::time::Duration::from_secs(5)) + } else { + false + }; state.reconcile_layer_keyboard_focus(); - if pointer_on_layer + if opening_interaction + && !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed()) && !state.session_lock.active() && !state.input_capture.captures(1) && state.layer_keyboard_eligible(&layer) diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index be26387..e142678 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -72,6 +72,7 @@ impl SeatHandler for Ferese { } fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { + self.pending_layer_focus = None; // Smithay has already sent enter. last_enter uses a separate mutex; // querying current_focus here would reenter the held keyboard lock. if let Some(serial) = seat.get_keyboard().and_then(|keyboard| keyboard.last_enter()) { diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 4c3fbe5..f4856ae 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -867,6 +867,7 @@ impl Ferese { ); } InputEvent::TouchCancel { .. } => { + self.pending_layer_focus = None; self.consumed_touch_slots.clear(); let touch = self.seat.get_touch().expect("seat has touch capability"); touch.cancel(self); @@ -1012,6 +1013,7 @@ impl Ferese { } fn focus_window_at(&mut self, position: Point, serial: Serial, raise: bool) { + self.pending_layer_focus = None; if self.input_capture.captures(1) { return; } @@ -1044,6 +1046,14 @@ impl Ferese { // release an expanded panel's focus too. self.restore_keyboard_focus(); } + if raise + && layer.cached_state().keyboard_interactivity + == smithay::wayland::shell::wlr_layer::KeyboardInteractivity::None + { + // The client may enable OnDemand only after receiving this + // opening press. Touch does not establish pointer focus. + self.pending_layer_focus = Some((layer.wl_surface().clone(), std::time::Instant::now())); + } return; } diff --git a/compositor/src/input/layer_focus_tests.rs b/compositor/src/input/layer_focus_tests.rs index 724f228..2891864 100644 --- a/compositor/src/input/layer_focus_tests.rs +++ b/compositor/src/input/layer_focus_tests.rs @@ -759,3 +759,82 @@ fn animated_overview_exit_revokes_top_focus_when_it_becomes_hidden() { "completion of the visibility transition revokes focus without a client commit" ); } + +#[test] +fn touch_opened_on_demand_layer_does_not_need_pointer_focus() { + if !crate::startup_tests::private_runtime( + "input::layer_focus_tests::touch_opened_on_demand_layer_does_not_need_pointer_focus", + ) { + return; + } + let (mut state, mut events, output) = fixture(); + let (panel, mut wire) = layer(&mut state, &mut events, &output); + let pointer = state.seat.get_pointer().unwrap(); + let keyboard = state.seat.get_keyboard().unwrap(); + let inside = Point::from((20., 20.)); + assert_eq!(pointer.current_focus(), None); + // TouchDown focuses the target before delivering the opening touch. + // Unlike the pointer regression, no pointer motion establishes focus. + state.focus_window_at(inside, SERIAL_COUNTER.next_serial(), true); + assert_eq!(keyboard.current_focus(), None); + request(&mut wire, 3, 4, &[2], None); // OnDemand. + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + assert_eq!( + keyboard.current_focus(), + Some(panel.wl_surface().clone()), + "opening must not require a second click" + ); + state.focus_window_at(inside, SERIAL_COUNTER.next_serial(), true); + assert_eq!( + keyboard.current_focus(), + Some(panel.wl_surface().clone()), + "inside clicks keep the panel open" + ); + + let outside = Point::from((400., 300.)); + let input_time = state.last_pointer_time; + pointer.motion( + &mut state, + None, + &MotionEvent { + location: outside, + serial: SERIAL_COUNTER.next_serial(), + time: input_time, + }, + ); + state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true); + assert_eq!(keyboard.current_focus(), None, "empty-desktop clicks dismiss the panel"); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + assert_eq!( + keyboard.current_focus(), + None, + "later animation commits must not reclaim focus" + ); + commit_policy(&mut state, &mut events, &mut wire, 0); + state.focus_window_at(inside, SERIAL_COUNTER.next_serial(), true); + state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true); + commit_policy(&mut state, &mut events, &mut wire, 2); + assert_eq!( + keyboard.current_focus(), + None, + "a later interaction cancels delayed focus" + ); + commit_policy(&mut state, &mut events, &mut wire, 0); + pointer.motion( + &mut state, + Some((panel.wl_surface().clone(), (0., 0.).into())), + &MotionEvent { + location: inside, + serial: SERIAL_COUNTER.next_serial(), + time: input_time, + }, + ); + commit_policy(&mut state, &mut events, &mut wire, 2); + assert_eq!( + keyboard.current_focus(), + None, + "pointer hover alone cannot open keyboard focus" + ); +} diff --git a/compositor/src/state.rs b/compositor/src/state.rs index f3de58a..e760173 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -470,6 +470,7 @@ pub struct Ferese { pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings, pub(crate) tab_bars: HashMap>, pub(crate) consumed_pointer_buttons: HashSet, + pub(crate) pending_layer_focus: Option<(WlSurface, Instant)>, pub(crate) consumed_touch_slots: HashSet, pub(crate) focus_history: ferese_core::FocusHistory, pub(crate) focus_cycle: Option, @@ -785,6 +786,7 @@ impl Ferese { tab_indicator_settings: config.tab_indicator, tab_bars: HashMap::new(), consumed_pointer_buttons: HashSet::new(), + pending_layer_focus: None, consumed_touch_slots: HashSet::new(), focus_history: Default::default(), focus_cycle: None,