aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-02-15 11:15:35 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-02-15 13:28:57 +0300
commitd7f3ca00c70b264032025c262b3c0b10e67c04be (patch)
tree6482ce869331d5befa38dfed24efb34c285d4a17 /src/layout
parentfd8140e091df24e02de279b287d42b087eab19e2 (diff)
downloadniri-d7f3ca00c70b264032025c262b3c0b10e67c04be.tar.gz
niri-d7f3ca00c70b264032025c262b3c0b10e67c04be.tar.bz2
niri-d7f3ca00c70b264032025c262b3c0b10e67c04be.zip
Implement scrolling the view during interactive move
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs122
-rw-r--r--src/layout/scrolling.rs105
-rw-r--r--src/layout/workspace.rs28
3 files changed, 240 insertions, 15 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index baa22285..739709f4 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -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);
+ }
}
}
}
diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs
index 81f01559..03eb1552 100644
--- a/src/layout/scrolling.rs
+++ b/src/layout/scrolling.rs
@@ -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;
@@ -3229,6 +3323,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
}
#[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.);
assert!(self.view_size.h > 0.);
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index fa2c906a..8c8753c5 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -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)