mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Implement scrolling the view during interactive move
This commit is contained in:
@@ -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
|
||||
|
||||
+110
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+102
-3
@@ -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;
|
||||
@@ -3228,6 +3322,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.active_column_idx
|
||||
}
|
||||
|
||||
#[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.);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user