Merge branch 'YaLTeR:main' into main

This commit is contained in:
Kiko 2024-07-05 19:43:12 +00:00 committed by GitHub
commit 711cad75e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 216 additions and 122 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>);
@ -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 {

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

@ -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 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;
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);
}
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,10 +1971,8 @@ 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);
}
let col = &mut self.columns[self.active_column_idx];
cancel_resize_for_column(&mut self.interactive_resize, col);
}
fn view_pos(&self) -> f64 {

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

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.
> 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`:
```