diff options
| author | rustn00b <83183600+rustn00b@users.noreply.github.com> | 2025-01-09 08:29:36 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-09 08:29:36 +0000 |
| commit | 80815a1591aa3362a5e1c095e9ab81b2945041a7 (patch) | |
| tree | a09a434ff35efc61e1596a34d7d90ba5f69f4111 /src | |
| parent | 8412bfb8136544549e3174fd48859d0be0090c78 (diff) | |
| download | niri-80815a1591aa3362a5e1c095e9ab81b2945041a7.tar.gz niri-80815a1591aa3362a5e1c095e9ab81b2945041a7.tar.bz2 niri-80815a1591aa3362a5e1c095e9ab81b2945041a7.zip | |
Add a window swap operation (#899)
Swap the active window with the a neighboring column's active window.
---------
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
Take into account PR comments
- no longer behave like an expel when a swap is made in a direction
where there is no column to swap with
- fix janky animation
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/mod.rs | 17 | ||||
| -rw-r--r-- | src/layout/mod.rs | 16 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 6 | ||||
| -rw-r--r-- | src/layout/scrolling.rs | 136 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 10 |
5 files changed, 183 insertions, 2 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs index 263f533d..576bc2f5 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -36,6 +36,7 @@ use touch_move_grab::TouchMoveGrab; use self::move_grab::MoveGrab; use self::resize_grab::ResizeGrab; use self::spatial_movement_grab::SpatialMovementGrab; +use crate::layout::scrolling::ScrollDirection; use crate::niri::State; use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; @@ -1132,6 +1133,22 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } + Action::SwapWindowRight => { + self.niri + .layout + .swap_window_in_direction(ScrollDirection::Right); + self.maybe_warp_cursor_to_focus(); + // FIXME: granular + self.niri.queue_redraw_all(); + } + Action::SwapWindowLeft => { + self.niri + .layout + .swap_window_in_direction(ScrollDirection::Left); + self.maybe_warp_cursor_to_focus(); + // FIXME: granular + self.niri.queue_redraw_all(); + } Action::SwitchPresetColumnWidth => { self.niri.layout.toggle_width(); } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 4923282b..9fa4681a 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -55,6 +55,7 @@ pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch}; use self::workspace::{OutputId, Workspace}; use crate::animation::Clock; +use crate::layout::scrolling::ScrollDirection; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; @@ -2025,6 +2026,13 @@ impl<W: LayoutElement> Layout<W> { monitor.expel_from_column(); } + pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.swap_window_in_direction(direction); + } + pub fn center_column(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -4382,6 +4390,10 @@ mod tests { ] } + fn arbitrary_scroll_direction() -> impl Strategy<Value = ScrollDirection> { + prop_oneof![Just(ScrollDirection::Left), Just(ScrollDirection::Right)] + } + #[derive(Debug, Clone, Copy, Arbitrary)] enum Op { AddOutput(#[proptest(strategy = "1..=5usize")] usize), @@ -4462,6 +4474,9 @@ mod tests { }, ConsumeWindowIntoColumn, ExpelWindowFromColumn, + SwapWindowInDirection( + #[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection, + ), CenterColumn, CenterWindow { #[proptest(strategy = "proptest::option::of(1..=5usize)")] @@ -4984,6 +4999,7 @@ mod tests { } Op::ConsumeWindowIntoColumn => layout.consume_into_column(), Op::ExpelWindowFromColumn => layout.expel_from_column(), + Op::SwapWindowInDirection(direction) => layout.swap_window_in_direction(direction), Op::CenterColumn => layout.center_column(), Op::CenterWindow { id } => { let id = id.filter(|id| layout.has_window(id)); diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 2de3c760..57ca7f53 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -9,7 +9,7 @@ use smithay::backend::renderer::element::utils::{ use smithay::output::Output; use smithay::utils::{Logical, Point, Rectangle, Size}; -use super::scrolling::{Column, ColumnWidth}; +use super::scrolling::{Column, ColumnWidth, ScrollDirection}; use super::tile::Tile; use super::workspace::{ OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement, @@ -707,6 +707,10 @@ impl<W: LayoutElement> Monitor<W> { self.active_workspace().expel_from_column(); } + pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) { + self.active_workspace().swap_window_in_direction(direction); + } + pub fn center_column(&mut self) { self.active_workspace().center_column(); } diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 89975736..e54beccb 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -239,6 +239,16 @@ pub enum WindowHeight { Preset(usize), } +/// Horizontal direction for an operation +/// +/// As operations often have a symmetrical counterpart, e.g. focus-right/focus-left, methods +/// on `Scrolling` can sometimes be factored using the direction of the operation as a parameter. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ScrollDirection { + Left, + Right, +} + impl<W: LayoutElement> ScrollingSpace<W> { pub fn new( view_size: Size<f64, Logical>, @@ -1749,6 +1759,132 @@ impl<W: LayoutElement> ScrollingSpace<W> { new_col.tiles[0].animate_move_from(offset); } + pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) { + if self.columns.is_empty() { + return; + } + + // if this is the first (resp. last column), then this operation is equivalent + // to an `consume_or_expel_window_left` (resp. `consume_or_expel_window_right`) + match direction { + ScrollDirection::Left => { + if self.active_column_idx == 0 { + return; + } + } + ScrollDirection::Right => { + if self.active_column_idx == self.columns.len() - 1 { + return; + } + } + } + + let source_column_idx = self.active_column_idx; + let target_column_idx = self.active_column_idx.wrapping_add_signed(match direction { + ScrollDirection::Left => -1, + ScrollDirection::Right => 1, + }); + + // if both source and target columns contain a single tile, then the operation is equivalent + // to a simple column move + if self.columns[source_column_idx].tiles.len() == 1 + && self.columns[target_column_idx].tiles.len() == 1 + { + return self.move_column_to(target_column_idx); + } + + let source_tile_idx = self.columns[source_column_idx].active_tile_idx; + let target_tile_idx = self.columns[target_column_idx].active_tile_idx; + let source_column_drained = self.columns[source_column_idx].tiles.len() == 1; + + // capture the original positions of the tiles + let (mut source_pt, mut target_pt) = ( + self.columns[source_column_idx].render_offset() + + self.columns[source_column_idx].tile_offset(source_tile_idx), + self.columns[target_column_idx].render_offset() + + self.columns[target_column_idx].tile_offset(target_tile_idx), + ); + source_pt.x += self.column_x(source_column_idx); + target_pt.x += self.column_x(target_column_idx); + + let transaction = Transaction::new(); + + // If the source column contains a single tile, this will also remove the column. + // When this happens `source_column_drained` will be set and the column will need to be + // recreated with `add_tile` + let source_removed = self.remove_tile_by_idx( + source_column_idx, + source_tile_idx, + transaction.clone(), + None, + ); + + { + // special case when the source column disappears after removing its last tile + let adjusted_target_column_idx = + if direction == ScrollDirection::Right && source_column_drained { + target_column_idx - 1 + } else { + target_column_idx + }; + + self.add_tile_to_column( + adjusted_target_column_idx, + Some(target_tile_idx), + source_removed.tile, + false, + ); + + let RemovedTile { + tile: target_tile, .. + } = self.remove_tile_by_idx( + adjusted_target_column_idx, + target_tile_idx + 1, + transaction.clone(), + None, + ); + + if source_column_drained { + // recreate the drained column with only the target tile + self.add_tile( + Some(source_column_idx), + target_tile, + true, + source_removed.width, + source_removed.is_full_width, + None, + ) + } else { + // simply add the removed target tile to the source column + self.add_tile_to_column( + source_column_idx, + Some(source_tile_idx), + target_tile, + false, + ); + } + } + + // update the active tile in the modified columns + self.columns[source_column_idx].active_tile_idx = source_tile_idx; + self.columns[target_column_idx].active_tile_idx = target_tile_idx; + + // Animations + self.columns[target_column_idx].tiles[target_tile_idx] + .animate_move_from(source_pt - target_pt); + + // FIXME: this stop_move_animations() causes the target tile animation to "reset" when + // swapping. It's here as a workaround to stop the unwanted animation of moving the source + // tile down when adding the target tile above it. This code needs to be written in some + // other way not to trigger that animation, or to cancel it properly, so that swap doesn't + // cancel all ongoing target tile animations. + self.columns[source_column_idx].tiles[source_tile_idx].stop_move_animations(); + self.columns[source_column_idx].tiles[source_tile_idx] + .animate_move_from(target_pt - source_pt); + + self.activate_column(target_column_idx); + } + pub fn center_column(&mut self) { if self.columns.is_empty() { return; diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index b11b46ac..a931c694 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -15,7 +15,8 @@ use smithay::wayland::shell::xdg::SurfaceCachedState; use super::floating::{FloatingSpace, FloatingSpaceRenderElement}; use super::scrolling::{ - Column, ColumnWidth, InsertHint, InsertPosition, ScrollingSpace, ScrollingSpaceRenderElement, + Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace, + ScrollingSpaceRenderElement, }; use super::tile::{Tile, TileRenderSnapshot}; use super::{ActivateWindow, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac}; @@ -969,6 +970,13 @@ impl<W: LayoutElement> Workspace<W> { self.scrolling.expel_from_column(); } + pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) { + if self.floating_is_active.get() { + return; + } + self.scrolling.swap_window_in_direction(direction); + } + pub fn center_column(&mut self) { if self.floating_is_active.get() { self.floating.center_window(None); |
