diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-29 08:56:20 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-29 09:51:49 +0400 |
| commit | ba10bab010564ce68e743dd66d20cadd9e8c3c89 (patch) | |
| tree | dc252f2f1d4b88884e4c5f00c25a59be1b9833d1 /src/layout | |
| parent | 55038b7c07e9a7c08fca39793c5c4602e7099a0c (diff) | |
| download | niri-ba10bab010564ce68e743dd66d20cadd9e8c3c89.tar.gz niri-ba10bab010564ce68e743dd66d20cadd9e8c3c89.tar.bz2 niri-ba10bab010564ce68e743dd66d20cadd9e8c3c89.zip | |
Implement horizontal touchpad swipe
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/mod.rs | 99 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 211 |
2 files changed, 283 insertions, 27 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs index ba8663a1..6b32aafa 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1675,6 +1675,63 @@ impl<W: LayoutElement> Layout<W> { None } + pub fn view_offset_gesture_begin(&mut self, output: &Output) { + let monitors = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors, + MonitorSet::NoOutputs { .. } => unreachable!(), + }; + + for monitor in monitors { + for (idx, ws) in monitor.workspaces.iter_mut().enumerate() { + // Cancel the gesture on other workspaces. + if &monitor.output != output || idx != monitor.active_workspace_idx { + ws.view_offset_gesture_end(true); + continue; + } + + ws.view_offset_gesture_begin(); + } + } + } + + pub fn view_offset_gesture_update(&mut self, delta_x: f64) -> Option<Option<Output>> { + let monitors = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors, + MonitorSet::NoOutputs { .. } => return None, + }; + + for monitor in monitors { + for ws in &mut monitor.workspaces { + if let Some(refresh) = ws.view_offset_gesture_update(delta_x) { + if refresh { + return Some(Some(monitor.output.clone())); + } else { + return Some(None); + } + } + } + } + + None + } + + pub fn view_offset_gesture_end(&mut self, cancelled: bool) -> Option<Output> { + let monitors = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors, + MonitorSet::NoOutputs { .. } => return None, + }; + + for monitor in monitors { + for ws in &mut monitor.workspaces { + if ws.view_offset_gesture_end(cancelled) { + return Some(monitor.output.clone()); + } + } + } + + None + } + pub fn move_workspace_down(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -1724,25 +1781,31 @@ impl<W: LayoutElement> Layout<W> { } impl Layout<Window> { - pub fn refresh(&self) { + pub fn refresh(&mut self) { let _span = tracy_client::span!("MonitorSet::refresh"); - match &self.monitor_set { + match &mut self.monitor_set { MonitorSet::Normal { monitors, active_monitor_idx, .. } => { - for (idx, mon) in monitors.iter().enumerate() { + for (idx, mon) in monitors.iter_mut().enumerate() { let is_active = idx == *active_monitor_idx; - for ws in &mon.workspaces { + for (ws_idx, ws) in mon.workspaces.iter_mut().enumerate() { ws.refresh(is_active); + + // Cancel the view offset gesture after workspace switches, moves, etc. + if ws_idx != mon.active_workspace_idx { + ws.view_offset_gesture_end(false); + } } } } MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { ws.refresh(false); + ws.view_offset_gesture_end(false); } } } @@ -1931,6 +1994,10 @@ mod tests { }) } + fn arbitrary_view_offset_gesture_delta() -> impl Strategy<Value = f64> { + prop_oneof![(-10f64..10f64), (-50000f64..50000f64),] + } + #[derive(Debug, Clone, Copy, Arbitrary)] enum Op { AddOutput(#[proptest(strategy = "1..=5usize")] usize), @@ -1996,6 +2063,15 @@ mod tests { SetWindowHeight(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), Communicate(#[proptest(strategy = "1..=5usize")] usize), MoveWorkspaceToOutput(#[proptest(strategy = "1..=5u8")] u8), + ViewOffsetGestureBegin { + #[proptest(strategy = "1..=5usize")] + output_idx: usize, + }, + ViewOffsetGestureUpdate { + #[proptest(strategy = "arbitrary_view_offset_gesture_delta()")] + delta: f64, + }, + ViewOffsetGestureEnd, } impl Op { @@ -2231,6 +2307,21 @@ mod tests { layout.move_workspace_to_output(&output); } + Op::ViewOffsetGestureBegin { output_idx: id } => { + let name = format!("output{id}"); + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { + return; + }; + + layout.view_offset_gesture_begin(&output); + } + Op::ViewOffsetGestureUpdate { delta } => { + layout.view_offset_gesture_update(delta); + } + Op::ViewOffsetGestureEnd => { + // We don't handle cancels in this gesture. + layout.view_offset_gesture_end(false); + } } } } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 2b0b0823..34867755 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -55,8 +55,8 @@ pub struct Workspace<W: LayoutElement> { /// for natural handling of fullscreen windows, which must ignore work area padding. view_offset: i32, - /// Animation of the view offset, if one is currently ongoing. - view_offset_anim: Option<Animation>, + /// Adjustment of the view offset, if one is currently ongoing. + view_offset_adj: Option<ViewOffsetAdjustment>, /// Whether to activate the previous, rather than the next, column upon column removal. /// @@ -81,6 +81,17 @@ niri_render_elements! { } } +#[derive(Debug)] +enum ViewOffsetAdjustment { + Animation(Animation), + Gesture(ViewGesture), +} + +#[derive(Debug)] +struct ViewGesture { + current_view_offset: f64, +} + /// Width of a column. #[derive(Debug, Clone, Copy, PartialEq)] pub enum ColumnWidth { @@ -160,6 +171,15 @@ impl OutputId { } } +impl ViewOffsetAdjustment { + pub fn current_view_offset(&self) -> f64 { + match self { + ViewOffsetAdjustment::Animation(anim) => anim.value(), + ViewOffsetAdjustment::Gesture(gesture) => gesture.current_view_offset, + } + } +} + impl ColumnWidth { fn resolve(self, options: &Options, view_width: i32) -> i32 { match self { @@ -192,7 +212,7 @@ impl<W: LayoutElement> Workspace<W> { columns: vec![], active_column_idx: 0, view_offset: 0, - view_offset_anim: None, + view_offset_adj: None, activate_prev_column_on_removal: false, options, } @@ -207,22 +227,21 @@ impl<W: LayoutElement> Workspace<W> { columns: vec![], active_column_idx: 0, view_offset: 0, - view_offset_anim: None, + view_offset_adj: None, activate_prev_column_on_removal: false, options, } } pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) { - match &mut self.view_offset_anim { - Some(anim) => { - anim.set_current_time(current_time); - self.view_offset = anim.value().round() as i32; - if anim.is_done() { - self.view_offset_anim = None; - } + if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { + anim.set_current_time(current_time); + self.view_offset = anim.value().round() as i32; + if anim.is_done() { + self.view_offset_adj = None; } - None => (), + } else if let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj { + self.view_offset = gesture.current_view_offset.round() as i32; } for (col_idx, col) in self.columns.iter_mut().enumerate() { @@ -232,7 +251,7 @@ impl<W: LayoutElement> Workspace<W> { } pub fn are_animations_ongoing(&self) -> bool { - self.view_offset_anim.is_some() || self.columns.iter().any(Column::are_animations_ongoing) + self.view_offset_adj.is_some() || self.columns.iter().any(Column::are_animations_ongoing) } pub fn update_config(&mut self, options: Rc<Options>) { @@ -382,8 +401,8 @@ impl<W: LayoutElement> Workspace<W> { let new_col_x = self.column_x(idx); - let final_x = if let Some(anim) = &self.view_offset_anim { - current_x - self.view_offset + anim.to().round() as i32 + let final_x = if let Some(adj) = &self.view_offset_adj { + current_x - self.view_offset + adj.current_view_offset().round() as i32 } else { current_x }; @@ -406,7 +425,7 @@ impl<W: LayoutElement> Workspace<W> { self.view_offset = from_view_offset; // If we're already animating towards that, don't restart it. - if let Some(anim) = &self.view_offset_anim { + if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj { if anim.value().round() as i32 == self.view_offset && anim.to().round() as i32 == new_view_offset { @@ -416,16 +435,16 @@ impl<W: LayoutElement> Workspace<W> { // If our view offset is already this, we don't need to do anything. if self.view_offset == new_view_offset { - self.view_offset_anim = None; + self.view_offset_adj = None; return; } - self.view_offset_anim = Some(Animation::new( + self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( self.view_offset as f64, new_view_offset as f64, self.options.animations.horizontal_view_movement, niri_config::Animation::default_horizontal_view_movement(), - )); + ))); } fn animate_view_offset_to_column_fit(&mut self, current_x: i32, idx: usize) { @@ -594,7 +613,7 @@ impl<W: LayoutElement> Workspace<W> { // exclusive zones. self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0); } - self.view_offset_anim = None; + self.view_offset_adj = None; } self.activate_column(idx); @@ -666,7 +685,7 @@ impl<W: LayoutElement> Workspace<W> { // exclusive zones. self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0); } - self.view_offset_anim = None; + self.view_offset_adj = None; } self.activate_column(idx); @@ -776,7 +795,9 @@ impl<W: LayoutElement> Workspace<W> { column.update_window(window); column.update_tile_sizes(); - if idx == self.active_column_idx { + if idx == self.active_column_idx + && !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_))) + { // We might need to move the view to ensure the resized window is still visible. let current_x = self.view_pos(); @@ -1171,7 +1192,7 @@ impl<W: LayoutElement> Workspace<W> { return false; } - if self.view_offset_anim.is_some() { + if self.view_offset_adj.is_some() { return false; } @@ -1210,6 +1231,150 @@ impl<W: LayoutElement> Workspace<W> { rv } + + pub fn view_offset_gesture_begin(&mut self) { + if self.columns.is_empty() { + return; + } + + let gesture = ViewGesture { + current_view_offset: self.view_offset as f64, + }; + self.view_offset_adj = Some(ViewOffsetAdjustment::Gesture(gesture)); + } + + pub fn view_offset_gesture_update(&mut self, delta_x: f64) -> Option<bool> { + let Some(ViewOffsetAdjustment::Gesture(gesture)) = &mut self.view_offset_adj else { + return None; + }; + + let mut new_offset = gesture.current_view_offset + delta_x; + gesture.current_view_offset = new_offset; + + if self.columns.is_empty() { + return Some(true); + } + + // Switch the next window to be active, if necessary. + // + // The logic here is similar to PaperWM. The idea is: make the next window (in the + // direction of the gesture) active when it becomes "more visible" than the current active + // window. + + // Make an iterator over column indices into the gesture direction. + let mut idxs_before = (0..=self.active_column_idx).rev(); + let mut idxs_after = self.active_column_idx..self.columns.len(); + let next_column_idxs = if delta_x < 0. { + &mut idxs_before as &mut dyn Iterator<Item = usize> + } else { + &mut idxs_after as &mut dyn Iterator<Item = usize> + }; + + let mut last = None; + for col_idx in next_column_idxs { + let col = &self.columns[col_idx]; + let col_x = self.column_x(col_idx); + let col_w = col.width(); + + let mut area_for_col = if col.is_fullscreen { + Rectangle::from_loc_and_size((0, 0), self.view_size) + } else { + self.working_area + }; + + if let Some((last_col_x, _)) = last { + area_for_col.loc.x += last_col_x + new_offset.round() as i32; + } else { + // First iteration of the loop; col_idx == self.active_column_idx. + area_for_col.loc.x += col_x + new_offset.round() as i32; + } + + // Check if the column is fully visible. + if area_for_col.loc.x <= col_x + && col_x + col_w <= area_for_col.loc.x + area_for_col.size.w + { + // Make it the new active column. + if let Some((last_col_x, _)) = last { + new_offset += (last_col_x - col_x) as f64; + self.active_column_idx = col_idx; + } + break; + } + + // Check if the column is already past the working area. + if (delta_x >= 0. && area_for_col.loc.x + area_for_col.size.w <= col_x) + || (delta_x < 0. && col_x + col_w <= area_for_col.loc.x) + { + break; + } + + // Compute the visible width (inside the working area). + let visible_width = col_w + - max(0, area_for_col.loc.x - col_x) + - max( + 0, + (col_x + col_w) - (area_for_col.loc.x + area_for_col.size.w), + ); + let visible_ratio = if col_w == 0 { + 1. + } else { + visible_width as f64 / col_w as f64 + }; + + if let Some((last_col_x, last_ratio)) = last { + // Check if we reached the first visible window. + if area_for_col.loc.x < col_x + col_w + && col_x < area_for_col.loc.x + area_for_col.size.w + { + // If it's more visible than the last one, make it active. + if visible_ratio >= last_ratio { + new_offset += (last_col_x - col_x) as f64; + self.active_column_idx = col_idx; + } + + break; + } + + // Still working through invisible windows. + new_offset += (last_col_x - col_x) as f64; + self.active_column_idx = col_idx; + } + + last = Some((col_x, visible_ratio)); + } + + let Some(ViewOffsetAdjustment::Gesture(gesture)) = &mut self.view_offset_adj else { + unreachable!(); + }; + + gesture.current_view_offset = new_offset; + Some(true) + } + + pub fn view_offset_gesture_end(&mut self, _cancelled: bool) -> bool { + let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj else { + return false; + }; + + // We do not handle cancelling, just like GNOME Shell doesn't. For this gesture, proper + // cancelling would require keeping track of the original active column, and then updating + // it in all the right places (adding columns, removing columns, etc.) -- quite a bit of + // effort and bug potential. + + // FIXME: keep track of gesture velocity and use it to compute the final point and + // to animate to it. + let offset = gesture.current_view_offset.round() as i32; + + self.view_offset = offset; + self.view_offset_adj = None; + + if !self.columns.is_empty() { + let current_x = self.view_pos(); + self.animate_view_offset_to_column(current_x, self.active_column_idx, None); + } + + true + } } impl Workspace<Window> { |
