Implement scrolling the view during interactive move

This commit is contained in:
Ivan Molodetskikh
2025-02-15 11:15:35 +03:00
parent fd8140e091
commit d7f3ca00c7
4 changed files with 240 additions and 46 deletions
-31
View File
@@ -1,5 +1,3 @@
use std::time::Duration;
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
@@ -17,7 +15,6 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
}
impl MoveGrab {
@@ -26,7 +23,6 @@ impl MoveGrab {
last_location: start_data.location,
start_data,
window,
is_moving: false,
}
}
@@ -64,14 +60,6 @@ impl PointerGrab<State> for MoveGrab {
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
@@ -104,25 +92,6 @@ impl PointerGrab<State> for MoveGrab {
) {
handle.button(data, event);
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// FIXME: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
// When moving with the left button, right toggles floating, and vice versa.
let toggle_floating_button = if self.start_data.button == 0x110 {
0x111
+110 -12
View File
@@ -1087,6 +1087,12 @@ impl<W: LayoutElement> Layout<W> {
else {
unreachable!()
};
// Unlock the view on the workspaces.
for ws in self.workspaces_mut() {
ws.view_offset_gesture_end(false, None);
}
return Some(RemovedTile {
tile: move_.tile,
width: move_.width,
@@ -2372,6 +2378,8 @@ impl<W: LayoutElement> Layout<W> {
assert!(primary_idx < monitors.len());
assert!(active_monitor_idx < monitors.len());
let mut saw_view_offset_gesture = false;
for (idx, monitor) in monitors.iter().enumerate() {
assert!(
!monitor.workspaces.is_empty(),
@@ -2508,6 +2516,28 @@ impl<W: LayoutElement> Layout<W> {
}
workspace.verify_invariants(move_win_id.as_ref());
let has_view_offset_gesture = workspace.scrolling().view_offset().is_gesture();
if self.interactive_move.is_some() {
// We'd like to check that all workspaces have the gesture here, furthermore we
// want to check that they have the gesture only if the interactive move
// targets the scrolling layout. However, we cannot do that because we start
// and stop the gesture lazily. Otherwise the gesture code would pollute a lot
// of places like adding new workspaces, implicitly moving windows between
// floating and tiling on fullscreen, etc.
//
// assert!(
// has_view_offset_gesture,
// "during an interactive move in the scrolling layout, \
// all workspaces should be in a view offset gesture"
// );
} else if saw_view_offset_gesture {
assert!(
!has_view_offset_gesture,
"only one workspace can have an ongoing view offset gesture"
);
}
saw_view_offset_gesture = has_view_offset_gesture;
}
}
}
@@ -2517,6 +2547,23 @@ impl<W: LayoutElement> Layout<W> {
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
move_.tile.advance_animations();
// Scroll the view if needed.
if !move_.is_floating {
let output = move_.output.clone();
let pos_within_output = move_.pointer_pos_within_output;
if let Some(mon) = self.monitor_for_output_mut(&output) {
if let Some((ws, offset)) = mon.workspace_under(pos_within_output) {
let ws_id = ws.id();
let ws = mon
.workspaces
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
ws.dnd_scroll_gesture_scroll(pos_within_output - offset);
}
}
}
}
match &mut self.monitor_set {
@@ -2535,11 +2582,15 @@ impl<W: LayoutElement> Layout<W> {
pub fn are_animations_ongoing(&self, output: Option<&Output>) -> bool {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
#[allow(clippy::collapsible_if)]
if output.map_or(true, |output| *output == move_.output) {
if move_.tile.are_animations_ongoing() {
return true;
}
// Keep advancing animations if we might need to scroll the view.
if !move_.is_floating {
return true;
}
}
}
@@ -2919,6 +2970,7 @@ impl<W: LayoutElement> Layout<W> {
win.request_size_once(size, true);
}
return;
}
}
@@ -3516,6 +3568,7 @@ impl<W: LayoutElement> Layout<W> {
return false;
}
let is_floating = ws.is_floating(&window_id);
let (tile, tile_offset, _visible) = ws
.tiles_with_render_positions()
.find(|(tile, _, _)| tile.window().id() == &window_id)
@@ -3537,6 +3590,13 @@ impl<W: LayoutElement> Layout<W> {
pointer_ratio_within_window,
});
// Lock the view for scrolling interactive move.
if !is_floating {
for ws in self.workspaces_mut() {
ws.dnd_scroll_gesture_begin();
}
}
true
}
@@ -3744,14 +3804,25 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
let tile = self
.workspaces_mut()
.flat_map(|ws| ws.tiles_mut())
.find(|tile| *tile.window().id() == window_id)
.unwrap();
let offset = tile.interactive_move_offset;
tile.interactive_move_offset = Point::from((0., 0.));
tile.animate_move_from(offset);
for ws in self.workspaces_mut() {
if let Some(tile) = ws.tiles_mut().find(|tile| *tile.window().id() == window_id)
{
let offset = tile.interactive_move_offset;
tile.interactive_move_offset = Point::from((0., 0.));
tile.animate_move_from(offset);
}
// Unlock the view on the workspaces, but if the moved window was active,
// preserve that.
let moved_tile_was_active =
ws.active_window().is_some_and(|win| *win.id() == window_id);
ws.view_offset_gesture_end(false, None);
if moved_tile_was_active {
ws.activate_window(&window_id);
}
}
return;
}
@@ -3766,6 +3837,13 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
// Unlock the view on the workspaces.
if !move_.is_floating {
for ws in self.workspaces_mut() {
ws.view_offset_gesture_end(false, None);
}
}
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
@@ -4271,6 +4349,7 @@ impl<W: LayoutElement> Layout<W> {
self.is_active = is_active;
let mut ongoing_scrolling_dnd = None;
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
let win = move_.tile.window_mut();
@@ -4284,6 +4363,16 @@ impl<W: LayoutElement> Layout<W> {
win.send_pending_configure();
win.refresh();
ongoing_scrolling_dnd = Some(!move_.is_floating);
} else if let Some(InteractiveMoveState::Starting { window_id, .. }) =
&self.interactive_move
{
let (_, _, ws) = self
.workspaces()
.find(|(_, _, ws)| ws.has_window(window_id))
.unwrap();
ongoing_scrolling_dnd = Some(!ws.is_floating(window_id));
}
match &mut self.monitor_set {
@@ -4299,9 +4388,18 @@ impl<W: LayoutElement> Layout<W> {
for (ws_idx, ws) in mon.workspaces.iter_mut().enumerate() {
ws.refresh(is_active);
// Cancel the view offset gesture after workspace switches, moves, etc.
if ws_idx != mon.active_workspace_idx {
ws.view_offset_gesture_end(false, None);
if let Some(is_scrolling) = ongoing_scrolling_dnd {
// Lock or unlock the view for scrolling interactive move.
if is_scrolling {
ws.dnd_scroll_gesture_begin();
} else {
ws.view_offset_gesture_end(false, None);
}
} else {
// Cancel the view offset gesture after workspace switches, moves, etc.
if ws_idx != mon.active_workspace_idx {
ws.view_offset_gesture_end(false, None);
}
}
}
}
+102 -3
View File
@@ -123,7 +123,7 @@ struct ColumnData {
}
#[derive(Debug)]
enum ViewOffset {
pub(super) enum ViewOffset {
/// The view offset is static.
Static(f64),
/// The view offset is animating.
@@ -133,7 +133,7 @@ enum ViewOffset {
}
#[derive(Debug)]
struct ViewGesture {
pub(super) struct ViewGesture {
current_view_offset: f64,
tracker: SwipeTracker,
delta_from_tracker: f64,
@@ -141,6 +141,10 @@ struct ViewGesture {
stationary_view_offset: f64,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
// If this gesture is for drag-and-drop scrolling, this is the last event's unadjusted
// timestamp.
dnd_last_event_time: Option<Duration>,
}
#[derive(Debug)]
@@ -324,6 +328,17 @@ impl<W: LayoutElement> ScrollingSpace<W> {
}
}
if let ViewOffset::Gesture(gesture) = &mut self.view_offset {
// Make sure the last event time doesn't go too much out of date (for
// workspaces not under cursor), causing sudden jumps.
//
// This happens after any dnd_scroll_gesture_scroll() calls (in
// Layout::advance_animations()), so it doesn't mess up the time delta there.
if let Some(last_time) = &mut gesture.dnd_last_event_time {
*last_time = self.clock.now_unadjusted();
}
}
for col in &mut self.columns {
col.advance_animations();
}
@@ -2711,10 +2726,34 @@ impl<W: LayoutElement> ScrollingSpace<W> {
delta_from_tracker: self.view_offset.current(),
stationary_view_offset: self.view_offset.stationary(),
is_touchpad,
dnd_last_event_time: None,
};
self.view_offset = ViewOffset::Gesture(gesture);
}
pub fn dnd_scroll_gesture_begin(&mut self) {
if let ViewOffset::Gesture(ViewGesture {
dnd_last_event_time: Some(_),
..
}) = &self.view_offset
{
// Already active.
return;
}
let gesture = ViewGesture {
current_view_offset: self.view_offset.current(),
tracker: SwipeTracker::new(),
delta_from_tracker: self.view_offset.current(),
stationary_view_offset: self.view_offset.stationary(),
is_touchpad: false,
dnd_last_event_time: Some(self.clock.now_unadjusted()),
};
self.view_offset = ViewOffset::Gesture(gesture);
self.interactive_resize = None;
}
pub fn view_offset_gesture_update(
&mut self,
delta_x: f64,
@@ -2725,7 +2764,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
return None;
};
if gesture.is_touchpad != is_touchpad {
if gesture.is_touchpad != is_touchpad || gesture.dnd_last_event_time.is_some() {
return None;
}
@@ -2743,6 +2782,61 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(true)
}
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) {
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
return;
};
let Some(last_time) = gesture.dnd_last_event_time else {
// Not a DnD scroll.
return;
};
let now = self.clock.now_unadjusted();
gesture.dnd_last_event_time = Some(now);
let time_delta = now.saturating_sub(last_time).as_secs_f64();
let delta = delta * time_delta * 50.;
gesture.tracker.push(delta, now);
let view_offset = gesture.tracker.pos() + gesture.delta_from_tracker;
// Clamp it so that it doesn't go too much out of bounds.
let (leftmost, rightmost) = if self.columns.is_empty() {
(0., 0.)
} else {
let gaps = self.options.gaps;
let mut leftmost = -self.working_area.size.w;
let last_col_idx = self.columns.len() - 1;
let last_col_x = self
.columns
.iter()
.take(last_col_idx)
.fold(0., |col_x, col| col_x + col.width() + gaps);
let last_col_width = self.data[last_col_idx].width;
let mut rightmost = last_col_x + last_col_width - self.working_area.loc.x;
let active_col_x = self
.columns
.iter()
.take(self.active_column_idx)
.fold(0., |col_x, col| col_x + col.width() + gaps);
leftmost -= active_col_x;
rightmost -= active_col_x;
(leftmost, rightmost)
};
let min_offset = f64::min(leftmost, rightmost);
let max_offset = f64::max(leftmost, rightmost);
let clamped_offset = view_offset.clamp(min_offset, max_offset);
gesture.delta_from_tracker += clamped_offset - view_offset;
gesture.current_view_offset = clamped_offset;
}
pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool {
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
return false;
@@ -3228,6 +3322,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
self.active_column_idx
}
#[cfg(test)]
pub(super) fn view_offset(&self) -> &ViewOffset {
&self.view_offset
}
#[cfg(test)]
pub fn verify_invariants(&self, working_area: Rectangle<f64, Logical>) {
assert!(self.view_size.w > 0.);
+28
View File
@@ -1609,6 +1609,34 @@ impl<W: LayoutElement> Workspace<W> {
.view_offset_gesture_end(cancelled, is_touchpad)
}
pub fn dnd_scroll_gesture_begin(&mut self) {
self.scrolling.dnd_scroll_gesture_begin();
}
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>) {
// Taken from GTK 4.
const SCROLL_EDGE_SIZE: f64 = 30.;
// This working area intentionally does not include extra struts from Options.
let x = pos.x - self.working_area.loc.x;
let width = self.working_area.size.w;
let x = x.clamp(0., width);
let delta = if x < SCROLL_EDGE_SIZE {
-(SCROLL_EDGE_SIZE - x)
} else if width - x < SCROLL_EDGE_SIZE {
SCROLL_EDGE_SIZE - (width - x)
} else {
0.
};
self.scrolling.dnd_scroll_gesture_scroll(delta);
}
pub fn dnd_scroll_gesture_update_time(&mut self) {
self.scrolling.dnd_scroll_gesture_scroll(0.);
}
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
if self.floating.has_window(&window) {
self.floating.interactive_resize_begin(window, edges)