mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-06 00:23:14 +02:00
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:
@@ -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,
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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 ¤t.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;
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user