Make horizontal focus swipes follow touchpad motion
This commit is contained in:
1 parent
b29d271252
commit
a14f222119
4 files changed
+208
-7
No files matched your search
+14
-2
@@ -160,6 +160,7 @@ impl Ferese {
|
||||
InputEvent::GestureSwipeBegin { event } => {
|
||||
let pointer = seat.get_pointer().expect("seat has a pointer");
|
||||
self.finish_workspace_swipe(None);
|
||||
self.finish_focus_swipe(None);
|
||||
self.swipe.begin(
|
||||
event.fingers(),
|
||||
self.swipe_navigation_blocked()
|
||||
@@ -187,15 +188,24 @@ impl Ferese {
|
||||
self.swipe.update(event.delta_x(), event.delta_y());
|
||||
if self.swipe_navigation_blocked() {
|
||||
self.finish_workspace_swipe(None);
|
||||
self.finish_focus_swipe(None);
|
||||
} else if let Some((direction, progress)) = self.swipe.preview()
|
||||
&& let Some(BindingAction::SwitchRelativeWorkspace(next)) = self
|
||||
&& let Some(action) = self
|
||||
.bindings
|
||||
.iter()
|
||||
.find(|binding| binding.matches_swipe(self.swipe.fingers(), direction))
|
||||
.map(|binding| binding.action.clone())
|
||||
{
|
||||
match action {
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
self.preview_workspace_swipe(next, direction, progress);
|
||||
}
|
||||
BindingAction::Focus(focus @ (Direction::Left | Direction::Right)) => {
|
||||
self.preview_focus_swipe(focus, direction, progress);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
||||
self,
|
||||
@@ -211,7 +221,9 @@ impl Ferese {
|
||||
let fingers = self.swipe.fingers();
|
||||
let cancelled = event.cancelled() || self.swipe_navigation_blocked();
|
||||
let direction = self.swipe.finish(cancelled);
|
||||
let handled = self.finish_workspace_swipe(direction) || self.swipe.preview_started();
|
||||
let workspace_handled = self.finish_workspace_swipe(direction);
|
||||
let focus_handled = self.finish_focus_swipe(direction);
|
||||
let handled = workspace_handled || focus_handled || self.swipe.preview_started();
|
||||
if !handled
|
||||
&& let Some(direction) = direction
|
||||
&& let Some(action) = self
|
||||
|
||||
+189
-1
@@ -131,6 +131,35 @@ impl SlideOffset {
|
||||
}
|
||||
}
|
||||
|
||||
struct FocusSwipe {
|
||||
workspace: WorkspaceId,
|
||||
from: WindowId,
|
||||
to: WindowId,
|
||||
direction: Direction,
|
||||
gesture_direction: SwipeDirection,
|
||||
start: f64,
|
||||
destination: f64,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
impl FocusSwipe {
|
||||
fn position(&self) -> f64 {
|
||||
let distance = self.destination - self.start;
|
||||
// When both columns already fit, give bounded drag feedback and settle
|
||||
// back to the unchanged viewport after selecting the next window.
|
||||
let distance = if distance.abs() < 0.001 {
|
||||
if self.direction == Direction::Right {
|
||||
64.0
|
||||
} else {
|
||||
-64.0
|
||||
}
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
self.start + distance * self.progress
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkspaceSlideItem {
|
||||
workspace: WorkspaceId,
|
||||
start: SlideOffset,
|
||||
@@ -308,6 +337,7 @@ pub struct Ferese {
|
||||
pub(crate) backdrop_generation: u64,
|
||||
closing_windows: HashMap<WindowId, ClosingAnimation>,
|
||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||
focus_swipe: Option<FocusSwipe>,
|
||||
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
|
||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
||||
viewport_coupled_widths: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
||||
@@ -537,6 +567,7 @@ impl Ferese {
|
||||
backdrop_generation: 0,
|
||||
closing_windows: HashMap::new(),
|
||||
viewport_animations: HashMap::new(),
|
||||
focus_swipe: None,
|
||||
workspace_slides: HashMap::new(),
|
||||
scrolling_world_x: HashMap::new(),
|
||||
viewport_coupled_widths: HashMap::new(),
|
||||
@@ -1960,6 +1991,13 @@ impl Ferese {
|
||||
|
||||
fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
|
||||
let delta = delta.mul_f64(self.animation_speed);
|
||||
if self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe))
|
||||
{
|
||||
self.focus_swipe = None;
|
||||
}
|
||||
let visible_workspaces = if self.viewport_animations.is_empty() {
|
||||
HashSet::new()
|
||||
} else {
|
||||
@@ -2112,7 +2150,10 @@ impl Ferese {
|
||||
if blocked_workspaces.contains(workspace) {
|
||||
continue;
|
||||
}
|
||||
if self.animations_enabled {
|
||||
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
||||
viewport.current = swipe.position();
|
||||
viewport.velocity = 0.0;
|
||||
} else if self.animations_enabled {
|
||||
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||
} else {
|
||||
viewport.snap();
|
||||
@@ -2419,6 +2460,103 @@ impl Ferese {
|
||||
self.focus_direction_with_slide(direction, false);
|
||||
}
|
||||
|
||||
fn focus_swipe_is_current(&self, swipe: &FocusSwipe) -> bool {
|
||||
self.animations_enabled
|
||||
&& !self.session_lock.active
|
||||
&& !self.overview.is_presenting()
|
||||
&& self.workspaces.active_id() == swipe.workspace
|
||||
&& self.focused_window == Some(swipe.from)
|
||||
&& self.workspaces.workspace(swipe.workspace).is_some_and(|workspace| {
|
||||
workspace.fullscreen.is_none()
|
||||
&& workspace.layout.contains(swipe.from)
|
||||
&& workspace.layout.contains(swipe.to)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn preview_focus_swipe(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
gesture_direction: SwipeDirection,
|
||||
progress: f64,
|
||||
) {
|
||||
if self.focus_swipe.is_none() {
|
||||
if self.swipe.preview_started() || !self.animations_enabled || self.overview.is_presenting() {
|
||||
return;
|
||||
}
|
||||
let Some(from) = self.focused_window else { return };
|
||||
let Some(bounds) = self.output_bounds() else { return };
|
||||
let workspace = self.workspaces.active();
|
||||
if workspace.fullscreen.is_some() || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_)) {
|
||||
return;
|
||||
}
|
||||
let Ok(Some(to)) = workspace.layout.directional_neighbor(from, direction, bounds) else {
|
||||
return;
|
||||
};
|
||||
let workspace_id = workspace.id;
|
||||
let start = self
|
||||
.viewport_animations
|
||||
.get(&workspace_id)
|
||||
.map(|viewport| viewport.current)
|
||||
.or_else(|| workspace.layout.viewport_x())
|
||||
.unwrap_or(0.0);
|
||||
let mut candidate = workspace.layout.clone();
|
||||
if let WorkspaceLayout::Scrolling(layout) = &mut candidate
|
||||
&& layout.slide_focus_from(from, to).is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if candidate
|
||||
.geometry_with_constraints(bounds, self.gap_config, &self.window_constraints(), Some(to))
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.focus_swipe = Some(FocusSwipe {
|
||||
workspace: workspace_id,
|
||||
from,
|
||||
to,
|
||||
direction,
|
||||
gesture_direction,
|
||||
start,
|
||||
destination: candidate.viewport_x().unwrap_or(start),
|
||||
progress,
|
||||
});
|
||||
self.swipe.mark_preview_started();
|
||||
}
|
||||
if let Some(swipe) = &mut self.focus_swipe {
|
||||
swipe.progress = progress;
|
||||
}
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
self.cursor_redraw_pending = true;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
|
||||
let Some(swipe) = self.focus_swipe.take() else {
|
||||
return false;
|
||||
};
|
||||
let current = self.focus_swipe_is_current(&swipe);
|
||||
// Release from the last input position even if no frame rendered that update.
|
||||
if current && let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace) {
|
||||
viewport.current = swipe.position();
|
||||
viewport.velocity = 0.0;
|
||||
}
|
||||
let neighbor = self.output_bounds().and_then(|bounds| {
|
||||
self.workspaces
|
||||
.active()
|
||||
.layout
|
||||
.directional_neighbor(swipe.from, swipe.direction, bounds)
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
self.last_animation_tick = Instant::now();
|
||||
if current && direction == Some(swipe.gesture_direction) && neighbor == Some(swipe.to) {
|
||||
self.focus_direction_from_swipe(swipe.direction);
|
||||
} else {
|
||||
self.relayout();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn focus_direction_from_swipe(&mut self, direction: Direction) {
|
||||
self.focus_direction_with_slide(direction, true);
|
||||
}
|
||||
@@ -3677,6 +3815,56 @@ mod tests {
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_drag_reverses_and_settles_from_the_release_position() {
|
||||
for (direction, gesture_direction, destination) in [
|
||||
(Direction::Right, SwipeDirection::Left, 900.0),
|
||||
(Direction::Left, SwipeDirection::Right, 100.0),
|
||||
] {
|
||||
for committed in [false, true] {
|
||||
let mut swipe = FocusSwipe {
|
||||
workspace: WorkspaceId(1),
|
||||
from: WindowId(1),
|
||||
to: WindowId(2),
|
||||
direction,
|
||||
gesture_direction,
|
||||
start: 500.0,
|
||||
destination,
|
||||
progress: 0.75,
|
||||
};
|
||||
let far = swipe.position();
|
||||
swipe.progress = 0.25;
|
||||
let near = swipe.position();
|
||||
assert!((near - swipe.start).abs() < (far - swipe.start).abs());
|
||||
let mut viewport = AnimatedValue::new(near);
|
||||
viewport.set_target(if committed { destination } else { swipe.start });
|
||||
assert_eq!(viewport.current, near);
|
||||
for _ in 0..300 {
|
||||
viewport.advance(Duration::from_millis(16), SpringConfig::default());
|
||||
}
|
||||
assert_eq!(viewport.current, viewport.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fully_visible_columns_still_give_bounded_horizontal_drag_feedback() {
|
||||
for (direction, sign) in [(Direction::Right, 1.0), (Direction::Left, -1.0)] {
|
||||
let swipe = FocusSwipe {
|
||||
workspace: WorkspaceId(1),
|
||||
from: WindowId(1),
|
||||
to: WindowId(2),
|
||||
direction,
|
||||
gesture_direction: SwipeDirection::Left,
|
||||
start: 0.0,
|
||||
destination: 0.0,
|
||||
progress: 0.5,
|
||||
};
|
||||
assert_eq!(swipe.position(), sign * 32.0);
|
||||
assert_eq!(swipe.destination, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_drag_tracks_progress_and_releases_without_a_jump() {
|
||||
for committed in [false, true] {
|
||||
|
||||
@@ -259,10 +259,11 @@ The touchpad device keys are booleans, all defaulting to `true`: `tap`,
|
||||
`natural-scroll` and `disable-while-typing`. These apply to DRM devices; nested
|
||||
previews use the host's physical input settings.
|
||||
|
||||
Workspace swipes follow your fingers and settle when released. Up goes to the next workspace on the
|
||||
Workspace swipes and left/right window-focus 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,
|
||||
and right focuses the window to the left. Workspace swipes skip workspaces owned
|
||||
by other monitors and stop at the first/last workspace. Short or cancelled workspace
|
||||
by other monitors and stop at the first/last workspace. Short or cancelled navigation
|
||||
swipes slide back; diagonal swipes do not navigate. Navigation pauses while locked, during
|
||||
window grabs, or when an application inhibits shortcuts.
|
||||
|
||||
|
||||
+2
-2
@@ -87,9 +87,9 @@ for dependencies and custom screenshot commands.
|
||||
| Three-finger swipe | Action on release |
|
||||
| --- | --- |
|
||||
| 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 | Drag the window row; release to focus the window to the right / left |
|
||||
|
||||
Short or cancelled workspace swipes slide back. Diagonal swipes do not navigate.
|
||||
Short or cancelled navigation 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
|
||||
bindings are supported too.
|
||||
|
||||
Reference in new issue
Block a user