diff options
| author | Kirottu <56396750+Kirottu@users.noreply.github.com> | 2025-01-25 10:49:51 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-25 08:49:51 +0000 |
| commit | 852da5714affd067de731599136ed619dc3bba40 (patch) | |
| tree | e42514f81d8a7f74f5437746503367082917777a /src | |
| parent | 4f793038117b4fef38f491e665a66589eb896e0a (diff) | |
| download | niri-852da5714affd067de731599136ed619dc3bba40.tar.gz niri-852da5714affd067de731599136ed619dc3bba40.tar.bz2 niri-852da5714affd067de731599136ed619dc3bba40.zip | |
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>
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/mod.rs | 43 | ||||
| -rw-r--r-- | src/layout/mod.rs | 247 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 47 |
3 files changed, 335 insertions, 2 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs index 12746376..c2691739 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -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 diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 63bedc7f..8eb4c85b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -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; diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 8bb92a33..3eaac97f 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -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. |
