Skip to content

Commit

Permalink
Add a window swap operation
Browse files Browse the repository at this point in the history
Swap the active window with the a neighboring column's active window.
  • Loading branch information
rustn00b authored and YaLTeR committed Jan 5, 2025
1 parent 098c826 commit b47eb55
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 2 deletions.
4 changes: 4 additions & 0 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,8 @@ pub enum Action {
ConsumeOrExpelWindowRightById(u64),
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
SwapWindowLeft,
SwapWindowRight,
CenterColumn,
CenterWindow,
#[knuffel(skip)]
Expand Down Expand Up @@ -1397,6 +1399,8 @@ impl From<niri_ipc::Action> for Action {
}
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight,
niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft,
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
Expand Down
4 changes: 4 additions & 0 deletions niri-ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ pub enum Action {
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn {},
/// Swap focused window with one to the right
SwapWindowRight {},
/// Swap focused window with one to the left
SwapWindowLeft {},
/// Center the focused column on the screen.
CenterColumn {},
/// Center a window on the screen.
Expand Down
17 changes: 17 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
16 changes: 16 additions & 0 deletions src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -4462,6 +4474,9 @@ mod tests {
},
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
SwapWindowInDirection(
#[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection,
),
CenterColumn,
CenterWindow {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
Expand Down Expand Up @@ -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));
Expand Down
6 changes: 5 additions & 1 deletion src/layout/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
Expand Down
127 changes: 127 additions & 0 deletions src/layout/scrolling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -1749,6 +1759,123 @@ 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 self.consume_or_expel_window_left(None);
}
}
ScrollDirection::Right => {
if self.active_column_idx == self.columns.len() - 1 {
return self.consume_or_expel_window_right(None);
}
}
}

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 (source_pt, target_pt) = (
Point::from((self.column_x(source_column_idx), 0.))
+ self.columns[source_column_idx].tile_offset(source_tile_idx),
Point::from((self.column_x(target_column_idx), 0.))
+ self.columns[target_column_idx].tile_offset(target_tile_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);
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;
Expand Down
10 changes: 9 additions & 1 deletion src/layout/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit b47eb55

Please sign in to comment.