mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-23 02:05:33 +07:00
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
This commit is contained in:
@@ -1240,6 +1240,8 @@ pub enum Action {
|
|||||||
ConsumeOrExpelWindowRightById(u64),
|
ConsumeOrExpelWindowRightById(u64),
|
||||||
ConsumeWindowIntoColumn,
|
ConsumeWindowIntoColumn,
|
||||||
ExpelWindowFromColumn,
|
ExpelWindowFromColumn,
|
||||||
|
SwapWindowLeft,
|
||||||
|
SwapWindowRight,
|
||||||
CenterColumn,
|
CenterColumn,
|
||||||
CenterWindow,
|
CenterWindow,
|
||||||
#[knuffel(skip)]
|
#[knuffel(skip)]
|
||||||
@@ -1397,6 +1399,8 @@ impl From<niri_ipc::Action> for Action {
|
|||||||
}
|
}
|
||||||
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
|
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
|
||||||
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
|
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::CenterColumn {} => Self::CenterColumn,
|
||||||
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
|
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
|
||||||
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
|
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
|
||||||
|
|||||||
@@ -290,6 +290,10 @@ pub enum Action {
|
|||||||
ConsumeWindowIntoColumn {},
|
ConsumeWindowIntoColumn {},
|
||||||
/// Expel the focused window from the column.
|
/// Expel the focused window from the column.
|
||||||
ExpelWindowFromColumn {},
|
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.
|
/// Center the focused column on the screen.
|
||||||
CenterColumn {},
|
CenterColumn {},
|
||||||
/// Center a window on the screen.
|
/// Center a window on the screen.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use touch_move_grab::TouchMoveGrab;
|
|||||||
use self::move_grab::MoveGrab;
|
use self::move_grab::MoveGrab;
|
||||||
use self::resize_grab::ResizeGrab;
|
use self::resize_grab::ResizeGrab;
|
||||||
use self::spatial_movement_grab::SpatialMovementGrab;
|
use self::spatial_movement_grab::SpatialMovementGrab;
|
||||||
|
use crate::layout::scrolling::ScrollDirection;
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||||
use crate::utils::spawning::spawn;
|
use crate::utils::spawning::spawn;
|
||||||
@@ -1132,6 +1133,22 @@ impl State {
|
|||||||
// FIXME: granular
|
// FIXME: granular
|
||||||
self.niri.queue_redraw_all();
|
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 => {
|
Action::SwitchPresetColumnWidth => {
|
||||||
self.niri.layout.toggle_width();
|
self.niri.layout.toggle_width();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub use self::monitor::MonitorRenderElement;
|
|||||||
use self::monitor::{Monitor, WorkspaceSwitch};
|
use self::monitor::{Monitor, WorkspaceSwitch};
|
||||||
use self::workspace::{OutputId, Workspace};
|
use self::workspace::{OutputId, Workspace};
|
||||||
use crate::animation::Clock;
|
use crate::animation::Clock;
|
||||||
|
use crate::layout::scrolling::ScrollDirection;
|
||||||
use crate::niri_render_elements;
|
use crate::niri_render_elements;
|
||||||
use crate::render_helpers::renderer::NiriRenderer;
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||||
@@ -2025,6 +2026,13 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
monitor.expel_from_column();
|
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) {
|
pub fn center_column(&mut self) {
|
||||||
let Some(monitor) = self.active_monitor() else {
|
let Some(monitor) = self.active_monitor() else {
|
||||||
return;
|
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)]
|
#[derive(Debug, Clone, Copy, Arbitrary)]
|
||||||
enum Op {
|
enum Op {
|
||||||
AddOutput(#[proptest(strategy = "1..=5usize")] usize),
|
AddOutput(#[proptest(strategy = "1..=5usize")] usize),
|
||||||
@@ -4462,6 +4474,9 @@ mod tests {
|
|||||||
},
|
},
|
||||||
ConsumeWindowIntoColumn,
|
ConsumeWindowIntoColumn,
|
||||||
ExpelWindowFromColumn,
|
ExpelWindowFromColumn,
|
||||||
|
SwapWindowInDirection(
|
||||||
|
#[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection,
|
||||||
|
),
|
||||||
CenterColumn,
|
CenterColumn,
|
||||||
CenterWindow {
|
CenterWindow {
|
||||||
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
|
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
|
||||||
@@ -4984,6 +4999,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
Op::ConsumeWindowIntoColumn => layout.consume_into_column(),
|
Op::ConsumeWindowIntoColumn => layout.consume_into_column(),
|
||||||
Op::ExpelWindowFromColumn => layout.expel_from_column(),
|
Op::ExpelWindowFromColumn => layout.expel_from_column(),
|
||||||
|
Op::SwapWindowInDirection(direction) => layout.swap_window_in_direction(direction),
|
||||||
Op::CenterColumn => layout.center_column(),
|
Op::CenterColumn => layout.center_column(),
|
||||||
Op::CenterWindow { id } => {
|
Op::CenterWindow { id } => {
|
||||||
let id = id.filter(|id| layout.has_window(id));
|
let id = id.filter(|id| layout.has_window(id));
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use smithay::backend::renderer::element::utils::{
|
|||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::scrolling::{Column, ColumnWidth};
|
use super::scrolling::{Column, ColumnWidth, ScrollDirection};
|
||||||
use super::tile::Tile;
|
use super::tile::Tile;
|
||||||
use super::workspace::{
|
use super::workspace::{
|
||||||
OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement,
|
OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement,
|
||||||
@@ -707,6 +707,10 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
self.active_workspace().expel_from_column();
|
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) {
|
pub fn center_column(&mut self) {
|
||||||
self.active_workspace().center_column();
|
self.active_workspace().center_column();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,16 @@ pub enum WindowHeight {
|
|||||||
Preset(usize),
|
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> {
|
impl<W: LayoutElement> ScrollingSpace<W> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
view_size: Size<f64, Logical>,
|
view_size: Size<f64, Logical>,
|
||||||
@@ -1749,6 +1759,132 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
|||||||
new_col.tiles[0].animate_move_from(offset);
|
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) {
|
pub fn center_column(&mut self) {
|
||||||
if self.columns.is_empty() {
|
if self.columns.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ use smithay::wayland::shell::xdg::SurfaceCachedState;
|
|||||||
|
|
||||||
use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
|
use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
|
||||||
use super::scrolling::{
|
use super::scrolling::{
|
||||||
Column, ColumnWidth, InsertHint, InsertPosition, ScrollingSpace, ScrollingSpaceRenderElement,
|
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
|
||||||
|
ScrollingSpaceRenderElement,
|
||||||
};
|
};
|
||||||
use super::tile::{Tile, TileRenderSnapshot};
|
use super::tile::{Tile, TileRenderSnapshot};
|
||||||
use super::{ActivateWindow, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac};
|
use super::{ActivateWindow, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac};
|
||||||
@@ -969,6 +970,13 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
self.scrolling.expel_from_column();
|
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) {
|
pub fn center_column(&mut self) {
|
||||||
if self.floating_is_active.get() {
|
if self.floating_is_active.get() {
|
||||||
self.floating.center_window(None);
|
self.floating.center_window(None);
|
||||||
|
|||||||
Reference in New Issue
Block a user