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)]
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 {

View File

@ -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

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) {
let MonitorSet::Normal {
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) {
let column_idx = self
.columns

View File

@ -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);
}
}

View File

@ -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).