Add move-workspace-to-index and move-workspace-to-monitor actions (#1007)

* Added move-workspace-to-index and move-workspace-to-monitor IPC actions

* Added redraws to the workspace handling actions, fixed tests that panicked, fixed other mentioned problems.

* Fixed workspace focusing and handling numbered workspaces with `move-workspace-to-index`

* Fixed more inconsistencies with move-workspace-to-monitor

* Added back `self.workspace_switch = None`

* Reordered some workspace cleanup logic

* Fix formatting

* Add missing blank lines

* Fix moving workspace to same monitor and wrong current index updating

* Move function up and add fixme comment

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
Kirottu
2025-01-25 10:49:51 +02:00
committed by GitHub
parent 4f79303811
commit 852da5714a
5 changed files with 401 additions and 2 deletions

View File

@@ -1358,6 +1358,18 @@ pub enum Action {
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveWorkspaceDown,
MoveWorkspaceUp,
MoveWorkspaceToIndex(#[knuffel(argument)] usize),
#[knuffel(skip)]
MoveWorkspaceToIndexByRef {
new_idx: usize,
reference: WorkspaceReference,
},
#[knuffel(skip)]
MoveWorkspaceToMonitorByRef {
output_name: String,
reference: WorkspaceReference,
},
MoveWorkspaceToMonitor(#[knuffel(argument)] String),
SetWorkspaceName(#[knuffel(argument)] String),
#[knuffel(skip)]
SetWorkspaceNameByRef {
@@ -1612,6 +1624,28 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::MoveWorkspaceToMonitorPrevious {} => {
Self::MoveWorkspaceToMonitorPrevious
}
niri_ipc::Action::MoveWorkspaceToIndex {
index,
reference: Some(reference),
} => Self::MoveWorkspaceToIndexByRef {
new_idx: index,
reference: WorkspaceReference::from(reference),
},
niri_ipc::Action::MoveWorkspaceToIndex {
index,
reference: None,
} => Self::MoveWorkspaceToIndex(index),
niri_ipc::Action::MoveWorkspaceToMonitor {
output,
reference: Some(reference),
} => Self::MoveWorkspaceToMonitorByRef {
output_name: output,
reference: WorkspaceReference::from(reference),
},
niri_ipc::Action::MoveWorkspaceToMonitor {
output,
reference: None,
} => Self::MoveWorkspaceToMonitor(output),
niri_ipc::Action::MoveWorkspaceToMonitorNext {} => Self::MoveWorkspaceToMonitorNext,
niri_ipc::Action::ToggleDebugTint {} => Self::ToggleDebugTint,
niri_ipc::Action::DebugToggleOpaqueRegions {} => Self::DebugToggleOpaqueRegions,

View File

@@ -365,6 +365,22 @@ pub enum Action {
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
/// Move a workspace to a specific index on its monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific index on its monitor")
)]
MoveWorkspaceToIndex {
/// New index for the workspace.
#[cfg_attr(feature = "clap", arg())]
index: usize,
/// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
@@ -519,6 +535,22 @@ pub enum Action {
MoveWorkspaceToMonitorPrevious {},
/// Move the focused workspace to the next monitor.
MoveWorkspaceToMonitorNext {},
/// Move a workspace to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific monitor")
)]
MoveWorkspaceToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.

View File

@@ -1163,6 +1163,18 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWorkspaceToIndex(new_idx) => {
self.niri.layout.move_workspace_to_idx(None, new_idx);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWorkspaceToIndexByRef { new_idx, reference } => {
if let Some(res) = self.niri.find_output_and_workspace_index(reference) {
self.niri.layout.move_workspace_to_idx(Some(res), new_idx);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::SetWorkspaceName(name) => {
self.niri.layout.set_workspace_name(name, None);
}
@@ -1497,6 +1509,37 @@ impl State {
}
}
}
Action::MoveWorkspaceToMonitor(new_output) => {
if let Some(new_output) = self.niri.output_by_name_match(&new_output).cloned() {
if self.niri.layout.move_workspace_to_output(&new_output)
&& !self.maybe_warp_cursor_to_focus_centered()
{
self.move_cursor_to_output(&new_output);
}
}
}
Action::MoveWorkspaceToMonitorByRef {
output_name,
reference,
} => {
if let Some((output, old_idx)) =
self.niri.find_output_and_workspace_index(reference)
{
if let Some(new_output) = self.niri.output_by_name_match(&output_name).cloned()
{
if self.niri.layout.move_workspace_to_output_by_id(
old_idx,
output,
new_output.clone(),
) {
// Cursor warp already calls `queue_redraw_all`
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&new_output);
}
}
}
}
}
Action::ToggleWindowFloating => {
self.niri.layout.toggle_window_floating(None);
// FIXME: granular

View File

@@ -3032,17 +3032,23 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn move_workspace_to_output(&mut self, output: &Output) {
pub fn move_workspace_to_output(&mut self, output: &Output) -> bool {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return;
return false;
};
let current = &mut monitors[*active_monitor_idx];
// Do not do anything if the output is already correct
if &current.output == output {
return false;
}
if current.active_workspace_idx == current.workspaces.len() - 1 {
// Insert a new empty workspace.
current.add_workspace_bottom();
@@ -3080,6 +3086,96 @@ impl<W: LayoutElement> Layout<W> {
target.clean_up_workspaces();
*active_monitor_idx = target_idx;
true
}
// FIXME: accept workspace by id and deduplicate logic with move_workspace_to_output()
pub fn move_workspace_to_output_by_id(
&mut self,
old_idx: usize,
old_output: Option<Output>,
new_output: Output,
) -> bool {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return false;
};
let current_idx = if let Some(old_output) = old_output {
monitors
.iter()
.position(|mon| mon.output == old_output)
.unwrap()
} else {
*active_monitor_idx
};
let target_idx = monitors
.iter()
.position(|mon| mon.output == new_output)
.unwrap();
if current_idx == target_idx {
return false;
}
let current = &mut monitors[current_idx];
let current_active_ws_idx = current.active_workspace_idx;
if old_idx == current.workspaces.len() - 1 {
// Insert a new empty workspace.
current.add_workspace_bottom();
}
let mut ws = current.workspaces.remove(old_idx);
if current.options.empty_workspace_above_first && old_idx == 0 {
current.add_workspace_top();
}
if old_idx < current.active_workspace_idx {
current.active_workspace_idx -= 1;
}
current.workspace_switch = None;
current.clean_up_workspaces();
ws.set_output(Some(new_output.clone()));
ws.original_output = OutputId::new(&new_output);
let target = &mut monitors[target_idx];
target.previous_workspace_id = Some(target.workspaces[target.active_workspace_idx].id());
if target.options.empty_workspace_above_first && target.workspaces.len() == 1 {
// Insert a new empty workspace on top to prepare for insertion of new workspce.
target.add_workspace_top();
}
// Insert the workspace after the currently active one. Unless the currently active one is
// the last empty workspace, then insert before.
let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
target.workspaces.insert(target_ws_idx, ws);
// Only switch active monitor if the workspace moved was the currently focused one on the
// current monitor
let res = if current_idx == *active_monitor_idx && old_idx == current_active_ws_idx {
*active_monitor_idx = target_idx;
target.active_workspace_idx = target_ws_idx;
true
} else {
if target_ws_idx <= target.active_workspace_idx {
target.active_workspace_idx += 1;
}
false
};
target.workspace_switch = None;
target.clean_up_workspaces();
res
}
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
@@ -3792,6 +3888,37 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_workspace_up();
}
pub fn move_workspace_to_idx(
&mut self,
reference: Option<(Option<Output>, usize)>,
new_idx: usize,
) {
let (monitor, old_idx) = if let Some((output, old_idx)) = reference {
let monitor = if let Some(output) = output {
let Some(monitor) = self.monitor_for_output_mut(&output) else {
return;
};
monitor
} else {
// In case a numbered workspace reference is used, assume the active monitor
let Some(monitor) = self.active_monitor() else {
return;
};
monitor
};
(monitor, old_idx)
} else {
let Some(monitor) = self.active_monitor() else {
return;
};
let index = monitor.active_workspace_idx;
(monitor, index)
};
monitor.move_workspace_to_idx(old_idx, new_idx);
}
pub fn set_workspace_name(&mut self, name: String, reference: Option<WorkspaceReference>) {
// ignore the request if the name is already used by another workspace
if self.find_workspace_by_name(&name).is_some() {
@@ -4607,6 +4734,18 @@ mod tests {
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
MoveWorkspaceDown,
MoveWorkspaceUp,
MoveWorkspaceToIndex {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
ws_name: Option<usize>,
#[proptest(strategy = "0..=4usize")]
target_idx: usize,
},
MoveWorkspaceToMonitor {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
ws_name: Option<usize>,
#[proptest(strategy = "0..=5usize")]
output_id: usize,
},
SetWorkspaceName {
#[proptest(strategy = "1..=5usize")]
new_ws_name: usize,
@@ -5179,6 +5318,78 @@ mod tests {
}
Op::MoveWorkspaceDown => layout.move_workspace_down(),
Op::MoveWorkspaceUp => layout.move_workspace_up(),
Op::MoveWorkspaceToIndex {
ws_name: Some(ws_name),
target_idx,
} => {
let MonitorSet::Normal { monitors, .. } = &mut layout.monitor_set else {
return;
};
let Some((old_idx, old_output)) = monitors.iter().find_map(|monitor| {
monitor
.workspaces
.iter()
.enumerate()
.find_map(|(i, ws)| {
if ws.name == Some(format!("ws{ws_name}")) {
Some(i)
} else {
None
}
})
.map(|i| (i, monitor.output.clone()))
}) else {
return;
};
layout.move_workspace_to_idx(Some((Some(old_output), old_idx)), target_idx)
}
Op::MoveWorkspaceToIndex {
ws_name: None,
target_idx,
} => layout.move_workspace_to_idx(None, target_idx),
Op::MoveWorkspaceToMonitor {
ws_name: None,
output_id: id,
} => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_workspace_to_output(&output);
}
Op::MoveWorkspaceToMonitor {
ws_name: Some(ws_name),
output_id: id,
} => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
let MonitorSet::Normal { monitors, .. } = &mut layout.monitor_set else {
return;
};
let Some((old_idx, old_output)) = monitors.iter().find_map(|monitor| {
monitor
.workspaces
.iter()
.enumerate()
.find_map(|(i, ws)| {
if ws.name == Some(format!("ws{ws_name}")) {
Some(i)
} else {
None
}
})
.map(|i| (i, monitor.output.clone()))
}) else {
return;
};
layout.move_workspace_to_output_by_id(old_idx, Some(old_output), output);
}
Op::SwitchPresetColumnWidth => layout.toggle_width(),
Op::SwitchPresetWindowWidth { id } => {
let id = id.filter(|id| layout.has_window(id));
@@ -6986,6 +7197,38 @@ mod tests {
check_ops(&ops);
}
#[test]
fn move_workspace_to_same_monitor_doesnt_reorder() {
let ops = [
Op::AddOutput(0),
Op::SetWorkspaceName {
new_ws_name: 0,
ws_name: None,
},
Op::AddWindow {
params: TestWindowParams::new(0),
},
Op::FocusWorkspaceDown,
Op::AddWindow {
params: TestWindowParams::new(1),
},
Op::AddWindow {
params: TestWindowParams::new(2),
},
Op::MoveWorkspaceToMonitor {
ws_name: Some(0),
output_id: 0,
},
];
let layout = check_ops(&ops);
let counts: Vec<_> = layout
.workspaces()
.map(|(_, _, ws)| ws.windows().count())
.collect();
assert_eq!(counts, &[1, 2, 0]);
}
fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
if parent_id == id {
return true;

View File

@@ -858,6 +858,53 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
pub fn move_workspace_to_idx(&mut self, old_idx: usize, new_idx: usize) {
let mut new_idx = new_idx.clamp(0, self.workspaces.len() - 1);
if old_idx == new_idx {
return;
}
let ws = self.workspaces.remove(old_idx);
self.workspaces.insert(new_idx, ws);
if new_idx > old_idx {
if new_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
self.add_workspace_bottom();
}
if self.options.empty_workspace_above_first && old_idx == 0 {
self.add_workspace_top();
new_idx += 1;
}
} else {
if old_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
self.add_workspace_bottom();
}
if self.options.empty_workspace_above_first && new_idx == 0 {
self.add_workspace_top();
new_idx += 1;
}
}
// Only refocus the workspace if it was already focused
if self.active_workspace_idx == old_idx {
self.active_workspace_idx = new_idx;
// If the workspace order was switched so that the current workspace moved down the
// workspace stack, focus correctly
} else if new_idx <= self.active_workspace_idx && old_idx > self.active_workspace_idx {
self.active_workspace_idx += 1;
} else if new_idx >= self.active_workspace_idx && old_idx < self.active_workspace_idx {
self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1);
}
self.workspace_switch = None;
self.clean_up_workspaces();
}
/// Returns the geometry of the active tile relative to and clamped to the output.
///
/// During animations, assumes the final view position.