Preserve shortcut focus during mouse movement

This commit is contained in:
Samuel Onoja committed 2026-10-25 16:35:27 +01:00
1 parent 392e9cfeb0
commit 3d08d64c41
4 files changed
+230 -11

No files matched your search

+14 -3
View File
@@ -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<Self>,
previous_position: Point<f64, Logical>,
position: Point<f64, Logical>,
) {
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);
}
+1 -1
View File
@@ -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()),
+210 -4
View File
@@ -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<Input> for Motion {
fn time(&self) -> smithay::backend::input::InputTime {
smithay::backend::input::InputTime::from_millis(1)
}
fn device(&self) -> WinitVirtualDevice {
WinitVirtualDevice
}
}
impl PointerMotionEvent<Input> 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<Input> 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<Input> for Motion {}
#[derive(Debug)]
struct Button(bool);
impl Event<Input> 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::<Input>(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::<Input>(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::<Input>(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::<Input>(InputEvent::PointerButton {
event: Button(true),
});
assert_eq!(state.focused_window, Some(third_id));
state.process_input_event::<Input>(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::<Input>(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::<Input>(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(
+5 -3
View File
@@ -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