aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--niri-config/src/lib.rs42
-rw-r--r--resources/default-config.kdl3
-rw-r--r--src/layout/mod.rs16
-rw-r--r--src/layout/workspace.rs31
-rw-r--r--src/niri.rs10
-rw-r--r--wiki/Configuration:-Input.md20
6 files changed, 116 insertions, 6 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index aaf81bc0..2946b8c8 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -75,7 +75,7 @@ pub struct Input {
#[knuffel(child)]
pub warp_mouse_to_focus: bool,
#[knuffel(child)]
- pub focus_follows_mouse: bool,
+ pub focus_follows_mouse: Option<FocusFollowsMouse>,
#[knuffel(child)]
pub workspace_auto_back_and_forth: bool,
}
@@ -289,6 +289,15 @@ pub struct Touch {
pub map_to_output: Option<String>,
}
+#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
+pub struct FocusFollowsMouse {
+ #[knuffel(property, str)]
+ pub max_scroll_amount: Option<Percent>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Percent(pub f64);
+
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Outputs(pub Vec<Output>);
@@ -1794,6 +1803,16 @@ where
}
impl Animation {
+ pub fn new_off() -> Self {
+ Self {
+ off: true,
+ kind: AnimationKind::Easing(EasingParams {
+ duration_ms: 0,
+ curve: AnimationCurve::Linear,
+ }),
+ }
+ }
+
fn decode_node<S: knuffel::traits::ErrorSpan>(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
@@ -2418,6 +2437,23 @@ impl FromStr for TapButtonMap {
}
}
+impl FromStr for Percent {
+ type Err = miette::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let Some((value, empty)) = s.split_once('%') else {
+ return Err(miette!("value must end with '%'"));
+ };
+
+ if !empty.is_empty() {
+ return Err(miette!("trailing characters after '%' are not allowed"));
+ }
+
+ let value: f64 = value.parse().map_err(|_| miette!("error parsing value"))?;
+ Ok(Percent(value / 100.))
+ }
+}
+
pub fn set_miette_hook() -> Result<(), miette::InstallError> {
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())))
}
@@ -2664,7 +2700,9 @@ mod tests {
},
disable_power_key_handling: true,
warp_mouse_to_focus: true,
- focus_follows_mouse: true,
+ focus_follows_mouse: Some(FocusFollowsMouse {
+ max_scroll_amount: None,
+ }),
workspace_auto_back_and_forth: true,
},
outputs: Outputs(vec![Output {
diff --git a/resources/default-config.kdl b/resources/default-config.kdl
index 469517da..1d8ebb67 100644
--- a/resources/default-config.kdl
+++ b/resources/default-config.kdl
@@ -44,7 +44,8 @@ input {
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
- // focus-follows-mouse
+ // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
+ // focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index ab7d7ec6..fadde1fb 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -947,6 +947,22 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ pub fn scroll_amount_to_activate(&self, window: &W::Id) -> f64 {
+ let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
+ return 0.;
+ };
+
+ for mon in monitors {
+ for ws in &mon.workspaces {
+ if ws.has_window(window) {
+ return ws.scroll_amount_to_activate(window);
+ }
+ }
+ }
+
+ 0.
+ }
+
pub fn activate_window(&mut self, window: &W::Id) {
let MonitorSet::Normal {
monitors,
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index c5ddfd21..625f4d5c 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -1391,6 +1391,37 @@ impl<W: LayoutElement> Workspace<W> {
}
}
+ pub fn scroll_amount_to_activate(&self, window: &W::Id) -> f64 {
+ let column_idx = self
+ .columns
+ .iter()
+ .position(|col| col.contains(window))
+ .unwrap();
+
+ if self.active_column_idx == column_idx {
+ return 0.;
+ }
+
+ let current_x = self.view_pos();
+ let new_view_offset = self.compute_new_view_offset_for_column(
+ current_x,
+ column_idx,
+ Some(self.active_column_idx),
+ );
+
+ // Consider the end of an ongoing animation because that's what compute to fit does too.
+ let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj {
+ current_x - self.view_offset + anim.to()
+ } else {
+ current_x
+ };
+
+ let new_col_x = self.column_x(column_idx);
+ let from_view_offset = final_x - new_col_x;
+
+ (from_view_offset - new_view_offset).abs() / self.working_area.size.w
+ }
+
pub fn activate_window(&mut self, window: &W::Id) {
let column_idx = self
.columns
diff --git a/src/niri.rs b/src/niri.rs
index 08331bef..2ff2f014 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -4140,9 +4140,9 @@ impl Niri {
}
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointerFocus) {
- if !self.config.borrow().input.focus_follows_mouse {
+ let Some(ffm) = self.config.borrow().input.focus_follows_mouse else {
return;
- }
+ };
let pointer = &self.seat.get_pointer().unwrap();
if pointer.is_grabbed() {
@@ -4160,6 +4160,12 @@ impl Niri {
if let Some(window) = &new_focus.window {
if current_focus.window.as_ref() != Some(window) {
+ if let Some(threshold) = ffm.max_scroll_amount {
+ if self.layout.scroll_amount_to_activate(window) > threshold.0 {
+ return;
+ }
+ }
+
self.layout.activate_window(window);
}
}
diff --git a/wiki/Configuration:-Input.md b/wiki/Configuration:-Input.md
index 05c6ce9b..fd4d0d68 100644
--- a/wiki/Configuration:-Input.md
+++ b/wiki/Configuration:-Input.md
@@ -68,7 +68,7 @@ input {
// disable-power-key-handling
// warp-mouse-to-focus
- // focus-follows-mouse
+ // focus-follows-mouse max-scroll-amount="0%"
// workspace-auto-back-and-forth
}
```
@@ -207,6 +207,24 @@ input {
}
```
+<sup>Since: 0.1.8</sup> You can optionally set `max-scroll-amount`.
+Then, focus-follows-mouse won't focus a window if it will result in the view scrolling more than the set amount.
+The value is a percentage of the working area width.
+
+```
+input {
+ // Allow focus-follows-mouse when it results in scrolling at most 10% of the screen.
+ focus-follows-mouse max-scroll-amount="10%"
+}
+```
+
+```
+input {
+ // Allow focus-follows-mouse only when it will not scroll the view.
+ focus-follows-mouse max-scroll-amount="0%"
+}
+```
+
#### `workspace-auto-back-and-forth`
Normally, switching to the same workspace by index twice will do nothing (since you're already on that workspace).