feat: workspace switching touchpad gestures

This commit is contained in:
Ryan Brue 2024-03-07 13:14:53 -06:00 committed by Victoria Brekenfeld
parent f1d8225ddb
commit fc2173d028
9 changed files with 767 additions and 89 deletions

176
src/input/gestures/mod.rs Normal file
View file

@ -0,0 +1,176 @@
use smithay::utils::{Logical, Point};
use std::{collections::VecDeque, time::Duration};
use tracing::trace;
use crate::shell::Direction;
const HISTORY_LIMIT: Duration = Duration::from_millis(150);
const DECELERATION_TOUCHPAD: f64 = 0.997;
#[derive(Debug, Clone, Copy)]
pub struct SwipeEvent {
delta: f64,
timestamp: Duration,
}
#[derive(Debug, Clone, Copy)]
pub enum SwipeAction {
NextWorkspace,
PrevWorkspace,
}
#[derive(Debug, Clone)]
pub struct GestureState {
pub fingers: u32,
pub direction: Option<Direction>,
pub action: Option<SwipeAction>,
pub delta: f64,
// Delta tracking inspired by Niri (GPL-3.0) https://github.com/YaLTeR/niri/tree/v0.1.3
pub history: VecDeque<SwipeEvent>,
}
impl GestureState {
pub fn new(fingers: u32) -> Self {
GestureState {
fingers,
direction: None,
action: None,
delta: 0.0,
history: VecDeque::new(),
}
}
pub fn update(&mut self, movement: Point<f64, Logical>, timestamp: Duration) -> bool {
let first_update = self.direction.is_none();
if first_update {
// Find proper direction
self.direction = if movement.x.abs() > movement.y.abs() {
if movement.x > 0.0 {
Some(Direction::Right)
} else {
Some(Direction::Left)
}
} else {
if movement.y > 0.0 {
Some(Direction::Down)
} else {
Some(Direction::Up)
}
}
}
let delta = match self.direction {
Some(Direction::Left) => -movement.x,
Some(Direction::Right) => movement.x,
Some(Direction::Up) => -movement.y,
Some(Direction::Down) => movement.y,
None => 0.0,
};
self.push(delta, timestamp);
first_update
}
/// Pushes a new reading into the tracker.
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(SwipeEvent { delta, timestamp });
self.delta += delta;
self.trim_history();
}
/// 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::<f64>();
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.delta - vel / (1000. * DECELERATION_TOUCHPAD.ln())
}
fn trim_history(&mut self) {
let Some(&SwipeEvent { 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();
}
}
}
impl Default for GestureState {
fn default() -> Self {
GestureState::new(0)
}
}
// rubber_band.rs from Niri (GPL-3.0) https://github.com/YaLTeR/niri/blob/db49deb7fd2fbe805ceec060aa4dec65009ad7a7/src/rubber_band.rs
#[derive(Debug, Clone, Copy)]
pub struct RubberBand {
pub stiffness: f64,
pub limit: f64,
}
impl RubberBand {
pub fn band(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
(1. - (1. / (x * c / d + 1.))) * d
}
pub fn derivative(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
c * d * d / (c * x + d).powi(2)
}
pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 {
let clamped = x.clamp(min, max);
let sign = if x < clamped { -1. } else { 1. };
let diff = (x - clamped).abs();
clamped + sign * self.band(diff)
}
pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 {
if min <= x && x <= max {
return 1.;
}
let clamped = x.clamp(min, max);
let diff = (x - clamped).abs();
self.derivative(diff)
}
}

View file

@ -3,6 +3,7 @@
use crate::{
backend::render::cursor::CursorState,
config::{xkb_config_to_wl, Action, Config, KeyModifiers, KeyPattern},
input::gestures::{GestureState, SwipeAction},
shell::{
focus::{target::PointerFocusTarget, FocusDirection},
grabs::{ResizeEdge, SeatMenuGrabState, SeatMoveGrabState},
@ -10,7 +11,8 @@ use crate::{
floating::ResizeGrabMarker,
tiling::{SwapWindowGrab, TilingLayout},
},
Direction, FocusResult, MoveResult, OverviewMode, ResizeDirection, ResizeMode, Trigger,
Direction, FocusResult, InvalidWorkspaceIndex, MoveResult, OverviewMode, ResizeDirection,
ResizeMode, Trigger, WorkspaceDelta,
},
state::Common,
utils::prelude::*,
@ -68,6 +70,8 @@ use std::{
crate::utils::id_gen!(next_seat_id, SEAT_ID, SEAT_IDS);
pub mod gestures;
#[repr(transparent)]
pub struct SeatId(pub usize);
pub struct ActiveOutput(pub RefCell<Output>);
@ -1071,42 +1075,127 @@ impl State {
}
InputEvent::GestureSwipeBegin { event, .. } => {
if let Some(seat) = self.common.seat_with_device(&event.device()) {
let serial = SERIAL_COUNTER.next_serial();
let pointer = seat.get_pointer().unwrap();
pointer.gesture_swipe_begin(
self,
&GestureSwipeBeginEvent {
serial,
time: event.time_msec(),
fingers: event.fingers(),
},
);
if event.fingers() >= 3 {
self.common.gesture_state = Some(GestureState::new(event.fingers()));
} else {
let serial = SERIAL_COUNTER.next_serial();
let pointer = seat.get_pointer().unwrap();
pointer.gesture_swipe_begin(
self,
&GestureSwipeBeginEvent {
serial,
time: event.time_msec(),
fingers: event.fingers(),
},
);
}
}
}
InputEvent::GestureSwipeUpdate { event, .. } => {
if let Some(seat) = self.common.seat_with_device(&event.device()) {
let pointer = seat.get_pointer().unwrap();
pointer.gesture_swipe_update(
self,
&GestureSwipeUpdateEvent {
time: event.time_msec(),
delta: event.delta(),
},
);
if let Some(seat) = self.common.seat_with_device(&event.device()).cloned() {
let mut activate_action: Option<SwipeAction> = None;
if let Some(ref mut gesture_state) = self.common.gesture_state {
let first_update = gesture_state.update(
event.delta(),
Duration::from_millis(event.time_msec() as u64),
);
// Decide on action if first update
if first_update {
activate_action = match gesture_state.fingers {
3 => None, // TODO: 3 finger gestures
4 => {
if self.common.config.cosmic_conf.workspaces.workspace_layout
== WorkspaceLayout::Horizontal
{
match gesture_state.direction {
Some(Direction::Left) => {
Some(SwipeAction::NextWorkspace)
}
Some(Direction::Right) => {
Some(SwipeAction::PrevWorkspace)
}
_ => None, // TODO: Other actions
}
} else {
match gesture_state.direction {
Some(Direction::Up) => Some(SwipeAction::NextWorkspace),
Some(Direction::Down) => {
Some(SwipeAction::PrevWorkspace)
}
_ => None, // TODO: Other actions
}
}
}
_ => None,
};
gesture_state.action = activate_action;
}
match gesture_state.action {
Some(SwipeAction::NextWorkspace) | Some(SwipeAction::PrevWorkspace) => {
self.common.shell.update_workspace_delta(
&seat.active_output(),
gesture_state.delta,
)
}
_ => {}
}
} else {
let pointer = seat.get_pointer().unwrap();
pointer.gesture_swipe_update(
self,
&GestureSwipeUpdateEvent {
time: event.time_msec(),
delta: event.delta(),
},
);
}
match activate_action {
Some(SwipeAction::NextWorkspace) => {
let _ = self.to_next_workspace(&seat, true);
}
Some(SwipeAction::PrevWorkspace) => {
let _ = self.to_previous_workspace(&seat, true);
}
_ => {}
}
}
}
InputEvent::GestureSwipeEnd { event, .. } => {
if let Some(seat) = self.common.seat_with_device(&event.device()) {
let serial = SERIAL_COUNTER.next_serial();
let pointer = seat.get_pointer().unwrap();
pointer.gesture_swipe_end(
self,
&GestureSwipeEndEvent {
serial,
time: event.time_msec(),
cancelled: event.cancelled(),
},
);
if let Some(seat) = self.common.seat_with_device(&event.device()).cloned() {
if let Some(ref gesture_state) = self.common.gesture_state {
match gesture_state.action {
Some(SwipeAction::NextWorkspace) | Some(SwipeAction::PrevWorkspace) => {
let velocity = gesture_state.velocity();
let norm_velocity =
if self.common.config.cosmic_conf.workspaces.workspace_layout
== WorkspaceLayout::Horizontal
{
velocity / seat.active_output().geometry().size.w as f64
} else {
velocity / seat.active_output().geometry().size.h as f64
};
let _ = self
.common
.shell
.end_workspace_swipe(&seat.active_output(), norm_velocity);
}
_ => {}
}
self.common.gesture_state = None;
} else {
let serial = SERIAL_COUNTER.next_serial();
let pointer = seat.get_pointer().unwrap();
pointer.gesture_swipe_end(
self,
&GestureSwipeEndEvent {
serial,
time: event.time_msec(),
cancelled: event.cancelled(),
},
);
}
}
}
InputEvent::GesturePinchBegin { event, .. } => {
@ -1471,10 +1560,11 @@ impl State {
0 => 9,
x => x - 1,
};
let _ = self
.common
.shell
.activate(&current_output, workspace as usize);
let _ = self.common.shell.activate(
&current_output,
workspace as usize,
WorkspaceDelta::new_shortcut(),
);
}
Action::LastWorkspace => {
let current_output = seat.active_output();
@ -1484,24 +1574,14 @@ impl State {
.workspaces
.len(&current_output)
.saturating_sub(1);
let _ = self.common.shell.activate(&current_output, workspace);
let _ = self.common.shell.activate(
&current_output,
workspace,
WorkspaceDelta::new_shortcut(),
);
}
Action::NextWorkspace => {
let current_output = seat.active_output();
let workspace = self
.common
.shell
.workspaces
.active_num(&current_output)
.1
.saturating_add(1);
if self
.common
.shell
.activate(&current_output, workspace)
.is_err()
&& propagate
{
if self.to_next_workspace(seat, false).is_err() && propagate {
if let Some(inferred) = pattern.inferred_direction() {
self.handle_action(
Action::SwitchOutput(inferred),
@ -1516,21 +1596,7 @@ impl State {
}
}
Action::PreviousWorkspace => {
let current_output = seat.active_output();
let workspace = self
.common
.shell
.workspaces
.active_num(&current_output)
.1
.saturating_sub(1);
if self
.common
.shell
.activate(&current_output, workspace)
.is_err()
&& propagate
{
if self.to_previous_workspace(seat, false).is_err() && propagate {
if let Some(inferred) = pattern.inferred_direction() {
self.handle_action(
Action::SwitchOutput(inferred),
@ -1663,7 +1729,11 @@ impl State {
if let Some(next_output) = next_output {
let idx = self.common.shell.workspaces.active_num(&next_output).1;
match self.common.shell.activate(&next_output, idx) {
match self.common.shell.activate(
&next_output,
idx,
WorkspaceDelta::new_shortcut(),
) {
Ok(Some(new_pos)) => {
seat.set_active_output(&next_output);
if let Some(ptr) = seat.get_pointer() {
@ -1726,7 +1796,11 @@ impl State {
.cloned();
if let Some(next_output) = next_output {
let idx = self.common.shell.workspaces.active_num(&next_output).1;
match self.common.shell.activate(&next_output, idx) {
match self.common.shell.activate(
&next_output,
idx,
WorkspaceDelta::new_shortcut(),
) {
Ok(Some(new_pos)) => {
seat.set_active_output(&next_output);
if let Some(ptr) = seat.get_pointer() {
@ -1762,7 +1836,11 @@ impl State {
.cloned();
if let Some(prev_output) = prev_output {
let idx = self.common.shell.workspaces.active_num(&prev_output).1;
match self.common.shell.activate(&prev_output, idx) {
match self.common.shell.activate(
&prev_output,
idx,
WorkspaceDelta::new_shortcut(),
) {
Ok(Some(new_pos)) => {
seat.set_active_output(&prev_output);
if let Some(ptr) = seat.get_pointer() {
@ -2271,6 +2349,56 @@ impl State {
None
}
}
pub fn to_next_workspace(
&mut self,
seat: &Seat<State>,
gesture: bool,
) -> Result<Option<Point<i32, Global>>, InvalidWorkspaceIndex> {
let current_output = seat.active_output();
let workspace = self
.common
.shell
.workspaces
.active_num(&current_output)
.1
.saturating_add(1);
self.common.shell.activate(
&current_output,
workspace,
if gesture {
WorkspaceDelta::new_gesture()
} else {
WorkspaceDelta::new_shortcut()
},
)
}
pub fn to_previous_workspace(
&mut self,
seat: &Seat<State>,
gesture: bool,
) -> Result<Option<Point<i32, Global>>, InvalidWorkspaceIndex> {
let current_output = seat.active_output();
let workspace = self
.common
.shell
.workspaces
.active_num(&current_output)
.1
.saturating_sub(1);
self.common.shell.activate(
&current_output,
workspace,
if gesture {
WorkspaceDelta::new_gesture()
} else {
WorkspaceDelta::new_shortcut()
},
)
}
}
fn sessions_for_output(state: &Common, output: &Output) -> impl Iterator<Item = Session> {