diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 1136fbb..244d15c 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -531,7 +531,8 @@ impl Ferese { return; }; let pointer = self.seat.get_pointer().expect("seat has a pointer"); - if self.capture_pointer_motion(pointer.current_location(), position, true) { + let previous_position = pointer.current_location(); + if self.capture_pointer_motion(previous_position, position, true) { return; } let position = self.constrain_pointer_position(&pointer, position); @@ -547,7 +548,7 @@ impl Ferese { }, ); pointer.frame(self); - self.focus_window_under_pointer(&pointer, position); + self.focus_window_under_pointer(&pointer, previous_position, position); self.activate_focused_pointer_constraint(&pointer); crate::backends::direct::render_cursor(self); } @@ -593,7 +594,7 @@ impl Ferese { }, ); pointer.frame(self); - self.focus_window_under_pointer(&pointer, location); + self.focus_window_under_pointer(&pointer, position, location); self.activate_focused_pointer_constraint(&pointer); crate::backends::direct::render_cursor(self); } @@ -1262,6 +1263,7 @@ impl Ferese { fn focus_window_under_pointer( &mut self, pointer: &PointerHandle, + previous_position: Point, position: Point, ) { if self.session_lock.active() { @@ -1303,6 +1305,15 @@ impl Ferese { if self.windows.ids().get(&window).copied() == self.focused_window { return; } + // Only entering a window changes hover focus. Motion within the same + // window must not undo a keyboard, wheel or side-button selection. + // Hit-test both positions against the current layout so a viewport + // animation beneath the pointer does not count as entering a window. + if self.window_under_visual(previous_position).as_ref() == Some(&window) + && self.layer_under(previous_position).is_none() + { + return; + } self.focus_window_at(position, SERIAL_COUNTER.next_serial(), false); } diff --git a/compositor/src/input/layer_focus_tests.rs b/compositor/src/input/layer_focus_tests.rs index b10899f..6e7091b 100644 --- a/compositor/src/input/layer_focus_tests.rs +++ b/compositor/src/input/layer_focus_tests.rs @@ -262,7 +262,7 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() { Some(panel.wl_surface().clone()), SERIAL_COUNTER.next_serial(), ); - state.focus_window_under_pointer(&pointer, on_window); + state.focus_window_under_pointer(&pointer, pointer.current_location(), on_window); assert_eq!( keyboard.current_focus(), Some(panel.wl_surface().clone()), diff --git a/compositor/src/input/pointer.rs b/compositor/src/input/pointer.rs index 2046717..ac8118d 100644 --- a/compositor/src/input/pointer.rs +++ b/compositor/src/input/pointer.rs @@ -372,8 +372,8 @@ impl Ferese { mod routing_tests { use super::*; use crate::input::{ - ButtonEvent, ButtonState, Event, InputBackend, InputEvent, PointerButtonEvent, - SERIAL_COUNTER, + AbsolutePositionEvent, ButtonEvent, ButtonState, Event, InputBackend, InputEvent, + PointerButtonEvent, PointerMotionEvent, SERIAL_COUNTER, }; use smithay::backend::input::UnusedEvent; use smithay::backend::winit::WinitVirtualDevice; @@ -385,8 +385,8 @@ mod routing_tests { type PointerButtonEvent = Button; type KeyboardKeyEvent = UnusedEvent; type PointerAxisEvent = UnusedEvent; - type PointerMotionEvent = UnusedEvent; - type PointerMotionAbsoluteEvent = UnusedEvent; + type PointerMotionEvent = Motion; + type PointerMotionAbsoluteEvent = Motion; type GestureSwipeBeginEvent = UnusedEvent; type GestureSwipeUpdateEvent = UnusedEvent; type GestureSwipeEndEvent = UnusedEvent; @@ -407,6 +407,51 @@ mod routing_tests { type SwitchToggleEvent = UnusedEvent; type SpecialEvent = UnusedEvent; } + + #[derive(Debug)] + struct Motion(f64, f64); + + impl Event for Motion { + fn time(&self) -> smithay::backend::input::InputTime { + smithay::backend::input::InputTime::from_millis(1) + } + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } + } + + impl PointerMotionEvent for Motion { + fn delta_x(&self) -> f64 { + self.0 + } + fn delta_y(&self) -> f64 { + self.1 + } + fn delta_x_unaccel(&self) -> f64 { + self.0 + } + fn delta_y_unaccel(&self) -> f64 { + self.1 + } + } + + impl AbsolutePositionEvent for Motion { + fn x(&self) -> f64 { + self.0 + } + fn y(&self) -> f64 { + self.1 + } + fn x_transformed(&self, _width: i32) -> f64 { + self.0 + } + fn y_transformed(&self, _height: i32) -> f64 { + self.1 + } + } + + impl smithay::backend::input::PointerMotionAbsoluteEvent for Motion {} + #[derive(Debug)] struct Button(bool); impl Event for Button { @@ -430,6 +475,167 @@ mod routing_tests { } } + #[test] + fn mouse_motion_within_a_window_preserves_shortcut_focus() { + if !crate::startup_tests::private_runtime( + "input::pointer::routing_tests::mouse_motion_within_a_window_preserves_shortcut_focus", + ) { + return; + } + for absolute in [false, true] { + let (mut state, mut events, _output) = crate::input::layer_focus_tests::fixture(); + state + .reload_config_source( + r#" + animations { enabled #false; } + input { focus-follows-mouse #true; } + scrolling { default-column-width 0.3; } + binding "Super+Shift+WheelUp" action="focus" argument="left" + binding "Super+MouseBack" action="focus" argument="left" + "# + .into(), + ) + .unwrap(); + let (first, _first_wire) = + crate::state::test_window(&mut state, &mut events, 0xff112233); + let (second, _second_wire) = + crate::state::test_window(&mut state, &mut events, 0xff334455); + let (third, _third_wire) = + crate::state::test_window(&mut state, &mut events, 0xff556677); + let first_id = state.windows.ids()[&first]; + let second_id = state.windows.ids()[&second]; + let third_id = state.windows.ids()[&third]; + let pointer = state.seat.get_pointer().unwrap(); + let keyboard = state.seat.get_keyboard().unwrap(); + let motion = |state: &mut Ferese, x, y| { + let current = pointer.current_location(); + state.process_input_event::(if absolute { + InputEvent::PointerMotionAbsolute { + event: Motion(x, y), + } + } else { + InputEvent::PointerMotion { + event: Motion(x - current.x, y - current.y), + } + }); + }; + let rect = state.presented_window_rect(third_id).unwrap(); + let (x, y) = (rect.x + 20., rect.y + 20.); + motion(&mut state, x, y); + assert_eq!(state.focused_window, Some(third_id)); + + crate::input::binding_mode_tests::key(&mut state, 133, true); // Super. + crate::input::binding_mode_tests::key(&mut state, 50, true); // Shift. + assert_eq!( + state.filter_pointer_scroll( + "mouse".into(), + AxisSource::Wheel, + [0., -15.], + [None, Some(-120.)], + [false; 2], + ), + [false, true] + ); + assert_eq!(state.focused_window, Some(second_id)); + motion(&mut state, x + 1., y); + assert_eq!( + state.focused_window, + Some(second_id), + "wheel focus must survive mouse jitter" + ); + + crate::input::binding_mode_tests::key(&mut state, 50, false); + state.process_input_event::(InputEvent::PointerButton { + event: Button(true), + }); + assert_eq!(state.focused_window, Some(first_id)); + motion(&mut state, x + 2., y); + assert_eq!( + state.focused_window, + Some(first_id), + "side-button focus must survive mouse jitter" + ); + state.process_input_event::(InputEvent::PointerButton { + event: Button(false), + }); + crate::input::binding_mode_tests::key(&mut state, 133, false); + motion(&mut state, x + 3., y); + assert_eq!( + state.focused_window, + Some(first_id), + "releasing modifiers must preserve the selection" + ); + assert_eq!( + keyboard.current_focus(), + Some(first.toplevel().unwrap().wl_surface().clone()) + ); + + let rect = state.presented_window_rect(second_id).unwrap(); + motion(&mut state, rect.x + 20., rect.y + 20.); + assert_eq!( + state.focused_window, + Some(second_id), + "entering a different window must focus it" + ); + motion(&mut state, x, y); + assert_eq!(state.focused_window, Some(third_id)); + + state.focus_direction(ferese_layout::Direction::Left); + motion(&mut state, x + 1., y); + assert_eq!(state.focused_window, Some(second_id)); + // A click still explicitly focuses the window under the pointer. + state.process_input_event::(InputEvent::PointerButton { + event: Button(true), + }); + assert_eq!(state.focused_window, Some(third_id)); + state.process_input_event::(InputEvent::PointerButton { + event: Button(false), + }); + } + } + + #[test] + fn viewport_scroll_under_pointer_does_not_restore_hover_focus() { + if !crate::startup_tests::private_runtime( + "input::pointer::routing_tests::viewport_scroll_under_pointer_does_not_restore_hover_focus", + ) { + return; + } + let (mut state, mut events, _output) = crate::input::layer_focus_tests::fixture(); + state.input_settings.focus_follows_mouse = true; + let (first, _first_wire) = crate::state::test_window(&mut state, &mut events, 0xff112233); + let (_second, _second_wire) = + crate::state::test_window(&mut state, &mut events, 0xff334455); + let (third, _third_wire) = crate::state::test_window(&mut state, &mut events, 0xff556677); + let first_id = state.windows.ids()[&first]; + let rect = state + .presented_window_rect(state.windows.ids()[&third]) + .unwrap(); + let pointer = state.seat.get_pointer().unwrap(); + state.process_input_event::(InputEvent::PointerMotionAbsolute { + event: Motion(rect.x + 20., rect.y + 20.), + }); + assert_eq!( + state.window_under_visual(pointer.current_location()), + Some(third.clone()) + ); + state.focus_direction(ferese_layout::Direction::Left); + state.focus_direction(ferese_layout::Direction::Left); + assert_eq!(state.focused_window, Some(first_id)); + assert_ne!( + state.window_under_visual(pointer.current_location()), + Some(third) + ); + state.process_input_event::(InputEvent::PointerMotion { + event: Motion(1., 0.), + }); + assert_eq!( + state.focused_window, + Some(first_id), + "layout movement beneath the pointer is not a hover entry" + ); + } + #[test] fn pointer_bindings_use_modes_and_consume_releases_after_modifier_changes() { if !crate::startup_tests::private_runtime( diff --git a/docs/configuration/input.md b/docs/configuration/input.md index 8569261..efb93e0 100644 --- a/docs/configuration/input.md +++ b/docs/configuration/input.md @@ -9,9 +9,11 @@ | `repeat-rate` | integer > 0 | `25` | Repeats per second | | `repeat-delay-ms` | integer ≥ 0 | `600` | Delay before repeat | -With focus-follows-mouse enabled, hovering a visible part of a window changes -keyboard focus without raising it or moving the scrolling viewport. Clicking or -using directional keyboard focus brings the selected window into view. +With focus-follows-mouse enabled, moving into a visible part of another window +changes keyboard focus without raising it or moving the scrolling viewport. +If you select a window with a shortcut, moving the pointer within its current +window keeps that selection until you cross into another window or click. +Clicking or using directional focus brings the selected window into view. In Overview (`Super+Tab`), click a window preview or a miniature inside a workspace card to activate that exact window. Hover highlights the target; directional