overview: Add DnD up/down scrolling

This commit is contained in:
Ivan Molodetskikh
2025-04-25 10:02:31 +03:00
parent b8a9be542f
commit 31e76cf451
3 changed files with 354 additions and 46 deletions
+31
View File
@@ -1208,6 +1208,8 @@ pub struct SpringParams {
pub struct Gestures {
#[knuffel(child, default)]
pub dnd_edge_view_scroll: DndEdgeViewScroll,
#[knuffel(child, default)]
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
@@ -1230,6 +1232,26 @@ impl Default for DndEdgeViewScroll {
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeWorkspaceSwitch {
#[knuffel(child, unwrap(argument), default = Self::default().trigger_height)]
pub trigger_height: FloatOrInt<0, 65535>,
#[knuffel(child, unwrap(argument), default = Self::default().delay_ms)]
pub delay_ms: u16,
#[knuffel(child, unwrap(argument), default = Self::default().max_speed)]
pub max_speed: FloatOrInt<0, 1_000_000>,
}
impl Default for DndEdgeWorkspaceSwitch {
fn default() -> Self {
Self {
trigger_height: FloatOrInt(50.),
delay_ms: 100,
max_speed: FloatOrInt(1500.),
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Overview {
#[knuffel(child, unwrap(argument), default = Self::default().zoom)]
@@ -4556,6 +4578,15 @@ mod tests {
50.0,
),
},
dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch {
trigger_height: FloatOrInt(
50.0,
),
delay_ms: 100,
max_speed: FloatOrInt(
1500.0,
),
},
},
overview: Overview {
zoom: FloatOrInt(
+66 -15
View File
@@ -1177,6 +1177,12 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
mon.dnd_scroll_gesture_end();
}
}
// Unlock the view on the workspaces.
for ws in self.workspaces_mut() {
ws.dnd_scroll_gesture_end();
@@ -2750,33 +2756,40 @@ impl<W: LayoutElement> Layout<W> {
let mut dnd_scroll = None;
if let Some(dnd) = &self.dnd {
dnd_scroll = Some((dnd.output.clone(), dnd.pointer_pos_within_output));
dnd_scroll = Some((dnd.output.clone(), dnd.pointer_pos_within_output, true));
}
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
move_.tile.advance_animations();
if !move_.is_floating && dnd_scroll.is_none() {
dnd_scroll = Some((move_.output.clone(), move_.pointer_pos_within_output));
if dnd_scroll.is_none() {
dnd_scroll = Some((
move_.output.clone(),
move_.pointer_pos_within_output,
!move_.is_floating,
));
}
}
// Scroll the view if needed.
if let Some((output, pos_within_output)) = dnd_scroll {
if let Some((output, pos_within_output, is_scrolling)) = dnd_scroll {
if let Some(mon) = self.monitor_for_output_mut(&output) {
let zoom = mon.overview_zoom();
mon.dnd_scroll_gesture_scroll(pos_within_output, 1. / zoom);
if let Some((ws, geo)) = mon.workspace_under(pos_within_output) {
let ws_id = ws.id();
let ws = mon
.workspaces
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
// 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);
if is_scrolling {
if let Some((ws, geo)) = mon.workspace_under(pos_within_output) {
let ws_id = ws.id();
let ws = mon
.workspaces
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
// 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);
}
}
}
}
@@ -3948,6 +3961,12 @@ impl<W: LayoutElement> Layout<W> {
pointer_ratio_within_window,
});
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
mon.dnd_scroll_gesture_begin();
}
}
// Lock the view for scrolling interactive move.
if !is_floating {
for ws in self.workspaces_mut() {
@@ -4181,6 +4200,12 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
mon.dnd_scroll_gesture_end();
}
}
let mut ws_id = None;
for ws in self.workspaces_mut() {
let id = ws.id();
@@ -4244,6 +4269,12 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
mon.dnd_scroll_gesture_end();
}
}
// Unlock the view on the workspaces.
if !move_.is_floating {
for ws in self.workspaces_mut() {
@@ -4431,6 +4462,12 @@ impl<W: LayoutElement> Layout<W> {
});
if begin_gesture {
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
mon.dnd_scroll_gesture_begin();
}
}
for ws in self.workspaces_mut() {
ws.dnd_scroll_gesture_begin();
}
@@ -4444,6 +4481,12 @@ impl<W: LayoutElement> Layout<W> {
self.dnd = None;
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
mon.dnd_scroll_gesture_end();
}
}
for ws in self.workspaces_mut() {
ws.dnd_scroll_gesture_end();
}
@@ -4912,6 +4955,14 @@ impl<W: LayoutElement> Layout<W> {
let is_active = self.is_active
&& idx == *active_monitor_idx
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
if ongoing_scrolling_dnd.is_some() && self.overview_open {
// Begin the scroll on new monitors and when opening the overview.
mon.dnd_scroll_gesture_begin();
} else if !self.overview_open {
mon.dnd_scroll_gesture_end();
}
for (ws_idx, ws) in mon.workspaces.iter_mut().enumerate() {
ws.refresh(is_active);
+257 -31
View File
@@ -38,6 +38,11 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
limit: 0.05,
};
/// Amount of DnD edge scrolling to scroll the height of one workspace.
///
/// This constant is tied to the default dnd-edge-workspace-switch max-speed setting.
const WORKSPACE_DND_EDGE_SCROLL_MOVEMENT: f64 = 1500.;
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
@@ -96,11 +101,23 @@ pub struct WorkspaceSwitchGesture {
start_idx: f64,
/// Current, fractional workspace index.
pub(super) current_idx: f64,
/// Animation for the extra offset to the current position.
///
/// For example, if there's a workspace switch during a DnD scroll.
animation: Option<Animation>,
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
/// Whether the gesture is clamped to +-1 workspace around the center.
is_clamped: bool,
// If this gesture is for drag-and-drop scrolling, this is the last event's unadjusted
// timestamp.
dnd_last_event_time: Option<Duration>,
// Time when the drag-and-drop scroll delta became non-zero, used for debouncing.
//
// If `None` then the scroll delta is currently zero.
dnd_nonzero_start_time: Option<Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -161,7 +178,9 @@ impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 {
match self {
WorkspaceSwitch::Animation(anim) => anim.value(),
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
WorkspaceSwitch::Gesture(gesture) => {
gesture.current_idx + gesture.animation.as_ref().map_or(0., |anim| anim.value())
}
}
}
@@ -187,12 +206,11 @@ impl WorkspaceSwitch {
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
#[must_use]
fn is_animation(&self) -> bool {
matches!(self, Self::Animation(..))
fn is_animation_ongoing(&self) -> bool {
match self {
WorkspaceSwitch::Animation(_) => true,
WorkspaceSwitch::Gesture(gesture) => gesture.animation.is_some(),
}
}
}
@@ -206,6 +224,11 @@ impl WorkspaceSwitchGesture {
(0., (workspace_count - 1) as f64)
}
}
fn animate_from(&mut self, from: f64, clock: Clock, config: niri_config::Animation) {
let current = self.animation.as_ref().map_or(0., Animation::value);
self.animation = Some(Animation::new(clock, from + current, 0., 0., config));
}
}
impl OverviewProgress {
@@ -344,25 +367,47 @@ impl<W: LayoutElement> Monitor<W> {
idx: usize,
config: Option<niri_config::Animation>,
) {
if self.active_workspace_idx == idx {
return;
}
// FIXME: also compute and use current velocity.
let current_idx = self.workspace_render_idx();
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
if self.active_workspace_idx != idx {
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
}
let prev_active_idx = self.active_workspace_idx;
self.active_workspace_idx = idx;
let config = config.unwrap_or(self.options.animations.workspace_switch.0);
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
self.clock.clone(),
current_idx,
idx as f64,
0.,
config,
)));
match &mut self.workspace_switch {
// During a DnD scroll, we want to visually animate even if idx matches the active idx.
Some(WorkspaceSwitch::Gesture(gesture)) if gesture.dnd_last_event_time.is_some() => {
gesture.center_idx = idx;
// Adjust start_idx to make current_idx point at idx.
let current_pos = gesture.current_idx - gesture.start_idx;
gesture.start_idx = idx as f64 - current_pos;
let prev_current_idx = gesture.current_idx;
gesture.current_idx = idx as f64;
let current_idx_delta = gesture.current_idx - prev_current_idx;
gesture.animate_from(-current_idx_delta, self.clock.clone(), config);
}
_ => {
// Don't animate if nothing changed.
if prev_active_idx == idx {
return;
}
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
self.clock.clone(),
current_idx,
idx as f64,
0.,
config,
)));
}
}
}
pub fn add_window(
@@ -725,14 +770,31 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn switch_workspace_up(&mut self) {
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
let new_idx = match &self.workspace_switch {
// During a DnD scroll, select the prev apparent workspace.
Some(WorkspaceSwitch::Gesture(gesture)) if gesture.dnd_last_event_time.is_some() => {
let current = gesture.current_idx;
let new = current.ceil() - 1.;
new.clamp(0., (self.workspaces.len() - 1) as f64) as usize
}
_ => self.active_workspace_idx.saturating_sub(1),
};
self.activate_workspace(new_idx);
}
pub fn switch_workspace_down(&mut self) {
self.activate_workspace(min(
self.active_workspace_idx + 1,
self.workspaces.len() - 1,
));
let new_idx = match &self.workspace_switch {
// During a DnD scroll, select the next apparent workspace.
Some(WorkspaceSwitch::Gesture(gesture)) if gesture.dnd_last_event_time.is_some() => {
let current = gesture.current_idx;
let new = current.floor() + 1.;
new.clamp(0., (self.workspaces.len() - 1) as f64) as usize
}
_ => min(self.active_workspace_idx + 1, self.workspaces.len() - 1),
};
self.activate_workspace(new_idx);
}
fn previous_workspace_idx(&self) -> Option<usize> {
@@ -767,11 +829,38 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn advance_animations(&mut self) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
match &mut self.workspace_switch {
Some(WorkspaceSwitch::Animation(anim)) => {
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
}
}
Some(WorkspaceSwitch::Gesture(gesture)) => {
// Make sure the last event time doesn't go too much out of date (for
// monitors 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 {
let now = self.clock.now_unadjusted();
if *last_time != now {
*last_time = now;
// If last_time was already == now, then dnd_scroll_gesture_scroll() must've
// updated the gesture already. Therefore, when this code runs, the pointer
// must be outside the DnD scrolling zone.
gesture.dnd_nonzero_start_time = None;
}
}
if let Some(anim) = &mut gesture.animation {
if anim.is_done() {
gesture.animation = None;
}
}
}
None => (),
}
for ws in &mut self.workspaces {
@@ -782,7 +871,7 @@ impl<W: LayoutElement> Monitor<W> {
pub(super) fn are_animations_ongoing(&self) -> bool {
self.workspace_switch
.as_ref()
.is_some_and(|s| s.is_animation())
.is_some_and(|s| s.is_animation_ongoing())
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
}
@@ -1330,9 +1419,44 @@ impl<W: LayoutElement> Monitor<W> {
center_idx,
start_idx: current_idx,
current_idx,
animation: None,
tracker: SwipeTracker::new(),
is_touchpad,
is_clamped: !self.overview_open,
dnd_last_event_time: None,
dnd_nonzero_start_time: None,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
pub fn dnd_scroll_gesture_begin(&mut self) {
if let Some(WorkspaceSwitch::Gesture(WorkspaceSwitchGesture {
dnd_last_event_time: Some(_),
..
})) = &self.workspace_switch
{
// Already active.
return;
}
if !self.overview_open {
// This gesture is only for the overview.
return;
}
let center_idx = self.active_workspace_idx;
let current_idx = self.workspace_render_idx();
let gesture = WorkspaceSwitchGesture {
center_idx,
start_idx: current_idx,
current_idx,
animation: None,
tracker: SwipeTracker::new(),
is_touchpad: false,
is_clamped: false,
dnd_last_event_time: Some(self.clock.now_unadjusted()),
dnd_nonzero_start_time: None,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
@@ -1347,7 +1471,7 @@ impl<W: LayoutElement> Monitor<W> {
return None;
};
if gesture.is_touchpad != is_touchpad {
if gesture.is_touchpad != is_touchpad || gesture.dnd_last_event_time.is_some() {
return None;
}
@@ -1389,6 +1513,91 @@ impl<W: LayoutElement> Monitor<W> {
Some(true)
}
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
let zoom = self.overview_zoom();
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
let Some(last_time) = gesture.dnd_last_event_time else {
// Not a DnD scroll.
return false;
};
let config = &self.options.gestures.dnd_edge_workspace_switch;
let trigger_height = config.trigger_height.0;
// Restrict the scrolling horizontally to the strip of workspaces to avoid unwanted trigger
// after using the hot corner or during horizontal scroll.
let width = self.view_size.w * zoom;
let x = pos.x - (self.view_size.w - width) / 2.;
// Consider the working area so layer-shell docks and such don't prevent scrolling.
let y = pos.y - self.working_area.loc.y;
let height = self.working_area.size.h;
let y = y.clamp(0., height);
let trigger_height = trigger_height.clamp(0., height / 2.);
let delta = if x < 0. || width <= x {
// Outside the bounds horizontally.
0.
} else if y < trigger_height {
-(trigger_height - y)
} else if height - y < trigger_height {
trigger_height - (height - y)
} else {
0.
};
let delta = if trigger_height < 0.01 {
// Sanity check for trigger-height 0 or small window sizes.
0.
} else {
// Normalize to [0, 1].
delta / trigger_height
};
let delta = delta * speed;
let now = self.clock.now_unadjusted();
gesture.dnd_last_event_time = Some(now);
if delta == 0. {
// We're outside the scrolling zone.
gesture.dnd_nonzero_start_time = None;
return false;
}
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
// Delay starting the gesture a bit to avoid unwanted movement when dragging across
// monitors.
let delay = Duration::from_millis(u64::from(config.delay_ms));
if now.saturating_sub(nonzero_start) < delay {
return true;
}
let time_delta = now.saturating_sub(last_time).as_secs_f64();
let delta = delta * time_delta * config.max_speed.0;
gesture.tracker.push(delta, now);
let total_height = WORKSPACE_DND_EDGE_SCROLL_MOVEMENT;
let pos = gesture.tracker.pos() / total_height;
let unclamped = gesture.start_idx + pos;
let (min, max) = gesture.min_max(self.workspaces.len());
let clamped = unclamped.clamp(min, max);
// Make sure that DnD scrolling too much outside the min/max does not "build up".
gesture.start_idx += clamped - unclamped;
gesture.current_idx = clamped;
true
}
pub fn workspace_switch_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
let Some(WorkspaceSwitch::Gesture(gesture)) = &self.workspace_switch else {
return false;
@@ -1399,7 +1608,9 @@ impl<W: LayoutElement> Monitor<W> {
}
let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad {
let total_height = if gesture.dnd_last_event_time.is_some() {
WORKSPACE_DND_EDGE_SCROLL_MOVEMENT
} else if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
self.workspace_size_with_gap(1.).h
@@ -1442,6 +1653,21 @@ impl<W: LayoutElement> Monitor<W> {
true
}
pub fn dnd_scroll_gesture_end(&mut self) {
if !matches!(
self.workspace_switch,
Some(WorkspaceSwitch::Gesture(WorkspaceSwitchGesture {
dnd_last_event_time: Some(_),
..
}))
) {
// Not a DnD scroll.
return;
};
self.workspace_switch_gesture_end(None);
}
pub fn scale(&self) -> smithay::output::Scale {
self.scale
}