diff --git a/src/backend/render/animations/mod.rs b/src/backend/render/animations/mod.rs new file mode 100644 index 00000000..cc5365e6 --- /dev/null +++ b/src/backend/render/animations/mod.rs @@ -0,0 +1 @@ +pub mod spring; diff --git a/src/backend/render/animations/spring.rs b/src/backend/render/animations/spring.rs new file mode 100644 index 00000000..72b43904 --- /dev/null +++ b/src/backend/render/animations/spring.rs @@ -0,0 +1,138 @@ +// From Niri (GPL-3.0) https://github.com/YaLTeR/niri/tree/db49deb7fd2fbe805ceec060aa4dec65009ad7a7 +use std::time::Duration; + +#[derive(Debug, Clone, Copy)] +pub struct SpringParams { + pub damping: f64, + pub mass: f64, + pub stiffness: f64, + pub epsilon: f64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Spring { + pub from: f64, + pub to: f64, + pub initial_velocity: f64, + pub params: SpringParams, +} + +impl SpringParams { + pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self { + let damping_ratio = damping_ratio.max(0.); + let stiffness = stiffness.max(0.); + let epsilon = epsilon.max(0.); + + let mass = 1.; + let critical_damping = 2. * (mass * stiffness).sqrt(); + let damping = damping_ratio * critical_damping; + + Self { + damping, + mass, + stiffness, + epsilon, + } + } +} + +impl Spring { + pub fn value_at(&self, t: Duration) -> f64 { + self.oscillate(t.as_secs_f64()) + } + + // Based on libadwaita (LGPL-2.1-or-later): + // https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c, + // which itself is based on (MIT): + // https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m + /// Computes and returns the duration until the spring is at rest. + pub fn duration(&self) -> Duration { + const DELTA: f64 = 0.001; + + let beta = self.params.damping / (2. * self.params.mass); + + if beta.abs() <= f64::EPSILON || beta < 0. { + return Duration::MAX; + } + + let omega0 = (self.params.stiffness / self.params.mass).sqrt(); + + // As first ansatz for the overdamped solution, + // and general estimation for the oscillating ones + // we take the value of the envelope when it's < epsilon. + let mut x0 = -self.params.epsilon.ln() / beta; + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 { + return Duration::from_secs_f64(x0); + } + + // Since the overdamped solution decays way slower than the envelope + // we need to use the value of the oscillation itself. + // Newton's root finding method is a good candidate in this particular case: + // https://en.wikipedia.org/wiki/Newton%27s_method + let mut y0 = self.oscillate(x0); + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + let mut x1 = (self.to - y0 + m * x0) / m; + let mut y1 = self.oscillate(x1); + + let mut i = 0; + while (self.to - y1).abs() > self.params.epsilon { + if i > 1000 { + return Duration::ZERO; + } + + x0 = x1; + y0 = y1; + + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + x1 = (self.to - y0 + m * x0) / m; + y1 = self.oscillate(x1); + i += 1; + } + + Duration::from_secs_f64(x1) + } + + /// Returns the spring position at a given time in seconds. + fn oscillate(&self, t: f64) -> f64 { + let b = self.params.damping; + let m = self.params.mass; + let k = self.params.stiffness; + let v0 = self.initial_velocity; + + let beta = b / (2. * m); + let omega0 = (k / m).sqrt(); + + let x0 = self.from - self.to; + + let envelope = (-beta * t).exp(); + + // Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x) + // for the differential equation m*ẍ+b*ẋ+kx = 0 + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) { + // Critically damped. + self.to + envelope * (x0 + (beta * x0 + v0) * t) + } else if beta < omega0 { + // Underdamped. + let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt(); + + self.to + + envelope + * (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin()) + } else { + // Overdamped. + let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt(); + + self.to + + envelope + * (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh()) + } + } +} diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index e33da095..d2971177 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -16,7 +16,7 @@ use crate::{ grabs::{SeatMenuGrabState, SeatMoveGrabState}, layout::tiling::ANIMATION_DURATION, CosmicMapped, CosmicMappedRenderElement, OverviewMode, SessionLock, Trigger, - WorkspaceRenderElement, + WorkspaceDelta, WorkspaceRenderElement, }, state::{Common, Fps}, utils::prelude::*, @@ -70,6 +70,8 @@ use smithay::{ }; use tracing::warn; +pub mod animations; + pub mod cursor; use self::cursor::CursorRenderElement; pub mod element; @@ -468,7 +470,7 @@ pub fn workspace_elements( renderer: &mut R, state: &mut Common, output: &Output, - previous: Option<(WorkspaceHandle, usize, Instant)>, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, current: (WorkspaceHandle, usize), cursor_mode: CursorMode, _fps: &mut Option<&mut Fps>, @@ -684,10 +686,18 @@ where let has_fullscreen = workspace.fullscreen.is_some(); let is_active_space = workspace.outputs().any(|o| o == &active_output); - let percentage = { - let percentage = Instant::now().duration_since(*start).as_millis() as f32 - / ANIMATION_DURATION.as_millis() as f32; - ease(EaseInOutCubic, 0.0, 1.0, percentage) + let percentage = match start { + WorkspaceDelta::Shortcut(st) => ease( + EaseInOutCubic, + 0.0, + 1.0, + Instant::now().duration_since(*st).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32, + ), + WorkspaceDelta::Gesture(prog) => *prog as f32, + WorkspaceDelta::GestureEnd(st, spring) => { + (spring.value_at(Instant::now().duration_since(*st)) as f32).clamp(0.0, 1.0) + } }; let offset = Point::::from(match (layout, *previous_idx < current.1) { (WorkspaceLayout::Vertical, true) => { @@ -1012,7 +1022,7 @@ pub fn render_workspace( age: usize, state: &mut Common, output: &Output, - previous: Option<(WorkspaceHandle, usize, Instant)>, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, current: (WorkspaceHandle, usize), mut cursor_mode: CursorMode, screencopy: Option<(Source, &[(ScreencopySession, BufferParams)])>, diff --git a/src/input/gestures/mod.rs b/src/input/gestures/mod.rs new file mode 100644 index 00000000..2998615a --- /dev/null +++ b/src/input/gestures/mod.rs @@ -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, + pub action: Option, + pub delta: f64, + // Delta tracking inspired by Niri (GPL-3.0) https://github.com/YaLTeR/niri/tree/v0.1.3 + pub history: VecDeque, +} + +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, 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::(); + 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) + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index 6f2b217a..ea449a4f 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -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); @@ -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 = 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(¤t_output, workspace as usize); + let _ = self.common.shell.activate( + ¤t_output, + workspace as usize, + WorkspaceDelta::new_shortcut(), + ); } Action::LastWorkspace => { let current_output = seat.active_output(); @@ -1484,24 +1574,14 @@ impl State { .workspaces .len(¤t_output) .saturating_sub(1); - let _ = self.common.shell.activate(¤t_output, workspace); + let _ = self.common.shell.activate( + ¤t_output, + workspace, + WorkspaceDelta::new_shortcut(), + ); } Action::NextWorkspace => { - let current_output = seat.active_output(); - let workspace = self - .common - .shell - .workspaces - .active_num(¤t_output) - .1 - .saturating_add(1); - if self - .common - .shell - .activate(¤t_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(¤t_output) - .1 - .saturating_sub(1); - if self - .common - .shell - .activate(¤t_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, + gesture: bool, + ) -> Result>, InvalidWorkspaceIndex> { + let current_output = seat.active_output(); + let workspace = self + .common + .shell + .workspaces + .active_num(¤t_output) + .1 + .saturating_add(1); + + self.common.shell.activate( + ¤t_output, + workspace, + if gesture { + WorkspaceDelta::new_gesture() + } else { + WorkspaceDelta::new_shortcut() + }, + ) + } + + pub fn to_previous_workspace( + &mut self, + seat: &Seat, + gesture: bool, + ) -> Result>, InvalidWorkspaceIndex> { + let current_output = seat.active_output(); + let workspace = self + .common + .shell + .workspaces + .active_num(¤t_output) + .1 + .saturating_sub(1); + + self.common.shell.activate( + ¤t_output, + workspace, + if gesture { + WorkspaceDelta::new_gesture() + } else { + WorkspaceDelta::new_shortcut() + }, + ) + } } fn sessions_for_output(state: &Common, output: &Output) -> impl Iterator { diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 53458187..ce42ded3 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -46,6 +46,7 @@ use smithay::{ }; use crate::{ + backend::render::animations::spring::{Spring, SpringParams}, config::{Config, KeyModifiers, KeyPattern}, state::client_should_see_privileged_protocols, utils::prelude::*, @@ -93,6 +94,9 @@ use self::{ }; const ANIMATION_DURATION: Duration = Duration::from_millis(200); +const GESTURE_MAX_LENGTH: f64 = 150.0; +const GESTURE_POSITION_THRESHOLD: f64 = 0.5; +const GESTURE_VELOCITY_THRESHOLD: f64 = 0.02; #[derive(Debug, Clone)] pub enum Trigger { @@ -223,9 +227,41 @@ pub struct SessionLock { pub surfaces: HashMap, } +#[derive(Debug, Clone, Copy)] +pub enum WorkspaceDelta { + Shortcut(Instant), + Gesture(f64), + GestureEnd(Instant, Spring), + // InvalidGesture(f64), TODO + // InvalidGestureEnd(Instant, Spring), TODO +} + +impl WorkspaceDelta { + pub fn new_gesture() -> Self { + WorkspaceDelta::Gesture(0.0) + } + + pub fn new_gesture_end(delta: f64, velocity: f64) -> Self { + let params: SpringParams = SpringParams::new(1.0, 1000.0, 0.0001); + WorkspaceDelta::GestureEnd( + Instant::now(), + Spring { + from: delta, + to: 1.0, + initial_velocity: velocity, + params, + }, + ) + } + + pub fn new_shortcut() -> Self { + WorkspaceDelta::Shortcut(Instant::now()) + } +} + #[derive(Debug)] pub struct WorkspaceSet { - previously_active: Option<(usize, Instant)>, + previously_active: Option<(usize, WorkspaceDelta)>, active: usize, pub group: WorkspaceGroupHandle, idx: usize, @@ -371,6 +407,7 @@ impl WorkspaceSet { fn activate( &mut self, idx: usize, + workspace_delta: WorkspaceDelta, state: &mut WorkspaceUpdateGuard<'_, State>, ) -> Result { if idx >= self.workspaces.len() { @@ -383,15 +420,36 @@ impl WorkspaceSet { state.remove_workspace_state(&self.workspaces[old_active].handle, WState::Urgent); state.remove_workspace_state(&self.workspaces[idx].handle, WState::Urgent); state.add_workspace_state(&self.workspaces[idx].handle, WState::Active); - - self.previously_active = Some((old_active, Instant::now())); + self.previously_active = Some((old_active, workspace_delta)); self.active = idx; Ok(true) } else { + if let Some((p_idx, _)) = self.previously_active { + self.previously_active = Some((p_idx, workspace_delta)); + return Ok(true); + } Ok(false) } } + fn activate_previous( + &mut self, + workspace_delta: WorkspaceDelta, + state: &mut WorkspaceUpdateGuard<'_, State>, + ) -> Result { + if let Some((idx, _)) = self.previously_active { + return self.activate(idx, workspace_delta, state); + } + Err(InvalidWorkspaceIndex) + } + + fn update_workspace_delta(&mut self, delta: f64) { + let easing = delta.clamp(0.0, GESTURE_MAX_LENGTH).abs() / GESTURE_MAX_LENGTH; + if let Some((idx, _)) = self.previously_active { + self.previously_active = Some((idx, WorkspaceDelta::Gesture(easing))); + } + } + fn set_output( &mut self, new_output: &Output, @@ -410,8 +468,21 @@ impl WorkspaceSet { fn refresh<'a>(&mut self, xdg_activation_state: &XdgActivationState) { if let Some((_, start)) = self.previously_active { - if Instant::now().duration_since(start).as_millis() >= ANIMATION_DURATION.as_millis() { - self.previously_active = None; + match start { + WorkspaceDelta::Shortcut(st) => { + if Instant::now().duration_since(st).as_millis() as f32 + >= ANIMATION_DURATION.as_millis() as f32 + { + self.previously_active = None; + } + } + WorkspaceDelta::GestureEnd(st, spring) => { + if Instant::now().duration_since(st).as_millis() > spring.duration().as_millis() + { + self.previously_active = None; + } + } + _ => {} } } else { self.workspaces[self.active].refresh(xdg_activation_state); @@ -855,7 +926,7 @@ impl Workspaces { .and_then(|set| set.workspaces.get_mut(num)) } - pub fn active(&self, output: &Output) -> (Option<(&Workspace, Instant)>, &Workspace) { + pub fn active(&self, output: &Output) -> (Option<(&Workspace, WorkspaceDelta)>, &Workspace) { let set = self.sets.get(output).or(self.backup_set.as_ref()).unwrap(); ( set.previously_active @@ -1095,6 +1166,7 @@ impl Shell { &mut self, output: &Output, idx: usize, + workspace_delta: WorkspaceDelta, ) -> Result>, InvalidWorkspaceIndex> { match &mut self.workspaces.mode { WorkspaceMode::OutputBound => { @@ -1105,7 +1177,7 @@ impl Shell { ) { set.workspaces[set.active].tiling_layer.cleanup_drag(); } - set.activate(idx, &mut self.workspace_state.update())?; + set.activate(idx, workspace_delta, &mut self.workspace_state.update())?; if let Some(xwm) = self .xwayland_state .as_mut() @@ -1151,7 +1223,143 @@ impl Shell { } WorkspaceMode::Global => { for set in self.workspaces.sets.values_mut() { - set.activate(idx, &mut self.workspace_state.update())?; + set.activate(idx, workspace_delta, &mut self.workspace_state.update())?; + } + Ok(None) + } + } + } + + pub fn update_workspace_delta(&mut self, output: &Output, delta: f64) { + match &mut self.workspaces.mode { + WorkspaceMode::OutputBound => { + if let Some(set) = self.workspaces.sets.get_mut(output) { + set.update_workspace_delta(delta); + } + } + WorkspaceMode::Global => { + for set in self.workspaces.sets.values_mut() { + set.update_workspace_delta(delta); + } + } + } + } + + pub fn end_workspace_swipe( + &mut self, + output: &Output, + velocity: f64, + ) -> Result>, InvalidWorkspaceIndex> { + match &mut self.workspaces.mode { + WorkspaceMode::OutputBound => { + if let Some(set) = self.workspaces.sets.get_mut(output) { + if matches!( + self.overview_mode, + OverviewMode::Started(Trigger::Pointer(_), _) + ) { + set.workspaces[set.active].tiling_layer.cleanup_drag(); + } + if let Some((_, workspace_delta)) = set.previously_active { + match workspace_delta { + WorkspaceDelta::Gesture(delta) => { + if (velocity > 0.0 && velocity.abs() >= GESTURE_VELOCITY_THRESHOLD) + || (velocity.abs() < GESTURE_VELOCITY_THRESHOLD + && delta.abs() > GESTURE_POSITION_THRESHOLD) + { + set.activate( + set.active, + WorkspaceDelta::new_gesture_end( + delta.abs(), + velocity.abs(), + ), + &mut self.workspace_state.update(), + )?; + } else { + set.activate_previous( + WorkspaceDelta::new_gesture_end( + 1.0 - delta.abs(), + velocity.abs(), + ), + &mut self.workspace_state.update(), + )?; + } + } + _ => {} // Do nothing + } + } + if let Some(xwm) = self + .xwayland_state + .as_mut() + .and_then(|state| state.xwm.as_mut()) + { + { + for window in set.workspaces[set.active] + .tiling_layer + .mapped() + .map(|(w, _)| w) + .chain(set.workspaces[set.active].floating_layer.space.elements()) + { + if let Some(surf) = window.active_window().x11_surface() { + let _ = xwm.raise_window(surf); + } + } + for window in set.sticky_layer.space.elements() { + if let Some(surf) = window.active_window().x11_surface() { + let _ = xwm.raise_window(surf); + } + } + if let Some(surf) = set.workspaces[set.active] + .fullscreen + .as_ref() + .and_then(|f| f.surface.x11_surface()) + { + let _ = xwm.raise_window(surf); + } + } + for surface in &self.override_redirect_windows { + let _ = xwm.raise_window(surface); + } + } + + let output_geo = output.geometry(); + Ok(Some( + output_geo.loc + + Point::from((output_geo.size.w / 2, output_geo.size.h / 2)), + )) + } else { + Ok(None) + } + } + WorkspaceMode::Global => { + for set in self.workspaces.sets.values_mut() { + if let Some((_, workspace_delta)) = set.previously_active { + match workspace_delta { + WorkspaceDelta::Gesture(delta) => { + if (velocity > 0.0 && velocity.abs() >= GESTURE_VELOCITY_THRESHOLD) + || (velocity.abs() < GESTURE_VELOCITY_THRESHOLD + && delta.abs() > GESTURE_POSITION_THRESHOLD) + { + set.activate( + set.active, + WorkspaceDelta::new_gesture_end( + delta.abs(), + velocity.abs(), + ), + &mut self.workspace_state.update(), + )?; + } else { + set.activate_previous( + WorkspaceDelta::new_gesture_end( + 1.0 - delta.abs(), + velocity.abs(), + ), + &mut self.workspace_state.update(), + )?; + } + } + _ => {} // Do nothing + } + } } Ok(None) } @@ -1923,7 +2131,13 @@ impl Shell { .shell .workspaces .idx_for_handle(&to_output, to) - .and_then(|to_idx| state.common.shell.activate(&to_output, to_idx).unwrap()) + .and_then(|to_idx| { + state + .common + .shell + .activate(&to_output, to_idx, WorkspaceDelta::new_shortcut()) + .unwrap() + }) } else { None }; diff --git a/src/state.rs b/src/state.rs index 3bc4c314..e5c9cfba 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,7 +3,7 @@ use crate::{ backend::{kms::KmsState, winit::WinitState, x11::X11State}, config::{Config, OutputConfig}, - input::Devices, + input::{gestures::GestureState, Devices}, shell::{grabs::SeatMoveGrabState, Shell}, utils::prelude::*, wayland::protocols::{ @@ -159,6 +159,7 @@ pub struct Common { pub clock: Clock, pub should_stop: bool, pub local_offset: time::UtcOffset, + pub gesture_state: Option, pub theme: cosmic::Theme, @@ -423,6 +424,7 @@ impl State { clock, should_stop: false, + gesture_state: None, theme: cosmic::theme::system_preference(), diff --git a/src/wayland/handlers/toplevel_management.rs b/src/wayland/handlers/toplevel_management.rs index 38260d69..33a6d1eb 100644 --- a/src/wayland/handlers/toplevel_management.rs +++ b/src/wayland/handlers/toplevel_management.rs @@ -10,7 +10,7 @@ use smithay::{ }; use crate::{ - shell::{CosmicSurface, Shell}, + shell::{CosmicSurface, Shell, WorkspaceDelta}, utils::prelude::*, wayland::protocols::{ toplevel_info::ToplevelInfoHandler, @@ -60,7 +60,11 @@ impl ToplevelManagementHandler for State { .unwrap() .clone(); - let _ = self.common.shell.activate(&output, idx as usize); // TODO: Move pointer? + let _ = self.common.shell.activate( + &output, + idx as usize, + WorkspaceDelta::new_shortcut(), + ); // TODO: Move pointer? mapped.focus_window(window); Common::set_focus(self, Some(&mapped.clone().into()), &seat, None); return; diff --git a/src/wayland/handlers/workspace.rs b/src/wayland/handlers/workspace.rs index 1e8a1f34..6897f8b4 100644 --- a/src/wayland/handlers/workspace.rs +++ b/src/wayland/handlers/workspace.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ + shell::WorkspaceDelta, state::ClientState, utils::prelude::*, wayland::protocols::workspace::{ @@ -38,7 +39,11 @@ impl WorkspaceHandler for State { }); if let Some((output, idx)) = maybe { - let _ = self.common.shell.activate(&output, idx); // TODO: move cursor? + let _ = self.common.shell.activate( + &output, + idx, + WorkspaceDelta::new_shortcut(), + ); // TODO: move cursor? } } Request::SetTilingState { workspace, state } => {