Add continuous touchpad workspace swiping
This commit is contained in:
1 parent
17b8482433
commit
464a1a635b
5 files changed
+328
-59
No files matched your search
+101
-17
@@ -1,4 +1,4 @@
|
|||||||
//! A swipe commits once on release; cancelled and ambiguous gestures do nothing.
|
//! Navigation previews follow finger motion; actions commit once on release.
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
@@ -14,6 +14,8 @@ pub(crate) struct Swipe {
|
|||||||
delta: Option<(f64, f64)>,
|
delta: Option<(f64, f64)>,
|
||||||
threshold: f64,
|
threshold: f64,
|
||||||
fingers: u32,
|
fingers: u32,
|
||||||
|
direction: Option<SwipeDirection>,
|
||||||
|
preview_started: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Swipe {
|
impl Swipe {
|
||||||
@@ -21,6 +23,8 @@ impl Swipe {
|
|||||||
self.delta = ((3..=5).contains(&fingers) && !blocked).then_some((0.0, 0.0));
|
self.delta = ((3..=5).contains(&fingers) && !blocked).then_some((0.0, 0.0));
|
||||||
self.threshold = f64::from(threshold);
|
self.threshold = f64::from(threshold);
|
||||||
self.fingers = fingers;
|
self.fingers = fingers;
|
||||||
|
self.direction = None;
|
||||||
|
self.preview_started = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fingers(&self) -> u32 {
|
pub fn fingers(&self) -> u32 {
|
||||||
@@ -35,30 +39,63 @@ impl Swipe {
|
|||||||
if let Some((x, y)) = &mut self.delta {
|
if let Some((x, y)) = &mut self.delta {
|
||||||
*x += dx;
|
*x += dx;
|
||||||
*y += dy;
|
*y += dy;
|
||||||
|
if self.direction.is_none() {
|
||||||
|
self.direction = dominant_direction(*x, *y, 6.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_preview_started(&mut self) {
|
||||||
|
self.preview_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preview_started(&self) -> bool {
|
||||||
|
self.preview_started
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preview(&self) -> Option<(SwipeDirection, f64)> {
|
||||||
|
let (x, y) = self.delta?;
|
||||||
|
if !x.is_finite() || !y.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let direction = self.direction?;
|
||||||
|
let distance = match direction {
|
||||||
|
SwipeDirection::Left => -x,
|
||||||
|
SwipeDirection::Right => x,
|
||||||
|
SwipeDirection::Up => -y,
|
||||||
|
SwipeDirection::Down => y,
|
||||||
|
};
|
||||||
|
Some((direction, (distance / (self.threshold.max(1.0) * 2.0)).clamp(0.0, 1.0)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn finish(&mut self, cancelled: bool) -> Option<SwipeDirection> {
|
pub fn finish(&mut self, cancelled: bool) -> Option<SwipeDirection> {
|
||||||
let (x, y) = self.delta.take()?;
|
let (x, y) = self.delta.take()?;
|
||||||
if cancelled || !x.is_finite() || !y.is_finite() {
|
if cancelled {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
const DOMINANCE: f64 = 1.25;
|
dominant_direction(x, y, self.threshold).filter(|direction| Some(*direction) == self.direction)
|
||||||
if x.abs() >= self.threshold && x.abs() > y.abs() * DOMINANCE {
|
}
|
||||||
Some(if x < 0.0 {
|
}
|
||||||
SwipeDirection::Left
|
|
||||||
} else {
|
fn dominant_direction(x: f64, y: f64, threshold: f64) -> Option<SwipeDirection> {
|
||||||
SwipeDirection::Right
|
if !x.is_finite() || !y.is_finite() {
|
||||||
})
|
return None;
|
||||||
} else if y.abs() >= self.threshold && y.abs() > x.abs() * DOMINANCE {
|
}
|
||||||
Some(if y < 0.0 {
|
const DOMINANCE: f64 = 1.25;
|
||||||
SwipeDirection::Up
|
if x.abs() >= threshold && x.abs() > y.abs() * DOMINANCE {
|
||||||
} else {
|
Some(if x < 0.0 {
|
||||||
SwipeDirection::Down
|
SwipeDirection::Left
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
None
|
SwipeDirection::Right
|
||||||
}
|
})
|
||||||
|
} else if y.abs() >= threshold && y.abs() > x.abs() * DOMINANCE {
|
||||||
|
Some(if y < 0.0 {
|
||||||
|
SwipeDirection::Up
|
||||||
|
} else {
|
||||||
|
SwipeDirection::Down
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +103,53 @@ impl Swipe {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cancellation_and_new_gestures_reset_preview_state() {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 80);
|
||||||
|
swipe.update(0.0, -200.0);
|
||||||
|
swipe.mark_preview_started();
|
||||||
|
assert_eq!(swipe.preview(), Some((SwipeDirection::Up, 1.0)));
|
||||||
|
assert_eq!(swipe.finish(true), None);
|
||||||
|
assert!(
|
||||||
|
swipe.preview_started(),
|
||||||
|
"release must suppress a second action after a preview"
|
||||||
|
);
|
||||||
|
assert_eq!(swipe.preview(), None);
|
||||||
|
swipe.begin(4, false, 80);
|
||||||
|
assert!(!swipe.preview_started());
|
||||||
|
swipe.update(-100.0, 0.0);
|
||||||
|
assert_eq!(swipe.finish(false), Some(SwipeDirection::Left));
|
||||||
|
swipe.begin(3, true, 80);
|
||||||
|
swipe.update(0.0, -200.0);
|
||||||
|
assert!(!swipe.active());
|
||||||
|
assert_eq!(swipe.finish(false), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_tracks_motion_and_reversal_before_release() {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 80);
|
||||||
|
swipe.update(0.0, -40.0);
|
||||||
|
assert_eq!(swipe.preview(), Some((SwipeDirection::Up, 0.25)));
|
||||||
|
swipe.update(0.0, -40.0);
|
||||||
|
assert_eq!(swipe.preview(), Some((SwipeDirection::Up, 0.5)));
|
||||||
|
swipe.update(0.0, 60.0);
|
||||||
|
assert_eq!(swipe.preview(), Some((SwipeDirection::Up, 0.125)));
|
||||||
|
assert_eq!(swipe.finish(false), None);
|
||||||
|
assert_eq!(swipe.preview(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reversal_across_the_origin_cancels_instead_of_committing_another_axis() {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 80);
|
||||||
|
swipe.update(0.0, -40.0);
|
||||||
|
swipe.update(0.0, 160.0);
|
||||||
|
assert_eq!(swipe.preview(), Some((SwipeDirection::Up, 0.0)));
|
||||||
|
assert_eq!(swipe.finish(false), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_swipe_accumulates_motion_and_commits_only_once_on_release() {
|
fn a_swipe_accumulates_motion_and_commits_only_once_on_release() {
|
||||||
let mut swipe = Swipe::default();
|
let mut swipe = Swipe::default();
|
||||||
|
|||||||
+38
-30
@@ -160,6 +160,7 @@ impl Ferese {
|
|||||||
match event {
|
match event {
|
||||||
InputEvent::GestureSwipeBegin { event } => {
|
InputEvent::GestureSwipeBegin { event } => {
|
||||||
let pointer = seat.get_pointer().expect("seat has a pointer");
|
let pointer = seat.get_pointer().expect("seat has a pointer");
|
||||||
|
self.finish_workspace_swipe(None);
|
||||||
self.swipe.begin(
|
self.swipe.begin(
|
||||||
event.fingers(),
|
event.fingers(),
|
||||||
self.swipe_navigation_blocked()
|
self.swipe_navigation_blocked()
|
||||||
@@ -185,6 +186,17 @@ impl Ferese {
|
|||||||
InputEvent::GestureSwipeUpdate { event } => {
|
InputEvent::GestureSwipeUpdate { event } => {
|
||||||
if self.swipe.active() {
|
if self.swipe.active() {
|
||||||
self.swipe.update(event.delta_x(), event.delta_y());
|
self.swipe.update(event.delta_x(), event.delta_y());
|
||||||
|
if self.swipe_navigation_blocked() {
|
||||||
|
self.finish_workspace_swipe(None);
|
||||||
|
} else if let Some((direction, progress)) = self.swipe.preview()
|
||||||
|
&& let Some(BindingAction::SwitchRelativeWorkspace(next)) = self
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.find(|binding| binding.matches_swipe(self.swipe.fingers(), direction))
|
||||||
|
.map(|binding| binding.action.clone())
|
||||||
|
{
|
||||||
|
self.preview_workspace_swipe(next, direction, progress);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
||||||
self,
|
self,
|
||||||
@@ -199,7 +211,10 @@ impl Ferese {
|
|||||||
if self.swipe.active() {
|
if self.swipe.active() {
|
||||||
let fingers = self.swipe.fingers();
|
let fingers = self.swipe.fingers();
|
||||||
let cancelled = event.cancelled() || self.swipe_navigation_blocked();
|
let cancelled = event.cancelled() || self.swipe_navigation_blocked();
|
||||||
if let Some(direction) = self.swipe.finish(cancelled)
|
let direction = self.swipe.finish(cancelled);
|
||||||
|
let handled = self.finish_workspace_swipe(direction) || self.swipe.preview_started();
|
||||||
|
if !handled
|
||||||
|
&& let Some(direction) = direction
|
||||||
&& let Some(action) = self
|
&& let Some(action) = self
|
||||||
.bindings
|
.bindings
|
||||||
.iter()
|
.iter()
|
||||||
@@ -721,55 +736,48 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
|
fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
|
||||||
let Some(output) = self.output_workspaces.focused_output() else {
|
if let Some(workspace) = self.relative_workspace_target(next) {
|
||||||
return;
|
if let Some(direction) = slide {
|
||||||
};
|
self.activate_managed_workspace_from_swipe(workspace, direction);
|
||||||
let Some(current) = self.output_workspaces.active_workspace(output) else {
|
} else {
|
||||||
return;
|
self.activate_managed_workspace(workspace);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn relative_workspace_target(&mut self, next: bool) -> Option<ferese_core::WorkspaceId> {
|
||||||
|
let output = self.output_workspaces.focused_output()?;
|
||||||
|
let current = self.output_workspaces.active_workspace(output)?;
|
||||||
let mut candidates = self.workspaces.ordered();
|
let mut candidates = self.workspaces.ordered();
|
||||||
candidates.retain(|workspace| {
|
candidates.retain(|workspace| {
|
||||||
self.output_workspaces
|
self.output_workspaces
|
||||||
.output_for_workspace(workspace.id)
|
.output_for_workspace(workspace.id)
|
||||||
.is_none_or(|owner| owner == output)
|
.is_none_or(|owner| owner == output)
|
||||||
});
|
});
|
||||||
let Some(index) = candidates.iter().position(|workspace| workspace.id == current) else {
|
let index = candidates.iter().position(|workspace| workspace.id == current)?;
|
||||||
return;
|
let neighbor = if next {
|
||||||
};
|
|
||||||
let index = if next {
|
|
||||||
index.checked_add(1)
|
index.checked_add(1)
|
||||||
} else {
|
} else {
|
||||||
index.checked_sub(1)
|
index.checked_sub(1)
|
||||||
};
|
};
|
||||||
if let Some(workspace) = index
|
if let Some(workspace) = neighbor.and_then(|index| candidates.get(index)) {
|
||||||
.and_then(|index| candidates.get(index))
|
return Some(workspace.id);
|
||||||
.map(|workspace| workspace.id)
|
}
|
||||||
{
|
if next
|
||||||
if let Some(direction) = slide {
|
|
||||||
self.activate_managed_workspace_from_swipe(workspace, direction);
|
|
||||||
} else {
|
|
||||||
self.activate_managed_workspace(workspace);
|
|
||||||
}
|
|
||||||
} else if next
|
|
||||||
&& self.workspaces.workspace(current).is_some_and(|workspace| {
|
&& self.workspaces.workspace(current).is_some_and(|workspace| {
|
||||||
workspace.layout.window_ids().next().is_some() || !workspace.floating.is_empty()
|
workspace.layout.window_ids().next().is_some() || !workspace.floating.is_empty()
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
let next_number = self
|
let number = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|workspace| workspace.name.parse::<u32>().ok())
|
.filter_map(|workspace| workspace.name.parse::<u32>().ok())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
.checked_add(1);
|
.checked_add(1)?;
|
||||||
if let Some(number) = next_number {
|
return self.workspaces.ensure_numeric(number).ok();
|
||||||
if let Some(direction) = slide {
|
|
||||||
self.switch_workspace_from_swipe(number, direction);
|
|
||||||
} else {
|
|
||||||
self.switch_workspace(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
||||||
|
|||||||
+183
-7
@@ -92,6 +92,17 @@ const CLOSE_ANIMATION_DURATION: Duration = Duration::from_millis(140);
|
|||||||
const WORKSPACE_SLIDE_DURATION: Duration = Duration::from_millis(220);
|
const WORKSPACE_SLIDE_DURATION: Duration = Duration::from_millis(220);
|
||||||
const WORKSPACE_SLIDE_FIRST_FRAME: Duration = Duration::from_millis(8);
|
const WORKSPACE_SLIDE_FIRST_FRAME: Duration = Duration::from_millis(8);
|
||||||
|
|
||||||
|
fn workspace_swipe_is_current(
|
||||||
|
outputs: &OutputWorkspaceMap,
|
||||||
|
output: OutputId,
|
||||||
|
from: WorkspaceId,
|
||||||
|
to: WorkspaceId,
|
||||||
|
) -> bool {
|
||||||
|
outputs.focused_output() == Some(output)
|
||||||
|
&& outputs.active_workspace(output) == Some(from)
|
||||||
|
&& outputs.output_for_workspace(to).is_none_or(|owner| owner == output)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
struct SlideOffset {
|
struct SlideOffset {
|
||||||
x: f64,
|
x: f64,
|
||||||
@@ -129,6 +140,8 @@ struct WorkspaceSlideItem {
|
|||||||
struct WorkspaceSlide {
|
struct WorkspaceSlide {
|
||||||
items: Vec<WorkspaceSlideItem>,
|
items: Vec<WorkspaceSlideItem>,
|
||||||
elapsed: Duration,
|
elapsed: Duration,
|
||||||
|
held_progress: Option<f64>,
|
||||||
|
gesture: Option<(WorkspaceId, WorkspaceId, SwipeDirection)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceSlide {
|
impl WorkspaceSlide {
|
||||||
@@ -170,11 +183,33 @@ impl WorkspaceSlide {
|
|||||||
Self {
|
Self {
|
||||||
items,
|
items,
|
||||||
elapsed: WORKSPACE_SLIDE_FIRST_FRAME,
|
elapsed: WORKSPACE_SLIDE_FIRST_FRAME,
|
||||||
|
held_progress: None,
|
||||||
|
gesture: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn progress(&self) -> f64 {
|
fn progress(&self) -> f64 {
|
||||||
smoothstep((self.elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64()).min(1.0))
|
self.held_progress.unwrap_or_else(|| {
|
||||||
|
smoothstep((self.elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64()).min(1.0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release(&mut self, committed: bool) {
|
||||||
|
let progress = self.progress();
|
||||||
|
for item in &mut self.items {
|
||||||
|
let position = item.start.between(item.target, progress);
|
||||||
|
if !committed && let Some((from, to, direction)) = self.gesture {
|
||||||
|
if item.workspace == from {
|
||||||
|
item.target = SlideOffset::default();
|
||||||
|
} else if item.workspace == to {
|
||||||
|
item.target = SlideOffset::for_swipe(direction).opposite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.start = position;
|
||||||
|
}
|
||||||
|
self.held_progress = None;
|
||||||
|
self.gesture = None;
|
||||||
|
self.elapsed = Duration::ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn advance(&mut self, delta: Duration) -> bool {
|
fn advance(&mut self, delta: Duration) -> bool {
|
||||||
@@ -1071,7 +1106,12 @@ impl Ferese {
|
|||||||
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
let Some(output_id) = self.output_workspaces.output_for_workspace(workspace) else {
|
let Some(output_id) = self
|
||||||
|
.workspace_slides
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, slide)| slide.contains(workspace).then_some(*output))
|
||||||
|
.or_else(|| self.output_workspaces.output_for_workspace(workspace))
|
||||||
|
else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
||||||
@@ -1878,6 +1918,9 @@ impl Ferese {
|
|||||||
let mut active_animation = false;
|
let mut active_animation = false;
|
||||||
let mut completed_slides = Vec::new();
|
let mut completed_slides = Vec::new();
|
||||||
for (output, slide) in &mut self.workspace_slides {
|
for (output, slide) in &mut self.workspace_slides {
|
||||||
|
if slide.held_progress.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if slide.advance(delta) {
|
if slide.advance(delta) {
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -2790,10 +2833,6 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn switch_workspace_from_swipe(&mut self, index: u32, direction: SwipeDirection) {
|
|
||||||
self.switch_workspace_internal(index, Some(direction), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn switch_workspace_internal(
|
fn switch_workspace_internal(
|
||||||
&mut self,
|
&mut self,
|
||||||
index: u32,
|
index: u32,
|
||||||
@@ -2833,6 +2872,70 @@ impl Ferese {
|
|||||||
self.restore_keyboard_focus();
|
self.restore_keyboard_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn preview_workspace_swipe(&mut self, next: bool, direction: SwipeDirection, progress: f64) {
|
||||||
|
if !self.animations_enabled || self.overview.is_presenting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some((output, (from, to, _))) = self
|
||||||
|
.workspace_slides
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, slide)| slide.gesture.map(|gesture| (*output, gesture)))
|
||||||
|
{
|
||||||
|
if !workspace_swipe_is_current(&self.output_workspaces, output, from, to) {
|
||||||
|
self.finish_workspace_swipe(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.workspace_slides
|
||||||
|
.get_mut(&output)
|
||||||
|
.expect("gesture output exists")
|
||||||
|
.held_progress = Some(progress);
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.swipe.preview_started() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(from) = self.output_workspaces.active_workspace(output) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(to) = self.relative_workspace_target(next) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let previous = self.workspace_slides.remove(&output);
|
||||||
|
let mut slide = WorkspaceSlide::new(previous, from, to, direction);
|
||||||
|
slide.held_progress = Some(progress);
|
||||||
|
slide.gesture = Some((from, to, direction));
|
||||||
|
self.workspace_slides.insert(output, slide);
|
||||||
|
self.swipe.mark_preview_started();
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_workspace_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
|
||||||
|
let Some((output, (from, to, expected))) = self
|
||||||
|
.workspace_slides
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, slide)| slide.gesture.map(|gesture| (*output, gesture)))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let committed = direction == Some(expected)
|
||||||
|
&& workspace_swipe_is_current(&self.output_workspaces, output, from, to)
|
||||||
|
&& self.activate_managed_workspace_from_swipe(to, expected);
|
||||||
|
if !committed {
|
||||||
|
if let Some(slide) = self.workspace_slides.get_mut(&output) {
|
||||||
|
slide.release(false);
|
||||||
|
}
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn update_workspace_slide(
|
fn update_workspace_slide(
|
||||||
&mut self,
|
&mut self,
|
||||||
requested_output: OutputId,
|
requested_output: OutputId,
|
||||||
@@ -2841,7 +2944,18 @@ impl Ferese {
|
|||||||
workspace: WorkspaceId,
|
workspace: WorkspaceId,
|
||||||
slide_direction: Option<SwipeDirection>,
|
slide_direction: Option<SwipeDirection>,
|
||||||
) {
|
) {
|
||||||
let previous_slide = self.workspace_slides.remove(&owner);
|
let mut previous_slide = self.workspace_slides.remove(&owner);
|
||||||
|
if owner == requested_output
|
||||||
|
&& let Some(slide) = previous_slide.as_mut()
|
||||||
|
&& slide
|
||||||
|
.gesture
|
||||||
|
.is_some_and(|(_, to, direction)| to == workspace && Some(direction) == slide_direction)
|
||||||
|
{
|
||||||
|
slide.release(true);
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
self.workspace_slides.insert(owner, previous_slide.unwrap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
if owner == requested_output
|
if owner == requested_output
|
||||||
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
||||||
&& from != workspace
|
&& from != workspace
|
||||||
@@ -3188,6 +3302,31 @@ impl ClientData for ClientState {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn workspace_drag_cancels_when_focus_or_target_ownership_changes() {
|
||||||
|
let mut outputs = OutputWorkspaceMap::default();
|
||||||
|
let left = OutputId(1);
|
||||||
|
let right = OutputId(2);
|
||||||
|
let from = WorkspaceId(1);
|
||||||
|
let to = WorkspaceId(3);
|
||||||
|
outputs
|
||||||
|
.connect(left, OutputGeometry::new(0, 0, 1000, 1000), from)
|
||||||
|
.unwrap();
|
||||||
|
outputs
|
||||||
|
.connect(right, OutputGeometry::new(1000, 0, 1000, 1000), WorkspaceId(2))
|
||||||
|
.unwrap();
|
||||||
|
assert!(workspace_swipe_is_current(&outputs, left, from, to));
|
||||||
|
outputs.focus_output(right).unwrap();
|
||||||
|
assert!(!workspace_swipe_is_current(&outputs, left, from, to));
|
||||||
|
outputs.focus_output(left).unwrap();
|
||||||
|
outputs.assign_workspace(right, to).unwrap();
|
||||||
|
assert!(!workspace_swipe_is_current(&outputs, left, from, to));
|
||||||
|
assert_eq!(outputs.active_workspace(left), Some(from));
|
||||||
|
assert_eq!(outputs.output_for_workspace(to), Some(right));
|
||||||
|
outputs.switch_workspace(left, WorkspaceId(4)).unwrap();
|
||||||
|
assert!(!workspace_swipe_is_current(&outputs, left, from, WorkspaceId(5)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selecting_a_window_activates_its_workspace_on_its_owner() {
|
fn selecting_a_window_activates_its_workspace_on_its_owner() {
|
||||||
for assignment in [None, Some(OutputId(1)), Some(OutputId(2))] {
|
for assignment in [None, Some(OutputId(1)), Some(OutputId(2))] {
|
||||||
@@ -3251,6 +3390,43 @@ mod tests {
|
|||||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_drag_tracks_progress_and_releases_without_a_jump() {
|
||||||
|
for committed in [false, true] {
|
||||||
|
let from = WorkspaceId(1);
|
||||||
|
let to = WorkspaceId(2);
|
||||||
|
let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Up);
|
||||||
|
slide.gesture = Some((from, to, SwipeDirection::Up));
|
||||||
|
slide.held_progress = Some(0.4);
|
||||||
|
slide.advance(Duration::from_secs(10));
|
||||||
|
assert_eq!(slide.offset(from, 1000.0, 1000.0), (0.0, -400.0));
|
||||||
|
assert_eq!(slide.offset(to, 1000.0, 1000.0), (0.0, 600.0));
|
||||||
|
slide.release(committed);
|
||||||
|
assert_eq!(slide.offset(from, 1000.0, 1000.0), (0.0, -400.0));
|
||||||
|
assert_eq!(slide.offset(to, 1000.0, 1000.0), (0.0, 600.0));
|
||||||
|
assert!(!slide.advance(WORKSPACE_SLIDE_DURATION));
|
||||||
|
let active = if committed { to } else { from };
|
||||||
|
assert_eq!(slide.offset(active, 1000.0, 1000.0), (0.0, 0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cancelling_a_chained_drag_returns_to_the_active_workspace() {
|
||||||
|
let first = WorkspaceId(1);
|
||||||
|
let second = WorkspaceId(2);
|
||||||
|
let third = WorkspaceId(3);
|
||||||
|
let mut previous = WorkspaceSlide::new(None, first, second, SwipeDirection::Up);
|
||||||
|
previous.advance(WORKSPACE_SLIDE_DURATION / 3);
|
||||||
|
let mut slide = WorkspaceSlide::new(Some(previous), second, third, SwipeDirection::Up);
|
||||||
|
slide.gesture = Some((second, third, SwipeDirection::Up));
|
||||||
|
slide.held_progress = Some(0.25);
|
||||||
|
let position = slide.offset(second, 1000.0, 1000.0);
|
||||||
|
slide.release(false);
|
||||||
|
assert_eq!(slide.offset(second, 1000.0, 1000.0), position);
|
||||||
|
slide.advance(WORKSPACE_SLIDE_DURATION);
|
||||||
|
assert_eq!(slide.offset(second, 1000.0, 1000.0), (0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_slide_moves_both_workspaces_without_a_gap() {
|
fn workspace_slide_moves_both_workspaces_without_a_gap() {
|
||||||
let width = 1600.0;
|
let width = 1600.0;
|
||||||
|
|||||||
@@ -255,11 +255,11 @@ The touchpad device keys are booleans, all defaulting to `true`: `tap`,
|
|||||||
`natural-scroll` and `disable-while-typing`. These apply to DRM devices; nested
|
`natural-scroll` and `disable-while-typing`. These apply to DRM devices; nested
|
||||||
previews use the host's physical input settings.
|
previews use the host's physical input settings.
|
||||||
|
|
||||||
Three-finger swipes navigate on release: up goes to the next workspace on the
|
Workspace swipes follow your fingers and settle when released. Up goes to the next workspace on the
|
||||||
current monitor, down to the previous; left focuses the window to the right,
|
current monitor, down to the previous; left focuses the window to the right,
|
||||||
and right focuses the window to the left. Workspace swipes skip workspaces owned
|
and right focuses the window to the left. Workspace swipes skip workspaces owned
|
||||||
by other monitors and stop at the first/last workspace. Cancelled, short and
|
by other monitors and stop at the first/last workspace. Short or cancelled workspace
|
||||||
diagonal swipes do nothing. Navigation pauses while locked, during
|
swipes slide back; diagonal swipes do not navigate. Navigation pauses while locked, during
|
||||||
window grabs, or when an application inhibits shortcuts.
|
window grabs, or when an application inhibits shortcuts.
|
||||||
|
|
||||||
Assign swipes in **Settings → Shortcuts** using the same actions and arguments as
|
Assign swipes in **Settings → Shortcuts** using the same actions and arguments as
|
||||||
|
|||||||
+3
-2
@@ -83,10 +83,11 @@ for dependencies and custom screenshot commands.
|
|||||||
|
|
||||||
| Three-finger swipe | Action on release |
|
| Three-finger swipe | Action on release |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Up / down | Next / previous workspace on this monitor |
|
| Up / down | Drag to the next / previous workspace on this monitor; release to settle |
|
||||||
| Left / right | Focus the window to the right / left |
|
| Left / right | Focus the window to the right / left |
|
||||||
|
|
||||||
Short, diagonal, or cancelled swipes do nothing. Gestures can use the same actions
|
Short or cancelled workspace swipes slide back. Diagonal swipes do not navigate.
|
||||||
|
Gestures can use the same actions
|
||||||
as keyboard bindings, including overview or moving a window. Four- and five-finger
|
as keyboard bindings, including overview or moving a window. Four- and five-finger
|
||||||
bindings are supported too.
|
bindings are supported too.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user