aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorrustn00b <83183600+rustn00b@users.noreply.github.com>2025-01-09 08:29:36 +0000
committerGitHub <noreply@github.com>2025-01-09 08:29:36 +0000
commit80815a1591aa3362a5e1c095e9ab81b2945041a7 (patch)
treea09a434ff35efc61e1596a34d7d90ba5f69f4111 /src
parent8412bfb8136544549e3174fd48859d0be0090c78 (diff)
downloadniri-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.rs17
-rw-r--r--src/layout/mod.rs16
-rw-r--r--src/layout/monitor.rs6
-rw-r--r--src/layout/scrolling.rs136
-rw-r--r--src/layout/workspace.rs10
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);