diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/move_grab.rs | 31 | ||||
| -rw-r--r-- | src/layout/mod.rs | 122 | ||||
| -rw-r--r-- | src/layout/scrolling.rs | 105 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 28 |
4 files changed, 240 insertions, 46 deletions
diff --git a/src/input/move_grab.rs b/src/input/move_grab.rs index 509a609b..96362b45 100644 --- a/src/input/move_grab.rs +++ b/src/input/move_grab.rs @@ -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 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) |
