Compare commits

...

4 Commits

Author SHA1 Message Date
Kiko
711cad75e1
Merge branch 'YaLTeR:main' into main 2024-07-05 19:43:12 +00:00
Ivan Molodetskikh
1da99f4003 Implement focus-follows-mouse max-scroll-amount 2024-07-05 20:53:11 +04:00
Ivan Molodetskikh
120eaa6c56 wiki: Fix repeat since annotation 2024-07-05 20:30:27 +04:00
Ivan Molodetskikh
fb636ef98d Refactor and simplify new view offset calculation
* Split new offset computation from starting the animation.
* Simplify new column on empty workspace logic.
2024-07-05 20:30:27 +04:00
7 changed files with 216 additions and 122 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>);
@ -1805,6 +1814,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>,
@ -2429,6 +2448,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())))
} }
@ -2675,7 +2711,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

@ -660,8 +660,9 @@ impl<W: LayoutElement> Workspace<W> {
}); });
} }
fn compute_new_view_offset_for_column(&self, current_x: f64, idx: usize) -> f64 { fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
if self.columns[idx].is_fullscreen { let col = &self.columns[idx];
if col.is_fullscreen {
return 0.; return 0.;
} }
@ -677,7 +678,7 @@ impl<W: LayoutElement> Workspace<W> {
final_x + self.working_area.loc.x, final_x + self.working_area.loc.x,
self.working_area.size.w, self.working_area.size.w,
new_col_x, new_col_x,
self.columns[idx].width(), col.width(),
self.options.gaps, self.options.gaps,
); );
@ -685,6 +686,71 @@ impl<W: LayoutElement> Workspace<W> {
new_offset - self.working_area.loc.x new_offset - self.working_area.loc.x
} }
fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
if col.is_fullscreen {
return self.compute_new_view_offset_for_column_fit(current_x, idx);
}
let width = col.width();
// Columns wider than the view are left-aligned (the fit code can deal with that).
if self.working_area.size.w <= width {
return self.compute_new_view_offset_for_column_fit(current_x, idx);
}
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x
}
fn compute_new_view_offset_for_column(
&self,
current_x: f64,
idx: usize,
prev_idx: Option<usize>,
) -> f64 {
match self.options.center_focused_column {
CenterFocusedColumn::Always => {
self.compute_new_view_offset_for_column_centered(current_x, idx)
}
CenterFocusedColumn::OnOverflow => {
let Some(prev_idx) = prev_idx else {
return self.compute_new_view_offset_for_column_fit(current_x, idx);
};
// Always take the left or right neighbor of the target as the source.
let source_idx = if prev_idx > idx {
min(idx + 1, self.columns.len() - 1)
} else {
idx.saturating_sub(1)
};
let source_x = self.column_x(source_idx);
let source_width = self.columns[source_idx].width();
let target_x = self.column_x(idx);
let target_width = self.columns[idx].width();
let total_width = if source_x < target_x {
// Source is left from target.
target_x - source_x + target_width
} else {
// Source is right from target.
source_x - target_x + source_width
} + self.options.gaps * 2.;
// If it fits together, do a normal animation, otherwise center the new column.
if total_width <= self.working_area.size.w {
self.compute_new_view_offset_for_column_fit(current_x, idx)
} else {
self.compute_new_view_offset_for_column_centered(current_x, idx)
}
}
CenterFocusedColumn::Never => {
self.compute_new_view_offset_for_column_fit(current_x, idx)
}
}
}
fn animate_view_offset(&mut self, current_x: f64, idx: usize, new_view_offset: f64) { fn animate_view_offset(&mut self, current_x: f64, idx: usize, new_view_offset: f64) {
self.animate_view_offset_with_config( self.animate_view_offset_with_config(
current_x, current_x,
@ -728,44 +794,24 @@ impl<W: LayoutElement> Workspace<W> {
))); )));
} }
fn animate_view_offset_to_column_fit(
&mut self,
current_x: f64,
idx: usize,
config: niri_config::Animation,
) {
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx);
self.animate_view_offset_with_config(current_x, idx, new_view_offset, config);
}
fn animate_view_offset_to_column_centered( fn animate_view_offset_to_column_centered(
&mut self, &mut self,
current_x: f64, current_x: f64,
idx: usize, idx: usize,
config: niri_config::Animation, config: niri_config::Animation,
) { ) {
if self.columns.is_empty() { let new_view_offset = self.compute_new_view_offset_for_column_centered(current_x, idx);
return; self.animate_view_offset_with_config(current_x, idx, new_view_offset, config);
} }
let col = &self.columns[idx];
if col.is_fullscreen {
self.animate_view_offset_to_column_fit(current_x, idx, config);
return;
}
let width = col.width();
// If the column is wider than the working area, then on commit it will be shifted to left
// edge alignment by the usual positioning code, so there's no use in trying to center it
// here.
if self.working_area.size.w <= width {
self.animate_view_offset_to_column_fit(current_x, idx, config);
return;
}
let new_view_offset = -(self.working_area.size.w - width) / 2. - self.working_area.loc.x;
fn animate_view_offset_to_column_with_config(
&mut self,
current_x: f64,
idx: usize,
prev_idx: Option<usize>,
config: niri_config::Animation,
) {
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx, prev_idx);
self.animate_view_offset_with_config(current_x, idx, new_view_offset, config); self.animate_view_offset_with_config(current_x, idx, new_view_offset, config);
} }
@ -783,57 +829,6 @@ impl<W: LayoutElement> Workspace<W> {
) )
} }
fn animate_view_offset_to_column_with_config(
&mut self,
current_x: f64,
idx: usize,
prev_idx: Option<usize>,
config: niri_config::Animation,
) {
match self.options.center_focused_column {
CenterFocusedColumn::Always => {
self.animate_view_offset_to_column_centered(current_x, idx, config)
}
CenterFocusedColumn::OnOverflow => {
let Some(prev_idx) = prev_idx else {
self.animate_view_offset_to_column_fit(current_x, idx, config);
return;
};
// Always take the left or right neighbor of the target as the source.
let source_idx = if prev_idx > idx {
min(idx + 1, self.columns.len() - 1)
} else {
idx.saturating_sub(1)
};
let source_x = self.column_x(source_idx);
let source_width = self.columns[source_idx].width();
let target_x = self.column_x(idx);
let target_width = self.columns[idx].width();
let total_width = if source_x < target_x {
// Source is left from target.
target_x - source_x + target_width
} else {
// Source is right from target.
source_x - target_x + source_width
} + self.options.gaps * 2.;
// If it fits together, do a normal animation, otherwise center the new column.
if total_width <= self.working_area.size.w {
self.animate_view_offset_to_column_fit(current_x, idx, config);
} else {
self.animate_view_offset_to_column_centered(current_x, idx, config);
}
}
CenterFocusedColumn::Never => {
self.animate_view_offset_to_column_fit(current_x, idx, config)
}
};
}
fn activate_column(&mut self, idx: usize) { fn activate_column(&mut self, idx: usize) {
self.activate_column_with_anim_config( self.activate_column_with_anim_config(
idx, idx,
@ -913,31 +908,24 @@ impl<W: LayoutElement> Workspace<W> {
is_full_width, is_full_width,
true, true,
); );
let width = column.width();
self.data.insert(col_idx, ColumnData::new(&column)); self.data.insert(col_idx, ColumnData::new(&column));
self.columns.insert(col_idx, column); self.columns.insert(col_idx, column);
if activate { if activate {
// If this is the first window on an empty workspace, skip the animation from whatever // If this is the first window on an empty workspace, remove the effect of whatever
// view_offset was left over. // view_offset was left over and skip the animation.
if was_empty { if was_empty {
if self.options.center_focused_column == CenterFocusedColumn::Always { self.view_offset = 0.;
self.view_offset =
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x;
} else {
// Try to make the code produce a left-aligned offset, even in presence of left
// exclusive zones.
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
}
self.view_offset_adj = None; self.view_offset_adj = None;
self.view_offset =
self.compute_new_view_offset_for_column(self.view_pos(), col_idx, None);
} }
let prev_offset = (!was_empty).then(|| self.static_view_offset()); let prev_offset = (!was_empty).then(|| self.static_view_offset());
self.activate_column_with_anim_config( let anim_config =
col_idx, anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0);
anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0), self.activate_column_with_anim_config(col_idx, anim_config);
);
self.activate_prev_column_on_removal = prev_offset; self.activate_prev_column_on_removal = prev_offset;
} }
@ -1054,23 +1042,17 @@ impl<W: LayoutElement> Workspace<W> {
column.update_config(self.scale.fractional_scale(), self.options.clone()); column.update_config(self.scale.fractional_scale(), self.options.clone());
column.set_view_size(self.view_size, self.working_area); column.set_view_size(self.view_size, self.working_area);
let width = column.width();
self.data.insert(idx, ColumnData::new(&column)); self.data.insert(idx, ColumnData::new(&column));
self.columns.insert(idx, column); self.columns.insert(idx, column);
if activate { if activate {
// If this is the first window on an empty workspace, skip the animation from whatever // If this is the first window on an empty workspace, remove the effect of whatever
// view_offset was left over. // view_offset was left over and skip the animation.
if was_empty { if was_empty {
if self.options.center_focused_column == CenterFocusedColumn::Always { self.view_offset = 0.;
self.view_offset =
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x;
} else {
// Try to make the code produce a left-aligned offset, even in presence of left
// exclusive zones.
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
}
self.view_offset_adj = None; self.view_offset_adj = None;
self.view_offset =
self.compute_new_view_offset_for_column(self.view_pos(), idx, None);
} }
let prev_offset = (!was_empty).then(|| self.static_view_offset()); let prev_offset = (!was_empty).then(|| self.static_view_offset());
@ -1409,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
@ -1947,6 +1960,10 @@ impl<W: LayoutElement> Workspace<W> {
} }
pub fn center_column(&mut self) { pub fn center_column(&mut self) {
if self.columns.is_empty() {
return;
}
let center_x = self.view_pos(); let center_x = self.view_pos();
self.animate_view_offset_to_column_centered( self.animate_view_offset_to_column_centered(
center_x, center_x,
@ -1954,10 +1971,8 @@ impl<W: LayoutElement> Workspace<W> {
self.options.animations.horizontal_view_movement.0, self.options.animations.horizontal_view_movement.0,
); );
if !self.columns.is_empty() { let col = &mut self.columns[self.active_column_idx];
let col = &mut self.columns[self.active_column_idx]; cancel_resize_for_column(&mut self.interactive_resize, col);
cancel_resize_for_column(&mut self.interactive_resize, col);
}
} }
fn view_pos(&self) -> f64 { fn view_pos(&self) -> f64 {

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

View File

@ -50,7 +50,7 @@ For this reason, most of the default keys use the `Mod` modifier.
> Here, look at `sym: Left` and `sym: Right`: these are the key names. > Here, look at `sym: Left` and `sym: Right`: these are the key names.
> I was pressing the left and the right arrow in this example. > I was pressing the left and the right arrow in this example.
<sup>Since: 0.1.7</sup> Binds will repeat by default (i.e. holding down a bind will make it trigger repeatedly). <sup>Since: 0.1.8</sup> Binds will repeat by default (i.e. holding down a bind will make it trigger repeatedly).
You can disable that for specific binds with `repeat=false`: You can disable that for specific binds with `repeat=false`:
``` ```