mirror of
https://github.com/YaLTeR/niri.git
synced 2024-08-17 16:20:23 +03:00
Merge branch 'YaLTeR:main' into main
This commit is contained in:
commit
711cad75e1
@ -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>);
|
||||
|
||||
@ -1805,6 +1814,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>,
|
||||
@ -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> {
|
||||
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())))
|
||||
}
|
||||
@ -2675,7 +2711,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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -660,8 +660,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
});
|
||||
}
|
||||
|
||||
fn compute_new_view_offset_for_column(&self, current_x: f64, idx: usize) -> f64 {
|
||||
if self.columns[idx].is_fullscreen {
|
||||
fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
|
||||
let col = &self.columns[idx];
|
||||
if col.is_fullscreen {
|
||||
return 0.;
|
||||
}
|
||||
|
||||
@ -677,7 +678,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
final_x + self.working_area.loc.x,
|
||||
self.working_area.size.w,
|
||||
new_col_x,
|
||||
self.columns[idx].width(),
|
||||
col.width(),
|
||||
self.options.gaps,
|
||||
);
|
||||
|
||||
@ -685,6 +686,71 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
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) {
|
||||
self.animate_view_offset_with_config(
|
||||
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(
|
||||
&mut self,
|
||||
current_x: f64,
|
||||
idx: usize,
|
||||
config: niri_config::Animation,
|
||||
) {
|
||||
if self.columns.is_empty() {
|
||||
return;
|
||||
let new_view_offset = self.compute_new_view_offset_for_column_centered(current_x, idx);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
self.activate_column_with_anim_config(
|
||||
idx,
|
||||
@ -913,31 +908,24 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
is_full_width,
|
||||
true,
|
||||
);
|
||||
let width = column.width();
|
||||
self.data.insert(col_idx, ColumnData::new(&column));
|
||||
self.columns.insert(col_idx, column);
|
||||
|
||||
if activate {
|
||||
// If this is the first window on an empty workspace, skip the animation from whatever
|
||||
// view_offset was left over.
|
||||
// If this is the first window on an empty workspace, remove the effect of whatever
|
||||
// view_offset was left over and skip the animation.
|
||||
if was_empty {
|
||||
if self.options.center_focused_column == CenterFocusedColumn::Always {
|
||||
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 = 0.;
|
||||
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());
|
||||
|
||||
self.activate_column_with_anim_config(
|
||||
col_idx,
|
||||
anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0),
|
||||
);
|
||||
let anim_config =
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1054,23 +1042,17 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
column.update_config(self.scale.fractional_scale(), self.options.clone());
|
||||
column.set_view_size(self.view_size, self.working_area);
|
||||
let width = column.width();
|
||||
self.data.insert(idx, ColumnData::new(&column));
|
||||
self.columns.insert(idx, column);
|
||||
|
||||
if activate {
|
||||
// If this is the first window on an empty workspace, skip the animation from whatever
|
||||
// view_offset was left over.
|
||||
// If this is the first window on an empty workspace, remove the effect of whatever
|
||||
// view_offset was left over and skip the animation.
|
||||
if was_empty {
|
||||
if self.options.center_focused_column == CenterFocusedColumn::Always {
|
||||
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 = 0.;
|
||||
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());
|
||||
@ -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) {
|
||||
let column_idx = self
|
||||
.columns
|
||||
@ -1947,6 +1960,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
|
||||
pub fn center_column(&mut self) {
|
||||
if self.columns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let center_x = self.view_pos();
|
||||
self.animate_view_offset_to_column_centered(
|
||||
center_x,
|
||||
@ -1954,11 +1971,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.options.animations.horizontal_view_movement.0,
|
||||
);
|
||||
|
||||
if !self.columns.is_empty() {
|
||||
let col = &mut self.columns[self.active_column_idx];
|
||||
cancel_resize_for_column(&mut self.interactive_resize, col);
|
||||
}
|
||||
}
|
||||
|
||||
fn view_pos(&self) -> f64 {
|
||||
self.column_x(self.active_column_idx) + self.view_offset
|
||||
|
10
src/niri.rs
10
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);
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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.
|
||||
> 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`:
|
||||
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user