diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index ab344838..ca37dc82 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1108,6 +1108,10 @@ impl ConfigClient { self.send(&ClientMessage::SeatEnableUnicodeInput { seat }); } + pub fn seat_set_mouse_follows_focus(&self, seat: Seat, enabled: bool) { + self.send(&ClientMessage::SeatSetMouseFollowsFocus { seat, enabled }); + } + pub fn set_show_float_pin_icon(&self, show: bool) { self.send(&ClientMessage::SetShowFloatPinIcon { show }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 8db94669..6dad4216 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -871,6 +871,10 @@ pub enum ClientMessage<'a> { dx2: i32, dy2: i32, }, + SeatSetMouseFollowsFocus { + seat: Seat, + enabled: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index dd6ecfeb..de8bd0ee 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -655,6 +655,15 @@ impl Seat { pub fn resize(self, dx1: i32, dy1: i32, dx2: i32, dy2: i32) { self.window().resize(dx1, dy1, dx2, dy2); } + + /// Sets whether the cursor should automatically move to the center of a window + /// when focus changes via keyboard commands (move-left, focus-right, show-workspace, etc.). + /// + /// The default is `false`. + #[deprecated = "This setting is unstable and might be removed in the future"] + pub fn unstable_set_mouse_follows_focus(self, enabled: bool) { + get!().seat_set_mouse_follows_focus(self, enabled) + } } /// A focus-follows-mouse mode. diff --git a/src/config/handler.rs b/src/config/handler.rs index 1c694f1e..6635f93c 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2425,6 +2425,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_seat_set_mouse_follows_focus( + &self, + seat: Seat, + enabled: bool, + ) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.set_mouse_follows_focus(enabled); + Ok(()) + } + fn get_sized(&self, sized: Resizable) -> Result { use jay_config::theme::sized::*; let sized = match sized { @@ -2597,6 +2607,7 @@ impl ConfigProxyHandler { return Err(CphError::WindowNotVisible(window_id)); } seat.focus_toplevel(window); + seat.maybe_schedule_warp_mouse_to_focus(); Ok(()) } @@ -3354,6 +3365,9 @@ impl ConfigProxyHandler { ClientMessage::SeatWarpMouseToFocus { seat } => self .handle_seat_warp_mouse_to_focus(seat) .wrn("seat_warp_mouse_to_focus")?, + ClientMessage::SeatSetMouseFollowsFocus { seat, enabled } => self + .handle_seat_set_mouse_follows_focus(seat, enabled) + .wrn("seat_set_mouse_follows_focus")?, ClientMessage::ConnectorSetUseNativeGamut { connector, use_native_gamut, diff --git a/src/control_center/cc_input.rs b/src/control_center/cc_input.rs index 2f6ce610..bac0db1f 100644 --- a/src/control_center/cc_input.rs +++ b/src/control_center/cc_input.rs @@ -232,6 +232,9 @@ impl InputPane { bool(ui, "Focus Follows Mouse", seat.focus_follows_mouse(), |v| { seat.set_focus_follows_mouse(v); }); + bool(ui, "Mouse Follows Focus", seat.mouse_follows_focus(), |v| { + seat.set_mouse_follows_focus(v); + }); combo_box_ui( ui, "Fallback Output Mode", diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 326d34a2..ea94cd79 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -260,6 +260,8 @@ pub struct WlSeatGlobal { simple_im: CloneCell>>, simple_im_enabled: Cell, warp_mouse_to_focus_scheduled: Cell, + warp_mouse_to_focus_skip_target_check: Cell, + mouse_follows_focus: Cell, } impl PartialEq for WlSeatGlobal { @@ -403,6 +405,8 @@ impl WlSeatGlobal { simple_im: CloneCell::new(simple_im), simple_im_enabled: Cell::new(true), warp_mouse_to_focus_scheduled: Cell::new(false), + warp_mouse_to_focus_skip_target_check: Cell::new(false), + mouse_follows_focus: Cell::new(false), }); slf.pointer_cursor.set_owner(slf.clone()); slf.modifiers_listener @@ -537,12 +541,13 @@ impl WlSeatGlobal { self.get_cursor_output() } - pub fn set_workspace(&self, ws: &Rc) { + pub fn set_workspace(self: &Rc, ws: &Rc) { let tl = match self.keyboard_node.get().node_toplevel() { Some(tl) => tl, _ => return, }; toplevel_set_workspace(&self.state, tl, ws); + self.maybe_schedule_warp_mouse_to_focus(); } pub fn mark_last_active(self: &Rc) { @@ -743,6 +748,7 @@ impl WlSeatGlobal { && let Some(tl) = parent.node_toplevel() { self.focus_node(tl); + self.maybe_schedule_warp_mouse_to_focus(); } } @@ -802,6 +808,7 @@ impl WlSeatGlobal { .find_output_in_direction(&ws.output.get(), direction) { target.take_keyboard_navigation_focus(self, direction); + self.maybe_schedule_warp_mouse_to_focus(); } return; } @@ -821,6 +828,14 @@ impl WlSeatGlobal { c.move_focus_from_child(self, tl.deref(), direction); } } + self.maybe_schedule_warp_mouse_to_focus(); + } + + pub fn maybe_schedule_warp_mouse_to_focus(self: &Rc) { + if self.mouse_follows_focus() { + self.warp_mouse_to_focus_skip_target_check.set(true); + self.schedule_warp_mouse_to_focus(); + } } pub fn schedule_warp_mouse_to_focus(self: &Rc) { @@ -848,10 +863,12 @@ impl WlSeatGlobal { { let ws = target.ensure_workspace(); toplevel_set_workspace(&self.state, tl, &ws); + self.maybe_schedule_warp_mouse_to_focus(); } else if let Some(parent) = data.parent.get() && let Some(c) = parent.node_into_container() { c.move_child(tl, direction); + self.maybe_schedule_warp_mouse_to_focus(); } } @@ -972,6 +989,7 @@ impl WlSeatGlobal { } } self.focus_node(node); + self.maybe_schedule_warp_mouse_to_focus(); } pub fn focus_prev(self: &Rc) { @@ -1035,6 +1053,7 @@ impl WlSeatGlobal { n.deref() .clone() .node_do_focus(self, Direction::Unspecified); + self.maybe_schedule_warp_mouse_to_focus(); return; } } @@ -1046,6 +1065,7 @@ impl WlSeatGlobal { n.deref() .clone() .node_do_focus(self, Direction::Unspecified); + self.maybe_schedule_warp_mouse_to_focus(); return; } } @@ -1087,6 +1107,7 @@ impl WlSeatGlobal { && ws.container_visible() { self.focus_node(ws.clone()); + self.maybe_schedule_warp_mouse_to_focus(); return; } None @@ -1111,6 +1132,7 @@ impl WlSeatGlobal { if let Some(n) = node { if node_viable(&*n) { n.node_do_focus(self, Direction::Unspecified); + self.maybe_schedule_warp_mouse_to_focus(); return; } } @@ -1164,6 +1186,7 @@ impl WlSeatGlobal { }; if node.node_visible() && node.node_accepts_focus() { node.node_do_focus(self, Direction::Unspecified); + self.maybe_schedule_warp_mouse_to_focus(); } } @@ -1507,6 +1530,15 @@ impl WlSeatGlobal { self.focus_follows_mouse.get() } + pub fn set_mouse_follows_focus(&self, enabled: bool) { + self.mouse_follows_focus.set(enabled); + self.state.trigger_cci(CCI_INPUT); + } + + pub fn mouse_follows_focus(&self) -> bool { + self.mouse_follows_focus.get() + } + pub fn set_fallback_output_mode(&self, fallback_output_mode: FallbackOutputMode) { self.fallback_output_mode.set(fallback_output_mode); self.state.trigger_cci(CCI_INPUT); @@ -2023,15 +2055,18 @@ pub async fn handle_warp_mouse_to_focus(state: Rc) { state.eng.yield_now().await; while let Some(seat) = state.pending_warp_mouse_to_focus.try_pop() { seat.warp_mouse_to_focus_scheduled.set(false); + let skip_target_check = seat.warp_mouse_to_focus_skip_target_check.take(); let Some(tl) = seat.keyboard_node.get().node_toplevel() else { continue; }; let (x, y) = tl.node_absolute_position().center(); - let Some(target) = state.node_at(x, y).node.node_toplevel() else { - continue; - }; - if target.node_id() != tl.node_id() { - continue; + if !skip_target_check { + let Some(target) = state.node_at(x, y).node.node_toplevel() else { + continue; + }; + if target.node_id() != tl.node_id() { + continue; + } } let (x, y) = (Fixed::from_int(x), Fixed::from_int(y)); seat.motion_event_abs(state.now_usec(), x, y, Warp); diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 1b416394..0c8fd488 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -1084,6 +1084,7 @@ impl WlSeatGlobal { } } self.focus_node(node); + self.maybe_schedule_warp_mouse_to_focus(); } } diff --git a/src/state.rs b/src/state.rs index 25aceac0..4eded0c4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -974,6 +974,7 @@ impl State { } }; self.show_workspace2(Some(seat), &ws.output.get(), &ws); + seat.maybe_schedule_warp_mouse_to_focus(); } pub fn float_map_ws(&self) -> Rc { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index b30d225e..90488e95 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -563,6 +563,7 @@ pub struct Config { pub workspace_display_order: Option, pub simple_im: Option, pub fallback_output_mode: Option, + pub mouse_follows_focus: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index c8dd76dd..9a150ecb 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -154,6 +154,7 @@ impl Parser for ConfigParser<'_> { fallback_output_mode_val, egui_val, clean_logs_older_than_val, + mouse_follows_focus, ), ) = ext.extract(( ( @@ -214,6 +215,7 @@ impl Parser for ConfigParser<'_> { opt(val("fallback-output-mode")), opt(val("egui")), opt(val("clean-logs-older-than")), + recover(opt(bol("unstable-mouse-follows-focus"))), ), ))?; let mut keymap = None; @@ -615,6 +617,7 @@ impl Parser for ConfigParser<'_> { workspace_display_order, simple_im, fallback_output_mode, + mouse_follows_focus: mouse_follows_focus.despan(), }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 17d120a1..171361d9 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -1664,6 +1664,12 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index cc5dbf93..b93db67a 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1061,6 +1061,10 @@ "type": "boolean", "description": "Configures whether moving the mouse over a window automatically moves the keyboard\nfocus to that window.\n\nThe default is `true`.\n" }, + "unstable-mouse-follows-focus": { + "type": "boolean", + "description": "Configures whether the mouse cursor is automatically centered on the active window\nwhen focus changes via keyboard commands.\n\nWhen enabled, the cursor will be automatically positioned to the center of the\nactive window when focus changes through keyboard commands such as `focus-left`,\n`focus-right`, `show-workspace`, etc.\n\nThe default is `false`.\n\nThis option is unstable due to various issues. It is not subject to the usual\nsemver guarantees.\n\n- Example:\n\n ```toml\n unstable-mouse-follows-focus = true\n ```\n" + }, "window-management-key": { "type": "string", "description": "Configures a key that will enable window management mode while pressed.\n\nIn window management mode, floating windows can be moved by pressing the left\nmouse button and all windows can be resize by pressing the right mouse button.\n\n- Example:\n\n ```toml\n window-management-key = \"Alt_L\"\n ```\n" diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index e9038628..5267e49d 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -2076,6 +2076,28 @@ The table has the following fields: The value of this field should be a boolean. +- `unstable-mouse-follows-focus` (optional): + + Configures whether the mouse cursor is automatically centered on the active window + when focus changes via keyboard commands. + + When enabled, the cursor will be automatically positioned to the center of the + active window when focus changes through keyboard commands such as `focus-left`, + `focus-right`, `show-workspace`, etc. + + The default is `false`. + + This option is unstable due to various issues. It is not subject to the usual + semver guarantees. + + - Example: + + ```toml + unstable-mouse-follows-focus = true + ``` + + The value of this field should be a boolean. + - `window-management-key` (optional): Configures a key that will enable window management mode while pressed. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index f4b6739a..ce380a82 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2865,6 +2865,27 @@ Config: focus to that window. The default is `true`. + unstable-mouse-follows-focus: + kind: boolean + required: false + description: | + Configures whether the mouse cursor is automatically centered on the active window + when focus changes via keyboard commands. + + When enabled, the cursor will be automatically positioned to the center of the + active window when focus changes through keyboard commands such as `focus-left`, + `focus-right`, `show-workspace`, etc. + + The default is `false`. + + This option is unstable due to various issues. It is not subject to the usual + semver guarantees. + + - Example: + + ```toml + unstable-mouse-follows-focus = true + ``` window-management-key: kind: string required: false