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)]
|
||||
#[repr(u32)]
|
||||
@@ -14,6 +14,8 @@ pub(crate) struct Swipe {
|
||||
delta: Option<(f64, f64)>,
|
||||
threshold: f64,
|
||||
fingers: u32,
|
||||
direction: Option<SwipeDirection>,
|
||||
preview_started: bool,
|
||||
}
|
||||
|
||||
impl Swipe {
|
||||
@@ -21,6 +23,8 @@ impl Swipe {
|
||||
self.delta = ((3..=5).contains(&fingers) && !blocked).then_some((0.0, 0.0));
|
||||
self.threshold = f64::from(threshold);
|
||||
self.fingers = fingers;
|
||||
self.direction = None;
|
||||
self.preview_started = false;
|
||||
}
|
||||
|
||||
pub fn fingers(&self) -> u32 {
|
||||
@@ -35,30 +39,63 @@ impl Swipe {
|
||||
if let Some((x, y)) = &mut self.delta {
|
||||
*x += dx;
|
||||
*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> {
|
||||
let (x, y) = self.delta.take()?;
|
||||
if cancelled || !x.is_finite() || !y.is_finite() {
|
||||
if cancelled {
|
||||
return None;
|
||||
}
|
||||
const DOMINANCE: f64 = 1.25;
|
||||
if x.abs() >= self.threshold && x.abs() > y.abs() * DOMINANCE {
|
||||
Some(if x < 0.0 {
|
||||
SwipeDirection::Left
|
||||
} else {
|
||||
SwipeDirection::Right
|
||||
})
|
||||
} else if y.abs() >= self.threshold && y.abs() > x.abs() * DOMINANCE {
|
||||
Some(if y < 0.0 {
|
||||
SwipeDirection::Up
|
||||
} else {
|
||||
SwipeDirection::Down
|
||||
})
|
||||
dominant_direction(x, y, self.threshold).filter(|direction| Some(*direction) == self.direction)
|
||||
}
|
||||
}
|
||||
|
||||
fn dominant_direction(x: f64, y: f64, threshold: f64) -> Option<SwipeDirection> {
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
const DOMINANCE: f64 = 1.25;
|
||||
if x.abs() >= threshold && x.abs() > y.abs() * DOMINANCE {
|
||||
Some(if x < 0.0 {
|
||||
SwipeDirection::Left
|
||||
} 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 {
|
||||
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]
|
||||
fn a_swipe_accumulates_motion_and_commits_only_once_on_release() {
|
||||
let mut swipe = Swipe::default();
|
||||
|
||||
+38
-30
@@ -160,6 +160,7 @@ impl Ferese {
|
||||
match event {
|
||||
InputEvent::GestureSwipeBegin { event } => {
|
||||
let pointer = seat.get_pointer().expect("seat has a pointer");
|
||||
self.finish_workspace_swipe(None);
|
||||
self.swipe.begin(
|
||||
event.fingers(),
|
||||
self.swipe_navigation_blocked()
|
||||
@@ -185,6 +186,17 @@ impl Ferese {
|
||||
InputEvent::GestureSwipeUpdate { event } => {
|
||||
if self.swipe.active() {
|
||||
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 {
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
||||
self,
|
||||
@@ -199,7 +211,10 @@ impl Ferese {
|
||||
if self.swipe.active() {
|
||||
let fingers = self.swipe.fingers();
|
||||
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
|
||||
.bindings
|
||||
.iter()
|
||||
@@ -721,55 +736,48 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
|
||||
let Some(output) = self.output_workspaces.focused_output() else {
|
||||
return;
|
||||
};
|
||||
let Some(current) = self.output_workspaces.active_workspace(output) else {
|
||||
return;
|
||||
};
|
||||
if let Some(workspace) = self.relative_workspace_target(next) {
|
||||
if let Some(direction) = slide {
|
||||
self.activate_managed_workspace_from_swipe(workspace, direction);
|
||||
} else {
|
||||
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();
|
||||
candidates.retain(|workspace| {
|
||||
self.output_workspaces
|
||||
.output_for_workspace(workspace.id)
|
||||
.is_none_or(|owner| owner == output)
|
||||
});
|
||||
let Some(index) = candidates.iter().position(|workspace| workspace.id == current) else {
|
||||
return;
|
||||
};
|
||||
let index = if next {
|
||||
let index = candidates.iter().position(|workspace| workspace.id == current)?;
|
||||
let neighbor = if next {
|
||||
index.checked_add(1)
|
||||
} else {
|
||||
index.checked_sub(1)
|
||||
};
|
||||
if let Some(workspace) = index
|
||||
.and_then(|index| candidates.get(index))
|
||||
.map(|workspace| workspace.id)
|
||||
{
|
||||
if let Some(direction) = slide {
|
||||
self.activate_managed_workspace_from_swipe(workspace, direction);
|
||||
} else {
|
||||
self.activate_managed_workspace(workspace);
|
||||
}
|
||||
} else if next
|
||||
if let Some(workspace) = neighbor.and_then(|index| candidates.get(index)) {
|
||||
return Some(workspace.id);
|
||||
}
|
||||
if next
|
||||
&& self.workspaces.workspace(current).is_some_and(|workspace| {
|
||||
workspace.layout.window_ids().next().is_some() || !workspace.floating.is_empty()
|
||||
})
|
||||
{
|
||||
let next_number = self
|
||||
let number = self
|
||||
.workspaces
|
||||
.iter()
|
||||
.filter_map(|workspace| workspace.name.parse::<u32>().ok())
|
||||
.max()
|
||||
.unwrap_or(1)
|
||||
.checked_add(1);
|
||||
if let Some(number) = next_number {
|
||||
if let Some(direction) = slide {
|
||||
self.switch_workspace_from_swipe(number, direction);
|
||||
} else {
|
||||
self.switch_workspace(number);
|
||||
}
|
||||
}
|
||||
.checked_add(1)?;
|
||||
return self.workspaces.ensure_numeric(number).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
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_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)]
|
||||
struct SlideOffset {
|
||||
x: f64,
|
||||
@@ -129,6 +140,8 @@ struct WorkspaceSlideItem {
|
||||
struct WorkspaceSlide {
|
||||
items: Vec<WorkspaceSlideItem>,
|
||||
elapsed: Duration,
|
||||
held_progress: Option<f64>,
|
||||
gesture: Option<(WorkspaceId, WorkspaceId, SwipeDirection)>,
|
||||
}
|
||||
|
||||
impl WorkspaceSlide {
|
||||
@@ -170,11 +183,33 @@ impl WorkspaceSlide {
|
||||
Self {
|
||||
items,
|
||||
elapsed: WORKSPACE_SLIDE_FIRST_FRAME,
|
||||
held_progress: None,
|
||||
gesture: None,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -1071,7 +1106,12 @@ impl Ferese {
|
||||
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
||||
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);
|
||||
};
|
||||
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
||||
@@ -1878,6 +1918,9 @@ impl Ferese {
|
||||
let mut active_animation = false;
|
||||
let mut completed_slides = Vec::new();
|
||||
for (output, slide) in &mut self.workspace_slides {
|
||||
if slide.held_progress.is_some() {
|
||||
continue;
|
||||
}
|
||||
if slide.advance(delta) {
|
||||
active_animation = true;
|
||||
} 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(
|
||||
&mut self,
|
||||
index: u32,
|
||||
@@ -2833,6 +2872,70 @@ impl Ferese {
|
||||
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(
|
||||
&mut self,
|
||||
requested_output: OutputId,
|
||||
@@ -2841,7 +2944,18 @@ impl Ferese {
|
||||
workspace: WorkspaceId,
|
||||
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
|
||||
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
||||
&& from != workspace
|
||||
@@ -3188,6 +3302,31 @@ impl ClientData for ClientState {
|
||||
|
||||
#[cfg(test)]
|
||||
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]
|
||||
fn selecting_a_window_activates_its_workspace_on_its_owner() {
|
||||
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)));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn workspace_slide_moves_both_workspaces_without_a_gap() {
|
||||
let width = 1600.0;
|
||||
|
||||
Reference in new issue
Block a user