Implement focus-follows-mouse max-scroll-amount

This commit is contained in:
Ivan Molodetskikh 2024-07-05 20:12:56 +04:00
parent 120eaa6c56
commit 1da99f4003
6 changed files with 116 additions and 6 deletions

View File

@ -75,7 +75,7 @@ pub struct Input {
#[knuffel(child)] #[knuffel(child)]
pub warp_mouse_to_focus: bool, pub warp_mouse_to_focus: bool,
#[knuffel(child)] #[knuffel(child)]
pub focus_follows_mouse: bool, pub focus_follows_mouse: Option<FocusFollowsMouse>,
#[knuffel(child)] #[knuffel(child)]
pub workspace_auto_back_and_forth: bool, pub workspace_auto_back_and_forth: bool,
} }
@ -289,6 +289,15 @@ pub struct Touch {
pub map_to_output: Option<String>, 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)] #[derive(Debug, Default, Clone, PartialEq)]
pub struct Outputs(pub Vec<Output>); pub struct Outputs(pub Vec<Output>);
@ -1794,6 +1803,16 @@ where
} }
impl Animation { 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>( fn decode_node<S: knuffel::traits::ErrorSpan>(
node: &knuffel::ast::SpannedNode<S>, node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<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> { pub fn set_miette_hook() -> Result<(), miette::InstallError> {
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))) miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())))
} }
@ -2664,7 +2700,9 @@ mod tests {
}, },
disable_power_key_handling: true, disable_power_key_handling: true,
warp_mouse_to_focus: 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, workspace_auto_back_and_forth: true,
}, },
outputs: Outputs(vec![Output { outputs: Outputs(vec![Output {

View File

@ -44,7 +44,8 @@ input {
// warp-mouse-to-focus // warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them. // 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 // You can configure outputs by their name, which you can find

View File

@ -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) { pub fn activate_window(&mut self, window: &W::Id) {
let MonitorSet::Normal { let MonitorSet::Normal {
monitors, monitors,

View File

@ -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) { pub fn activate_window(&mut self, window: &W::Id) {
let column_idx = self let column_idx = self
.columns .columns

View File

@ -4140,9 +4140,9 @@ impl Niri {
} }
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointerFocus) { 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; return;
} };
let pointer = &self.seat.get_pointer().unwrap(); let pointer = &self.seat.get_pointer().unwrap();
if pointer.is_grabbed() { if pointer.is_grabbed() {
@ -4160,6 +4160,12 @@ impl Niri {
if let Some(window) = &new_focus.window { if let Some(window) = &new_focus.window {
if current_focus.window.as_ref() != Some(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); self.layout.activate_window(window);
} }
} }

View File

@ -68,7 +68,7 @@ input {
// disable-power-key-handling // disable-power-key-handling
// warp-mouse-to-focus // warp-mouse-to-focus
// focus-follows-mouse // focus-follows-mouse max-scroll-amount="0%"
// workspace-auto-back-and-forth // 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` #### `workspace-auto-back-and-forth`
Normally, switching to the same workspace by index twice will do nothing (since you're already on that workspace). Normally, switching to the same workspace by index twice will do nothing (since you're already on that workspace).