Repository: YaLTeR/niri
Pull request #1258: Add option to warp-mouse-to-focus to always center
Author: login: TyberiusPrime
Created: 2025-03-13T14:44:54Z | Merged: 2025-03-22T08:00:44Z

For my mental model, it's important that the mouse always warps to the center of the focused window.  Niri right now only centers X or Y, but not both, depending on where the mouse was.

This PR adds an option to wrap-mouse-to-focus called 'force-center' which enables centering both, always.


Diff:
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 996286498d..db379746d7 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -95,7 +95,7 @@ pub struct Input {
     #[knuffel(child)]
     pub disable_power_key_handling: bool,
     #[knuffel(child)]
-    pub warp_mouse_to_focus: bool,
+    pub warp_mouse_to_focus: Option<WarpMouseToFocus>,
     #[knuffel(child)]
     pub focus_follows_mouse: Option<FocusFollowsMouse>,
     #[knuffel(child)]
@@ -369,6 +369,32 @@ pub struct FocusFollowsMouse {
     pub max_scroll_amount: Option<Percent>,
 }
 
+#[derive(knuffel::Decode, Debug, PartialEq, Eq, Clone, Copy)]
+pub struct WarpMouseToFocus {
+    #[knuffel(property, str)]
+    pub mode: Option<WarpMouseToFocusMode>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum WarpMouseToFocusMode {
+    CenterXy,
+    CenterXyAlways,
+}
+
+impl FromStr for WarpMouseToFocusMode {
+    type Err = miette::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "center-xy" => Ok(Self::CenterXy),
+            "center-xy-always" => Ok(Self::CenterXyAlways),
+            _ => Err(miette!(
+                r#"invalid mode for warp-mouse-to-focus, can be "center-xy" or "center-xy-always" (or leave unset for separate centering)"#
+            )),
+        }
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct Percent(pub f64);
 
@@ -4023,7 +4049,11 @@ mod tests {
                     ),
                 },
                 disable_power_key_handling: true,
-                warp_mouse_to_focus: true,
+                warp_mouse_to_focus: Some(
+                    WarpMouseToFocus {
+                        mode: None,
+                    },
+                ),
                 focus_follows_mouse: Some(
                     FocusFollowsMouse {
                         max_scroll_amount: None,
diff --git a/src/niri.rs b/src/niri.rs
index ac8b90a38b..30042fc94b 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -14,8 +14,8 @@ use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as
 use anyhow::{bail, ensure, Context};
 use calloop::futures::Scheduler;
 use niri_config::{
-    Config, FloatOrInt, Key, Modifiers, OutputName, PreviewRender, TrackLayout, WorkspaceReference,
-    DEFAULT_BACKGROUND_COLOR,
+    Config, FloatOrInt, Key, Modifiers, OutputName, PreviewRender, TrackLayout,
+    WarpMouseToFocusMode, WorkspaceReference, DEFAULT_BACKGROUND_COLOR,
 };
 use smithay::backend::allocator::Fourcc;
 use smithay::backend::input::Keycode;
@@ -510,6 +510,8 @@ struct SurfaceFrameThrottlingState {
 pub enum CenterCoords {
     Separately,
     Both,
+    // Force centering even if the cursor is already in the rectangle.
+    BothAlways,
 }
 
 #[derive(Clone, PartialEq, Eq)]
@@ -753,6 +755,7 @@ impl State {
                     center_f64(rect)
                 }
             }
+            CenterCoords::BothAlways => center_f64(rect),
         };
 
         self.move_cursor(p);
@@ -808,19 +811,27 @@ impl State {
     }
 
     pub fn maybe_warp_cursor_to_focus(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
-            return false;
-        }
-
-        self.move_cursor_to_focused_tile(CenterCoords::Separately)
+        let focused = match self.niri.config.borrow().input.warp_mouse_to_focus {
+            None => return false,
+            Some(inner) => match inner.mode {
+                None => CenterCoords::Separately,
+                Some(WarpMouseToFocusMode::CenterXy) => CenterCoords::Both,
+                Some(WarpMouseToFocusMode::CenterXyAlways) => CenterCoords::BothAlways,
+            },
+        };
+        self.move_cursor_to_focused_tile(focused)
     }
 
     pub fn maybe_warp_cursor_to_focus_centered(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
-            return false;
-        }
-
-        self.move_cursor_to_focused_tile(CenterCoords::Both)
+        let focused = match self.niri.config.borrow().input.warp_mouse_to_focus {
+            None => return false,
+            Some(inner) => match inner.mode {
+                None => CenterCoords::Both,
+                Some(WarpMouseToFocusMode::CenterXy) => CenterCoords::Both,
+                Some(WarpMouseToFocusMode::CenterXyAlways) => CenterCoords::BothAlways,
+            },
+        };
+        self.move_cursor_to_focused_tile(focused)
     }
 
     pub fn refresh_pointer_contents(&mut self) {
diff --git a/wiki/Configuration:-Input.md b/wiki/Configuration:-Input.md
index 9245583b4e..4a4c62ecab 100644
--- a/wiki/Configuration:-Input.md
+++ b/wiki/Configuration:-Input.md
@@ -243,7 +243,7 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+Does not make the cursor visible if it had been hidden.
 
 ```kdl
 input {
@@ -251,6 +251,21 @@ input {
 }
 ```
 
+By default, the cursor warps *separately* horizontally and vertically.
+I.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then the mouse will move only horizontally, and not vertically.
+
+<sup>Since: next release</sup> You can customize this with the `mode` property.
+
+- `mode="center-xy"`: warps by both X and Y coordinates together.
+So if the mouse was anywhere outside the newly focused window, it will warp to the center of the window.
+- `mode="center-xy-always"`: warps by both X and Y coordinates together, even if the mouse was already somewhere inside the newly focused window.
+
+```kdl
+input {
+    warp-mouse-to-focus mode="center-xy"
+}
+```
+
 #### `focus-follows-mouse`
 
 Focuses windows and outputs automatically when moving the mouse over them.


Reviews:
user: login: YaLTeR
body: Some name bikeshedding. This property does two separate things:
1. Center instead of separate coordinate
2. Always warp even if already within rectangle

Do you need/intended to do both of these things or just 1? If not, then let's leave just 1 and maybe rephrase the property name a little. If yes, then I am somewhat thinking that maybe it should be two properties, or at least some clear future path in case people start asking for 1 without 2.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:54:43Z
user: login: TyberiusPrime
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:06:34Z
user: login: YaLTeR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T15:10:38Z
user: login: YaLTeR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T19:04:56Z
user: login: TyberiusPrime
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T09:08:56Z
user: login: YaLTeR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T15:49:17Z
user: login: bbb651
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T21:13:57Z
user: login: YaLTeR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T06:02:14Z
user: login: YaLTeR
body: Did some cosmetic changes, now good to go.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-22T07:51:49Z

Review comments:
diff_hunk: @@ -791,15 +793,29 @@ impl State {
     }
 
     pub fn maybe_warp_cursor_to_focus(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
-            return false;
-        }
+        let focused = match self.niri.config.borrow().input.warp_mouse_to_focus {
+            Some(warp_config) => {
+                if warp_config.force_center {
+                    CenterCoords::BothForce
+                } else {
+                    CenterCoords::Separately
+                }
+            }
+            None => return false,
+        };
 
-        self.move_cursor_to_focused_tile(CenterCoords::Separately)
+        self.move_cursor_to_focused_tile(focused)
     }
 
     pub fn maybe_warp_cursor_to_focus_centered(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
+        if self
+            .niri
+            .config
+            .borrow()
+            .input
+            .warp_mouse_to_focus
+            .is_none()
path: src/niri.rs
user: login: YaLTeR
body: Shouldn't this also use BothForce? Since as you say you want it to work even if it's within the rectangle already?
created_at: 2025-03-13T14:51:04Z
author_association: MEMBER
diff_hunk: @@ -240,11 +240,13 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+By default, X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally. 
+If force-center is set, the mouse will always warp to the center of the focused window. 
 
 ```kdl
 input {
     warp-mouse-to-focus
+    // or warp-mouse-to-focus { force-center; }
path: wiki/Configuration:-Input.md
user: login: YaLTeR
body: Instead of this, it should be a knuffel property, so `warp-mouse-to-focus force-center=true`
created_at: 2025-03-13T14:51:47Z
author_association: MEMBER
diff_hunk: @@ -791,15 +793,29 @@ impl State {
     }
 
     pub fn maybe_warp_cursor_to_focus(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
-            return false;
-        }
+        let focused = match self.niri.config.borrow().input.warp_mouse_to_focus {
+            Some(warp_config) => {
+                if warp_config.force_center {
+                    CenterCoords::BothForce
+                } else {
+                    CenterCoords::Separately
+                }
+            }
+            None => return false,
+        };
 
-        self.move_cursor_to_focused_tile(CenterCoords::Separately)
+        self.move_cursor_to_focused_tile(focused)
     }
 
     pub fn maybe_warp_cursor_to_focus_centered(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
+        if self
+            .niri
+            .config
+            .borrow()
+            .input
+            .warp_mouse_to_focus
+            .is_none()
path: src/niri.rs
user: login: TyberiusPrime
body: That's the function that get's called when you're focusing a window on a different output, isn't it? I figured that the cursor would then always be outside of the rect, but I might be biased because I subconsciously assumed 'focus-follows-mouse' as well.
created_at: 2025-03-13T15:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -791,15 +793,29 @@ impl State {
     }
 
     pub fn maybe_warp_cursor_to_focus(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
-            return false;
-        }
+        let focused = match self.niri.config.borrow().input.warp_mouse_to_focus {
+            Some(warp_config) => {
+                if warp_config.force_center {
+                    CenterCoords::BothForce
+                } else {
+                    CenterCoords::Separately
+                }
+            }
+            None => return false,
+        };
 
-        self.move_cursor_to_focused_tile(CenterCoords::Separately)
+        self.move_cursor_to_focused_tile(focused)
     }
 
     pub fn maybe_warp_cursor_to_focus_centered(&mut self) -> bool {
-        if !self.niri.config.borrow().input.warp_mouse_to_focus {
+        if self
+            .niri
+            .config
+            .borrow()
+            .input
+            .warp_mouse_to_focus
+            .is_none()
path: src/niri.rs
user: login: YaLTeR
body: I mean, there's no need to complicate the logic by doing it subtly differently in different places when it doesn't matter in the end.
created_at: 2025-03-13T15:10:37Z
author_association: MEMBER
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
path: wiki/Configuration:-Input.md
user: login: YaLTeR
body: Writing `warp-mouse-to-focus` by itself should be equivalent to the current behavior ("separately"). We don't break existing configs.

You can do this by making it a `warp_mouse_to_focus: Option<WarpMouseToFocus>` and removing the Off variant.
created_at: 2025-03-14T18:56:52Z
author_association: MEMBER
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
+ - "separately": warp X and Y coordinates to the window's center separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally and vice versa. Only warps if the mouse was outside of the newly focused window.
+ - "both":  warp both X and Y coordinates to the window's center. Only warps if the mouse was outside of the newly focused window.
+ - "both-always": warp both X and Y coordinates to the window's center. Always warps, even if the mouse was already inside the newly focused window.
path: wiki/Configuration:-Input.md
user: login: YaLTeR
body: I was trying to come up with better names because "both" is kinda not obvious what it means just looking at the config, but I can't think of anything else rn
created_at: 2025-03-14T19:03:08Z
author_association: MEMBER
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
path: wiki/Configuration:-Input.md
user: login: YaLTeR
body: Also remove the "separately" variant (since it's the default). Which would mean either `Option<Option<WarpMouseToFocus>>` (idk if knuffel will understand this), or `Option<WarpMouseToFocus>` where `WarpMouseToFocus` itself contains one `Option<>` field.
created_at: 2025-03-14T19:04:48Z
author_association: MEMBER
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
+ - "separately": warp X and Y coordinates to the window's center separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally and vice versa. Only warps if the mouse was outside of the newly focused window.
+ - "both":  warp both X and Y coordinates to the window's center. Only warps if the mouse was outside of the newly focused window.
+ - "both-always": warp both X and Y coordinates to the window's center. Always warps, even if the mouse was already inside the newly focused window.
path: wiki/Configuration:-Input.md
user: login: TyberiusPrime
body: Maybe x-or-y, x-and-y, x-and-y-always?
created_at: 2025-03-15T09:08:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
+ - "separately": warp X and Y coordinates to the window's center separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally and vice versa. Only warps if the mouse was outside of the newly focused window.
+ - "both":  warp both X and Y coordinates to the window's center. Only warps if the mouse was outside of the newly focused window.
+ - "both-always": warp both X and Y coordinates to the window's center. Always warps, even if the mouse was already inside the newly focused window.
path: wiki/Configuration:-Input.md
user: login: YaLTeR
body: How about: `xy` and `xy-always`
created_at: 2025-03-15T15:49:17Z
author_association: MEMBER
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
+ - "separately": warp X and Y coordinates to the window's center separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally and vice versa. Only warps if the mouse was outside of the newly focused window.
+ - "both":  warp both X and Y coordinates to the window's center. Only warps if the mouse was outside of the newly focused window.
+ - "both-always": warp both X and Y coordinates to the window's center. Always warps, even if the mouse was already inside the newly focused window.
path: wiki/Configuration:-Input.md
user: login: bbb651
body: `never` is more consistent than `off` because of `center-focused-column "never"` (there is a lot more `off`s but as node names not as arguments), and reads better with never/always.
I think `both-always` could just be `always`, then it reads `warp-mouse-to-focus=always`? I expect warping to be to the center, so to me `separately` is the exception and it makes `both-always` more confusing.
And maybe `separate` > `separately`?
created_at: 2025-03-15T21:13:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,11 +240,14 @@ input {
 
 Makes the mouse warp to newly focused windows.
 
-X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
+ - "off": do not warp (default)
+ - "separately": warp X and Y coordinates to the window's center separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally and vice versa. Only warps if the mouse was outside of the newly focused window.
+ - "both":  warp both X and Y coordinates to the window's center. Only warps if the mouse was outside of the newly focused window.
+ - "both-always": warp both X and Y coordinates to the window's center. Always warps, even if the mouse was already inside the newly focused window.
path: wiki/Configuration:-Input.md
user: login: YaLTeR
body: There is not supposed to be any "off". "off" is just omitting the line entirely from the config. There's also not supposed to be "separately" because just writing the line by itself is already "separately". So the question is about "both" and "both-always".
created_at: 2025-03-16T06:02:14Z
author_association: MEMBER

PR comments:
user: login: TyberiusPrime
created_at: 2025-03-13T15:39:35Z
body: > Some name bikeshedding. This property does two separate things:
> 
>     1. Center instead of separate coordinate
> 
>     2. Always warp even if already within rectangle
> 
Hm, I believe it's a bit of a tri-state.

1. if you're doing them separately, force has no effect (which would it change, X or Y?)
2. Both, but only if outside of the window before ('unforced')
3. Both, but always center on focus change, independent of where the mouse was.

Right now this PR only does 1 & 3.

How should we present this to the user?

author_association: CONTRIBUTOR
user: login: YaLTeR
created_at: 2025-03-13T17:43:34Z
body: Do you need the mouse to warp even if it's already within the target window?
author_association: MEMBER
user: login: TyberiusPrime
created_at: 2025-03-14T07:19:39Z
body: > Do you need the mouse to warp even if it's already within the target window?

Yes, that's what I want, my brain is weird :).

I changed it to work like CenterFocusedColumn, 
having four states.

- off  (default)
- separately (current behavior if enable)
- both 
- both-always.
author_association: CONTRIBUTOR
user: login: TyberiusPrime
created_at: 2025-03-19T07:41:58Z
body: Once more, with passion.

- leave off -> off.
- `wrap-mouse-to-focus` -> previous behaviour, x-or-y
- `wrap-mouse-to-focus mode = "center-xy"` -> center x and y, if outside the window
- `wrap-mouse-to-focus mode = "center-xy-always"` -> center x and y, no matter what.

I went with 'center-xy', so we'd have 'room' if anybody ever wants to do non-center-behavior ('top-left'/'bottom-right' maybe?).

author_association: CONTRIBUTOR
user: login: YaLTeR
created_at: 2025-03-22T08:04:06Z
body: Thanks
author_association: MEMBER