aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-04-25 10:06:46 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-04-25 02:00:18 -0700
commitfae3a276418ef1f6f6baad465f160e33a6ac9d8a (patch)
tree99fa9626e829e68b5b50f8fa78da459bf7669956 /src/layout
parent31e76cf451eaf8c5f2bc139b3867f2153e72ee1d (diff)
downloadniri-fae3a276418ef1f6f6baad465f160e33a6ac9d8a.tar.gz
niri-fae3a276418ef1f6f6baad465f160e33a6ac9d8a.tar.bz2
niri-fae3a276418ef1f6f6baad465f160e33a6ac9d8a.zip
Implement DnD hold to activate window or workspace
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs100
-rw-r--r--src/layout/scrolling.rs37
-rw-r--r--src/layout/workspace.rs4
3 files changed, 124 insertions, 17 deletions
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<W: LayoutElement> {
/// Ongoing interactive move.
interactive_move: Option<InteractiveMoveState<W>>,
/// Ongoing drag-and-drop operation.
- dnd: Option<DndData>,
+ dnd: Option<DndData<W>>,
/// Clock for driving animations.
clock: Clock,
/// Time that we last updated render elements for.
@@ -433,11 +433,26 @@ struct InteractiveMoveData<W: LayoutElement> {
}
#[derive(Debug)]
-pub struct DndData {
+pub struct DndData<W: LayoutElement> {
/// Output where the pointer is currently located.
output: Output,
/// Current pointer position within output.
pointer_pos_within_output: Point<f64, Logical>,
+ /// Ongoing DnD hold to activate something.
+ hold: Option<DndHold<W>>,
+}
+
+#[derive(Debug)]
+struct DndHold<W: LayoutElement> {
+ /// Time when we started holding on the target.
+ start_time: Duration,
+ target: DndHoldTarget<W::Id>,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+enum DndHoldTarget<WindowId> {
+ Window(WindowId),
+ Workspace(WorkspaceId),
}
#[derive(Debug, Clone, Copy)]
@@ -2755,8 +2770,10 @@ impl<W: LayoutElement> Layout<W> {
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<W: LayoutElement> Layout<W> {
}
}
+ 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<W: LayoutElement> Layout<W> {
// 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<W: LayoutElement> Layout<W> {
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<W: LayoutElement> ScrollingSpace<W> {
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<W: LayoutElement> ScrollingSpace<W> {
}
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<W: LayoutElement> ScrollingSpace<W> {
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<f64, Logical>) -> InsertPosition {
@@ -2869,14 +2879,14 @@ impl<W: LayoutElement> ScrollingSpace<W> {
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<W: LayoutElement> ScrollingSpace<W> {
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<W: LayoutElement> ScrollingSpace<W> {
// 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<W: LayoutElement> ScrollingSpace<W> {
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>) -> 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<W: LayoutElement> Workspace<W> {
self.scrolling.dnd_scroll_gesture_begin();
}
- pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) {
+ pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
let config = &self.options.gestures.dnd_edge_view_scroll;
let trigger_width = config.trigger_width.0;
@@ -1706,7 +1706,7 @@ impl<W: LayoutElement> Workspace<W> {
};
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) {