From fae3a276418ef1f6f6baad465f160e33a6ac9d8a Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 25 Apr 2025 10:06:46 +0300 Subject: Implement DnD hold to activate window or workspace --- src/layout/mod.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++-- src/layout/scrolling.rs | 37 ++++++++++++------ src/layout/workspace.rs | 4 +- 3 files changed, 124 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 2ce8662e..6adf3ad7 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -298,7 +298,7 @@ pub struct Layout { /// Ongoing interactive move. interactive_move: Option>, /// Ongoing drag-and-drop operation. - dnd: Option, + dnd: Option>, /// Clock for driving animations. clock: Clock, /// Time that we last updated render elements for. @@ -433,11 +433,26 @@ struct InteractiveMoveData { } #[derive(Debug)] -pub struct DndData { +pub struct DndData { /// Output where the pointer is currently located. output: Output, /// Current pointer position within output. pointer_pos_within_output: Point, + /// Ongoing DnD hold to activate something. + hold: Option>, +} + +#[derive(Debug)] +struct DndHold { + /// Time when we started holding on the target. + start_time: Duration, + target: DndHoldTarget, +} + +#[derive(Debug, PartialEq, Eq)] +enum DndHoldTarget { + Window(WindowId), + Workspace(WorkspaceId), } #[derive(Debug, Clone, Copy)] @@ -2755,8 +2770,10 @@ impl Layout { let _span = tracy_client::span!("Layout::advance_animations"); let mut dnd_scroll = None; + let mut is_dnd = false; if let Some(dnd) = &self.dnd { dnd_scroll = Some((dnd.output.clone(), dnd.pointer_pos_within_output, true)); + is_dnd = true; } if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { @@ -2771,11 +2788,15 @@ impl Layout { } } + let is_overview_open = self.overview_open; + // Scroll the view if needed. if let Some((output, pos_within_output, is_scrolling)) = dnd_scroll { if let Some(mon) = self.monitor_for_output_mut(&output) { + let mut scrolled = false; + let zoom = mon.overview_zoom(); - mon.dnd_scroll_gesture_scroll(pos_within_output, 1. / zoom); + scrolled |= mon.dnd_scroll_gesture_scroll(pos_within_output, 1. / zoom); if is_scrolling { if let Some((ws, geo)) = mon.workspace_under(pos_within_output) { @@ -2788,7 +2809,77 @@ impl Layout { // As far as the DnD scroll gesture is concerned, the workspace spans across // the whole monitor horizontally. let ws_pos = Point::from((0., geo.loc.y)); - ws.dnd_scroll_gesture_scroll(pos_within_output - ws_pos, 1. / zoom); + scrolled |= + ws.dnd_scroll_gesture_scroll(pos_within_output - ws_pos, 1. / zoom); + } + } + + if scrolled { + // Don't trigger DnD hold while scrolling. + if let Some(dnd) = &mut self.dnd { + dnd.hold = None; + } + } else if is_dnd { + let target = mon + .window_under(pos_within_output) + .map(|(win, _)| DndHoldTarget::Window(win.id().clone())) + .or_else(|| { + mon.workspace_under_narrow(pos_within_output) + .map(|ws| DndHoldTarget::Workspace(ws.id())) + }); + + let dnd = self.dnd.as_mut().unwrap(); + if let Some(target) = target { + let now = self.clock.now_unadjusted(); + let start_time = if let Some(hold) = &mut dnd.hold { + if hold.target != target { + hold.start_time = now; + } + hold.target = target; + hold.start_time + } else { + let hold = dnd.hold.insert(DndHold { + start_time: now, + target, + }); + hold.start_time + }; + + // Delay copied from gnome-shell. + let delay = Duration::from_millis(750); + if delay <= now.saturating_sub(start_time) { + let hold = dnd.hold.take().unwrap(); + + // Synchronize workspace switch to overview close to get a monotonic + // animation. + let config = is_overview_open + .then_some(self.options.animations.overview_open_close.0); + + let mon = self.monitor_for_output_mut(&output).unwrap(); + + let ws_idx = match hold.target { + DndHoldTarget::Window(id) => mon + .workspaces + .iter_mut() + .position(|ws| ws.activate_window(&id)) + .unwrap(), + DndHoldTarget::Workspace(id) => { + mon.workspaces.iter().position(|ws| ws.id() == id).unwrap() + } + }; + + mon.dnd_scroll_gesture_end(); + mon.activate_workspace_with_anim_config(ws_idx, config); + + self.focus_output(&output); + + if is_overview_open { + self.close_overview(); + } + } + } else { + // No target, reset the hold timer. + dnd.hold = None; } } } @@ -4459,6 +4550,7 @@ impl Layout { self.dnd = Some(DndData { output, pointer_pos_within_output, + hold: None, }); if begin_gesture { diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 925c6d19..7e9df56d 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -588,6 +588,11 @@ impl ScrollingSpace { return self.compute_new_view_offset_for_column_fit(target_x, idx); }; + // Activating the same column. + if prev_idx == idx { + return self.compute_new_view_offset_for_column_fit(target_x, idx); + } + // Always take the left or right neighbor of the target as the source. let source_idx = if prev_idx > idx { min(idx + 1, self.columns.len() - 1) @@ -719,7 +724,10 @@ impl ScrollingSpace { } fn activate_column_with_anim_config(&mut self, idx: usize, config: niri_config::Animation) { - if self.active_column_idx == idx { + if self.active_column_idx == idx + // During a DnD scroll, animate even when activating the same window, for DnD hold. + && (self.columns.is_empty() || !self.view_offset.is_dnd_scroll()) + { return; } @@ -730,12 +738,14 @@ impl ScrollingSpace { config, ); - self.active_column_idx = idx; + if self.active_column_idx != idx { + self.active_column_idx = idx; - // A different column was activated; reset the flag. - self.activate_prev_column_on_removal = None; - self.view_offset_before_fullscreen = None; - self.interactive_resize = None; + // A different column was activated; reset the flag. + self.activate_prev_column_on_removal = None; + self.view_offset_before_fullscreen = None; + self.interactive_resize = None; + } } pub(super) fn insert_position(&self, pos: Point) -> InsertPosition { @@ -2869,14 +2879,14 @@ impl ScrollingSpace { Some(true) } - pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) { + pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) -> bool { let ViewOffset::Gesture(gesture) = &mut self.view_offset else { - return; + return false; }; let Some(last_time) = gesture.dnd_last_event_time else { // Not a DnD scroll. - return; + return false; }; let config = &self.options.gestures.dnd_edge_view_scroll; @@ -2887,7 +2897,7 @@ impl ScrollingSpace { if delta == 0. { // We're outside the scrolling zone. gesture.dnd_nonzero_start_time = None; - return; + return false; } let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now); @@ -2896,7 +2906,7 @@ impl ScrollingSpace { // monitors. let delay = Duration::from_millis(u64::from(config.delay_ms)); if now.saturating_sub(nonzero_start) < delay { - return; + return true; } let time_delta = now.saturating_sub(last_time).as_secs_f64(); @@ -2940,6 +2950,7 @@ impl ScrollingSpace { gesture.delta_from_tracker += clamped_offset - view_offset; gesture.current_view_offset = clamped_offset; + true } pub fn view_offset_gesture_end(&mut self, is_touchpad: Option) -> bool { @@ -3560,6 +3571,10 @@ impl ViewOffset { matches!(self, Self::Gesture(_)) } + pub fn is_dnd_scroll(&self) -> bool { + matches!(&self, ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some()) + } + pub fn is_animation_ongoing(&self) -> bool { match self { ViewOffset::Static(_) => false, diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index af455f97..58a2d40f 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -1678,7 +1678,7 @@ impl Workspace { self.scrolling.dnd_scroll_gesture_begin(); } - pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point, speed: f64) { + pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point, speed: f64) -> bool { let config = &self.options.gestures.dnd_edge_view_scroll; let trigger_width = config.trigger_width.0; @@ -1706,7 +1706,7 @@ impl Workspace { }; let delta = delta * speed; - self.scrolling.dnd_scroll_gesture_scroll(delta); + self.scrolling.dnd_scroll_gesture_scroll(delta) } pub fn dnd_scroll_gesture_end(&mut self) { -- cgit