From e0ec6e5b11bc222c252157beb064104f4c60dfbe Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sat, 2 Mar 2024 14:33:22 +0400 Subject: Make vertical touchpad swipe inertial Values and implementation are heavily inspired by AdwSwipeTracker. --- src/input.rs | 8 ++++- src/layout/mod.rs | 13 +++++--- src/layout/monitor.rs | 31 ++++++++++++------ src/lib.rs | 1 + src/swipe_tracker.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 src/swipe_tracker.rs (limited to 'src') diff --git a/src/input.rs b/src/input.rs index d832195a..232cd77f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,6 @@ use std::any::Any; use std::collections::HashSet; +use std::time::Duration; use niri_config::{Action, Binds, Modifiers}; use niri_ipc::LayoutSwitchTarget; @@ -1242,8 +1243,13 @@ impl State { } } + let timestamp = Duration::from_micros(event.time()); + let mut handled = false; - let res = self.niri.layout.workspace_switch_gesture_update(delta_y); + let res = self + .niri + .layout + .workspace_switch_gesture_update(delta_y, timestamp); if let Some(output) = res { if let Some(output) = output { self.niri.queue_redraw(output); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index fb86ccac..6ec3ca8c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1603,14 +1603,18 @@ impl Layout { } } - pub fn workspace_switch_gesture_update(&mut self, delta_y: f64) -> Option> { + pub fn workspace_switch_gesture_update( + &mut self, + delta_y: f64, + timestamp: Duration, + ) -> Option> { let monitors = match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => monitors, MonitorSet::NoOutputs { .. } => return None, }; for monitor in monitors { - if let Some(refresh) = monitor.workspace_switch_gesture_update(delta_y) { + if let Some(refresh) = monitor.workspace_switch_gesture_update(delta_y, timestamp) { if refresh { return Some(Some(monitor.output.clone())); } else { @@ -2041,6 +2045,7 @@ mod tests { WorkspaceSwitchGestureUpdate { #[proptest(strategy = "-400f64..400f64")] delta: f64, + timestamp: Duration, }, WorkspaceSwitchGestureEnd { cancelled: bool, @@ -2303,8 +2308,8 @@ mod tests { layout.workspace_switch_gesture_begin(&output); } - Op::WorkspaceSwitchGestureUpdate { delta } => { - layout.workspace_switch_gesture_update(delta); + Op::WorkspaceSwitchGestureUpdate { delta, timestamp } => { + layout.workspace_switch_gesture_update(delta, timestamp); } Op::WorkspaceSwitchGestureEnd { cancelled } => { layout.workspace_switch_gesture_end(cancelled); diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 70a6981d..ee02a204 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -15,6 +15,7 @@ use super::workspace::{ use super::{LayoutElement, Options}; use crate::animation::Animation; use crate::render_helpers::renderer::NiriRenderer; +use crate::swipe_tracker::SwipeTracker; use crate::utils::output_size; #[derive(Debug)] @@ -43,6 +44,7 @@ pub struct WorkspaceSwitchGesture { pub center_idx: usize, /// Current, fractional workspace index. pub current_idx: f64, + pub tracker: SwipeTracker, } pub type MonitorRenderElement = @@ -703,21 +705,28 @@ impl Monitor { let gesture = WorkspaceSwitchGesture { center_idx, current_idx, + tracker: SwipeTracker::new(), }; self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture)); } - pub fn workspace_switch_gesture_update(&mut self, delta_y: f64) -> Option { + pub fn workspace_switch_gesture_update( + &mut self, + delta_y: f64, + timestamp: Duration, + ) -> Option { let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else { return None; }; + gesture.tracker.push(delta_y, timestamp); + // Normalize like GNOME Shell's workspace switching. - let delta_y = delta_y / 400.; + let pos = gesture.tracker.pos() / 400.; let min = gesture.center_idx.saturating_sub(1) as f64; let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64; - let new_idx = (gesture.current_idx + delta_y).clamp(min, max); + let new_idx = (gesture.center_idx as f64 + pos).clamp(min, max); if gesture.current_idx == new_idx { return Some(false); @@ -738,15 +747,17 @@ impl Monitor { return true; } - // FIXME: keep track of gesture velocity and use it to compute the final point and to - // animate to it. - let current_idx = gesture.current_idx; - let idx = current_idx.round() as usize; + let pos = gesture.tracker.projected_end_pos() / 400.; - self.active_workspace_idx = idx; + let min = gesture.center_idx.saturating_sub(1) as f64; + let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64; + let new_idx = (gesture.center_idx as f64 + pos).clamp(min, max); + let new_idx = new_idx.round() as usize; + + self.active_workspace_idx = new_idx; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( - current_idx, - idx as f64, + gesture.current_idx, + new_idx as f64, self.options.animations.workspace_switch, niri_config::Animation::default_workspace_switch(), ))); diff --git a/src/lib.rs b/src/lib.rs index fdf588b3..9ccb0c2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod layout; pub mod niri; pub mod protocols; pub mod render_helpers; +pub mod swipe_tracker; pub mod ui; pub mod utils; pub mod window; diff --git a/src/swipe_tracker.rs b/src/swipe_tracker.rs new file mode 100644 index 00000000..d7f42778 --- /dev/null +++ b/src/swipe_tracker.rs @@ -0,0 +1,87 @@ +use std::collections::VecDeque; +use std::time::Duration; + +const HISTORY_LIMIT: Duration = Duration::from_millis(150); +const DECELERATION_TOUCHPAD: f64 = 0.997; + +#[derive(Debug)] +pub struct SwipeTracker { + history: VecDeque, + pos: f64, +} + +#[derive(Debug, Clone, Copy)] +struct Event { + delta: f64, + timestamp: Duration, +} + +impl SwipeTracker { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + history: VecDeque::new(), + pos: 0., + } + } + + /// Pushes a new reading into the tracker. + pub fn push(&mut self, delta: f64, timestamp: Duration) { + // For the events that we care about, timestamps should always increase + // monotonically. + if let Some(last) = self.history.back() { + if timestamp < last.timestamp { + trace!( + "ignoring event with timestamp {timestamp:?} earlier than last {:?}", + last.timestamp + ); + return; + } + } + + self.history.push_back(Event { delta, timestamp }); + self.pos += delta; + + self.retain_recent(); + } + + /// Returns the current gesture position. + pub fn pos(&self) -> f64 { + self.pos + } + + /// Computes the current gesture velocity. + pub fn velocity(&self) -> f64 { + let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else { + return 0.; + }; + + let total_time = (last.timestamp - first.timestamp).as_secs_f64(); + if total_time == 0. { + return 0.; + } + + let total_delta = self.history.iter().map(|event| event.delta).sum::(); + total_delta / total_time + } + + /// Computes the gesture end position after decelerating to a halt. + pub fn projected_end_pos(&self) -> f64 { + let vel = self.velocity(); + self.pos - vel / (1000. * DECELERATION_TOUCHPAD.ln()) + } + + fn retain_recent(&mut self) { + let Some(&Event { timestamp, .. }) = self.history.back() else { + return; + }; + + while let Some(first) = self.history.front() { + if timestamp <= first.timestamp + HISTORY_LIMIT { + break; + } + + let _ = self.history.pop_front(); + } + } +} -- cgit