diff options
| -rw-r--r-- | niri-config/src/lib.rs | 53 | ||||
| -rw-r--r-- | niri-ipc/src/lib.rs | 80 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 4 | ||||
| -rw-r--r-- | src/input/mod.rs | 158 | ||||
| -rw-r--r-- | src/layout/mod.rs | 312 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 75 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 38 | ||||
| -rw-r--r-- | src/niri.rs | 11 |
8 files changed, 598 insertions, 133 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 0820a018..ea99865d 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1073,8 +1073,16 @@ pub enum Action { Screenshot, ScreenshotScreen, ScreenshotWindow, + #[knuffel(skip)] + ScreenshotWindowById(u64), CloseWindow, + #[knuffel(skip)] + CloseWindowById(u64), FullscreenWindow, + #[knuffel(skip)] + FullscreenWindowById(u64), + #[knuffel(skip)] + FocusWindow(u64), FocusColumnLeft, FocusColumnRight, FocusColumnFirst, @@ -1115,6 +1123,11 @@ pub enum Action { MoveWindowToWorkspaceDown, MoveWindowToWorkspaceUp, MoveWindowToWorkspace(#[knuffel(argument)] WorkspaceReference), + #[knuffel(skip)] + MoveWindowToWorkspaceById { + window_id: u64, + reference: WorkspaceReference, + }, MoveColumnToWorkspaceDown, MoveColumnToWorkspaceUp, MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference), @@ -1133,7 +1146,14 @@ pub enum Action { MoveColumnToMonitorDown, MoveColumnToMonitorUp, SetWindowHeight(#[knuffel(argument, str)] SizeChange), + #[knuffel(skip)] + SetWindowHeightById { + id: u64, + change: SizeChange, + }, ResetWindowHeight, + #[knuffel(skip)] + ResetWindowHeightById(u64), SwitchPresetColumnWidth, MaximizeColumn, SetColumnWidth(#[knuffel(argument, str)] SizeChange), @@ -1154,9 +1174,13 @@ impl From<niri_ipc::Action> for Action { niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms), niri_ipc::Action::Screenshot => Self::Screenshot, niri_ipc::Action::ScreenshotScreen => Self::ScreenshotScreen, - niri_ipc::Action::ScreenshotWindow => Self::ScreenshotWindow, - niri_ipc::Action::CloseWindow => Self::CloseWindow, - niri_ipc::Action::FullscreenWindow => Self::FullscreenWindow, + niri_ipc::Action::ScreenshotWindow { id: None } => Self::ScreenshotWindow, + niri_ipc::Action::ScreenshotWindow { id: Some(id) } => Self::ScreenshotWindowById(id), + niri_ipc::Action::CloseWindow { id: None } => Self::CloseWindow, + niri_ipc::Action::CloseWindow { id: Some(id) } => Self::CloseWindowById(id), + niri_ipc::Action::FullscreenWindow { id: None } => Self::FullscreenWindow, + niri_ipc::Action::FullscreenWindow { id: Some(id) } => Self::FullscreenWindowById(id), + niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id), niri_ipc::Action::FocusColumnLeft => Self::FocusColumnLeft, niri_ipc::Action::FocusColumnRight => Self::FocusColumnRight, niri_ipc::Action::FocusColumnFirst => Self::FocusColumnFirst, @@ -1202,9 +1226,17 @@ impl From<niri_ipc::Action> for Action { niri_ipc::Action::FocusWorkspacePrevious => Self::FocusWorkspacePrevious, niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown, niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp, - niri_ipc::Action::MoveWindowToWorkspace { reference } => { - Self::MoveWindowToWorkspace(WorkspaceReference::from(reference)) - } + niri_ipc::Action::MoveWindowToWorkspace { + window_id: None, + reference, + } => Self::MoveWindowToWorkspace(WorkspaceReference::from(reference)), + niri_ipc::Action::MoveWindowToWorkspace { + window_id: Some(window_id), + reference, + } => Self::MoveWindowToWorkspaceById { + window_id, + reference: WorkspaceReference::from(reference), + }, niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown, niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp, niri_ipc::Action::MoveColumnToWorkspace { reference } => { @@ -1224,8 +1256,13 @@ impl From<niri_ipc::Action> for Action { niri_ipc::Action::MoveColumnToMonitorRight => Self::MoveColumnToMonitorRight, niri_ipc::Action::MoveColumnToMonitorDown => Self::MoveColumnToMonitorDown, niri_ipc::Action::MoveColumnToMonitorUp => Self::MoveColumnToMonitorUp, - niri_ipc::Action::SetWindowHeight { change } => Self::SetWindowHeight(change), - niri_ipc::Action::ResetWindowHeight => Self::ResetWindowHeight, + niri_ipc::Action::SetWindowHeight { id: None, change } => Self::SetWindowHeight(change), + niri_ipc::Action::SetWindowHeight { + id: Some(id), + change, + } => Self::SetWindowHeightById { id, change }, + niri_ipc::Action::ResetWindowHeight { id: None } => Self::ResetWindowHeight, + niri_ipc::Action::ResetWindowHeight { id: Some(id) } => Self::ResetWindowHeightById(id), niri_ipc::Action::SwitchPresetColumnWidth => Self::SwitchPresetColumnWidth, niri_ipc::Action::MaximizeColumn => Self::MaximizeColumn, niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change), diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index bf394a50..02465638 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -138,12 +138,42 @@ pub enum Action { Screenshot, /// Screenshot the focused screen. ScreenshotScreen, - /// Screenshot the focused window. - ScreenshotWindow, - /// Close the focused window. - CloseWindow, - /// Toggle fullscreen on the focused window. - FullscreenWindow, + /// Screenshot a window. + #[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))] + ScreenshotWindow { + /// Id of the window to screenshot. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option<u64>, + }, + /// Close a window. + #[cfg_attr(feature = "clap", clap(about = "Close the focused window"))] + CloseWindow { + /// Id of the window to close. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option<u64>, + }, + /// Toggle fullscreen on a window. + #[cfg_attr( + feature = "clap", + clap(about = "Toggle fullscreen on the focused window") + )] + FullscreenWindow { + /// Id of the window to toggle fullscreen of. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option<u64>, + }, + /// Focus a window by id. + FocusWindow { + /// Id of the window to focus. + #[cfg_attr(feature = "clap", arg(long))] + id: u64, + }, /// Focus the column to the left. FocusColumnLeft, /// Focus the column to the right. @@ -226,8 +256,18 @@ pub enum Action { MoveWindowToWorkspaceDown, /// Move the focused window to the workspace above. MoveWindowToWorkspaceUp, - /// Move the focused window to a workspace by reference (index or name). + /// Move a window to a workspace. + #[cfg_attr( + feature = "clap", + clap(about = "Move the focused window to a workspace by reference (index or name)") + )] MoveWindowToWorkspace { + /// Id of the window to move. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + window_id: Option<u64>, + /// Reference (index or name) of the workspace to move the window to. #[cfg_attr(feature = "clap", arg())] reference: WorkspaceReferenceArg, @@ -270,14 +310,34 @@ pub enum Action { MoveColumnToMonitorDown, /// Move the focused column to the monitor above. MoveColumnToMonitorUp, - /// Change the height of the focused window. + /// Change the height of a window. + #[cfg_attr( + feature = "clap", + clap(about = "Change the height of the focused window") + )] SetWindowHeight { + /// Id of the window whose height to set. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option<u64>, + /// How to change the height. #[cfg_attr(feature = "clap", arg())] change: SizeChange, }, - /// Reset the height of the focused window back to automatic. - ResetWindowHeight, + /// Reset the height of a window back to automatic. + #[cfg_attr( + feature = "clap", + clap(about = "Reset the height of the focused window back to automatic") + )] + ResetWindowHeight { + /// Id of the window whose height to reset. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option<u64>, + }, /// Switch between preset column widths. SwitchPresetColumnWidth, /// Toggle the maximized state of the focused column. diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 48c798aa..80dd2262 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -119,10 +119,8 @@ impl XdgShellHandler for State { self.niri.layout.toggle_full_width(); } if intersection.intersects(ResizeEdge::TOP_BOTTOM) { - // FIXME: don't activate once we can pass specific windows to actions. - self.niri.layout.activate_window(&window); self.niri.layer_shell_on_demand_focus = None; - self.niri.layout.reset_window_height(); + self.niri.layout.reset_window_height(Some(&window)); } // FIXME: granular. self.niri.queue_redraw_all(); diff --git a/src/input/mod.rs b/src/input/mod.rs index 91581642..e5820495 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -525,11 +525,29 @@ impl State { }); } } + Action::ScreenshotWindowById(id) => { + let mut windows = self.niri.layout.windows(); + let window = windows.find(|(_, m)| m.id().get() == id); + if let Some((Some(monitor), mapped)) = window { + let output = &monitor.output; + self.backend.with_primary_renderer(|renderer| { + if let Err(err) = self.niri.screenshot_window(renderer, output, mapped) { + warn!("error taking screenshot: {err:?}"); + } + }); + } + } Action::CloseWindow => { if let Some(mapped) = self.niri.layout.focus() { mapped.toplevel().send_close(); } } + Action::CloseWindowById(id) => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + if let Some((_, mapped)) = window { + mapped.toplevel().send_close(); + } + } Action::FullscreenWindow => { let focus = self.niri.layout.focus().map(|m| m.window.clone()); if let Some(window) = focus { @@ -538,6 +556,37 @@ impl State { self.niri.queue_redraw_all(); } } + Action::FullscreenWindowById(id) => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + self.niri.layout.toggle_fullscreen(&window); + // FIXME: granular + self.niri.queue_redraw_all(); + } + } + Action::FocusWindow(id) => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + let active_output = self.niri.layout.active_output().cloned(); + + self.niri.layout.activate_window(&window); + + let new_active = self.niri.layout.active_output().cloned(); + #[allow(clippy::collapsible_if)] + if new_active != active_output { + if !self.maybe_warp_cursor_to_focus_centered() { + self.move_cursor_to_output(&new_active.unwrap()); + } + } else { + self.maybe_warp_cursor_to_focus(); + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } + } Action::SwitchLayout(action) => { let keyboard = &self.niri.seat.get_keyboard().unwrap(); keyboard.with_xkb_state(self, |mut state| match action { @@ -804,15 +853,25 @@ impl State { self.niri.queue_redraw_all(); } Action::MoveWindowToWorkspace(reference) => { - if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference) + if let Some((mut output, index)) = + self.niri.find_output_and_workspace_index(reference) { + // The source output is always the active output, so if the target output is + // also the active output, we don't need to use move_to_output(). + if let Some(active) = self.niri.layout.active_output() { + if output.as_ref() == Some(active) { + output = None; + } + } + if let Some(output) = output { - self.niri.layout.move_to_workspace_on_output(&output, index); + self.niri.layout.move_to_output(None, &output, Some(index)); + if !self.maybe_warp_cursor_to_focus_centered() { self.move_cursor_to_output(&output); } } else { - self.niri.layout.move_to_workspace(index); + self.niri.layout.move_to_workspace(None, index); self.maybe_warp_cursor_to_focus(); } @@ -820,6 +879,51 @@ impl State { self.niri.queue_redraw_all(); } } + Action::MoveWindowToWorkspaceById { + window_id: id, + reference, + } => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + if let Some((output, index)) = + self.niri.find_output_and_workspace_index(reference) + { + let target_was_active = self + .niri + .layout + .active_output() + .map_or(false, |active| output.as_ref() == Some(active)); + + if let Some(output) = output { + self.niri + .layout + .move_to_output(Some(&window), &output, Some(index)); + + // If the active output changed (window was moved and focused). + #[allow(clippy::collapsible_if)] + if !target_was_active + && self.niri.layout.active_output() == Some(&output) + { + if !self.maybe_warp_cursor_to_focus_centered() { + self.move_cursor_to_output(&output); + } + } + } else { + self.niri.layout.move_to_workspace(Some(&window), index); + + // If we focused the target window. + let new_active_win = self.niri.layout.active_window(); + if new_active_win.map_or(false, |(win, _)| win.window == window) { + self.maybe_warp_cursor_to_focus(); + } + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } + } + } Action::MoveColumnToWorkspaceDown => { self.niri.layout.move_column_to_workspace_down(); self.maybe_warp_cursor_to_focus(); @@ -833,8 +937,15 @@ impl State { self.niri.queue_redraw_all(); } Action::MoveColumnToWorkspace(reference) => { - if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference) + if let Some((mut output, index)) = + self.niri.find_output_and_workspace_index(reference) { + if let Some(active) = self.niri.layout.active_output() { + if output.as_ref() == Some(active) { + output = None; + } + } + if let Some(output) = output { self.niri .layout @@ -864,8 +975,15 @@ impl State { self.niri.queue_redraw_all(); } Action::FocusWorkspace(reference) => { - if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference) + if let Some((mut output, index)) = + self.niri.find_output_and_workspace_index(reference) { + if let Some(active) = self.niri.layout.active_output() { + if output.as_ref() == Some(active) { + output = None; + } + } + if let Some(output) = output { self.niri.layout.focus_output(&output); self.niri.layout.switch_workspace(index); @@ -959,7 +1077,7 @@ impl State { } Action::MoveWindowToMonitorLeft => { if let Some(output) = self.niri.output_left() { - self.niri.layout.move_to_output(&output); + self.niri.layout.move_to_output(None, &output, None); self.niri.layout.focus_output(&output); if !self.maybe_warp_cursor_to_focus_centered() { self.move_cursor_to_output(&output); @@ -968,7 +1086,7 @@ impl State { } Action::MoveWindowToMonitorRight => { if let Some(output) = self.niri.output_right() { - self.niri.layout.move_to_output(&output); + self.niri.layout.move_to_output(None, &output, None); self.niri.layout.focus_output(&output); if !self.maybe_warp_cursor_to_focus_centered() { self.move_cursor_to_output(&output); @@ -977,7 +1095,7 @@ impl State { } Action::MoveWindowToMonitorDown => { if let Some(output) = self.niri.output_down() { - self.niri.layout.move_to_output(&output); + self.niri.layout.move_to_output(None, &output, None); self.niri.layout.focus_output(&output); if !self.maybe_warp_cursor_to_focus_centered() { self.move_cursor_to_output(&output); @@ -986,7 +1104,7 @@ impl State { } Action::MoveWindowToMonitorUp => { if let Some(output) = self.niri.output_up() { - self.niri.layout.move_to_output(&output); + self.niri.layout.move_to_output(None, &output, None); self.niri.layout.focus_output(&output); if !self.maybe_warp_cursor_to_focus_centered() { self.move_cursor_to_output(&output); @@ -1033,10 +1151,24 @@ impl State { self.niri.layout.set_column_width(change); } Action::SetWindowHeight(change) => { - self.niri.layout.set_window_height(change); + self.niri.layout.set_window_height(None, change); + } + Action::SetWindowHeightById { id, change } => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + self.niri.layout.set_window_height(Some(&window), change); + } } Action::ResetWindowHeight => { - self.niri.layout.reset_window_height(); + self.niri.layout.reset_window_height(None); + } + Action::ResetWindowHeightById(id) => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + self.niri.layout.reset_window_height(Some(&window)); + } } Action::ShowHotkeyOverlay => { if self.niri.hotkey_overlay.show() { @@ -1366,10 +1498,8 @@ impl State { self.niri.layout.toggle_full_width(); } if intersection.intersects(ResizeEdge::TOP_BOTTOM) { - // FIXME: don't activate once we can pass specific windows - // to actions. self.niri.layout.activate_window(&window); - self.niri.layout.reset_window_height(); + self.niri.layout.reset_window_height(Some(&window)); } // FIXME: granular. self.niri.queue_redraw_all(); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index fd1c5f7e..35f4a2c7 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1092,6 +1092,20 @@ impl<W: LayoutElement> Layout<W> { Some(&mon.workspaces[mon.active_workspace_idx]) } + pub fn active_workspace_mut(&mut self) -> Option<&mut Workspace<W>> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + else { + return None; + }; + + let mon = &mut monitors[*active_monitor_idx]; + Some(&mut mon.workspaces[mon.active_workspace_idx]) + } + pub fn active_window(&self) -> Option<(&W, &Output)> { let MonitorSet::Normal { monitors, @@ -1502,17 +1516,11 @@ impl<W: LayoutElement> Layout<W> { monitor.move_to_workspace_down(); } - pub fn move_to_workspace(&mut self, idx: usize) { + pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) { let Some(monitor) = self.active_monitor() else { return; }; - monitor.move_to_workspace(idx); - } - - pub fn move_to_workspace_on_output(&mut self, output: &Output, idx: usize) { - self.move_to_output(output); - self.focus_output(output); - self.move_to_workspace(idx); + monitor.move_to_workspace(window, idx); } pub fn move_column_to_workspace_up(&mut self) { @@ -1537,7 +1545,7 @@ impl<W: LayoutElement> Layout<W> { } pub fn move_column_to_workspace_on_output(&mut self, output: &Output, idx: usize) { - self.move_to_output(output); + self.move_column_to_output(output); self.focus_output(output); self.move_column_to_workspace(idx); } @@ -1943,18 +1951,38 @@ impl<W: LayoutElement> Layout<W> { monitor.set_column_width(change); } - pub fn set_window_height(&mut self, change: SizeChange) { - let Some(monitor) = self.active_monitor() else { + pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) { + let workspace = if let Some(window) = window { + Some( + self.workspaces_mut() + .find(|ws| ws.has_window(window)) + .unwrap(), + ) + } else { + self.active_workspace_mut() + }; + + let Some(workspace) = workspace else { return; }; - monitor.set_window_height(change); + workspace.set_window_height(window, change); } - pub fn reset_window_height(&mut self) { - let Some(monitor) = self.active_monitor() else { + pub fn reset_window_height(&mut self, window: Option<&W::Id>) { + let workspace = if let Some(window) = window { + Some( + self.workspaces_mut() + .find(|ws| ws.has_window(window)) + .unwrap(), + ) + } else { + self.active_workspace_mut() + }; + + let Some(workspace) = workspace else { return; }; - monitor.reset_window_height(); + workspace.reset_window_height(window); } pub fn focus_output(&mut self, output: &Output) { @@ -1973,7 +2001,12 @@ impl<W: LayoutElement> Layout<W> { } } - pub fn move_to_output(&mut self, output: &Output) { + pub fn move_to_output( + &mut self, + window: Option<&W::Id>, + output: &Output, + target_ws_idx: Option<usize>, + ) { if let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1985,25 +2018,71 @@ impl<W: LayoutElement> Layout<W> { .position(|mon| &mon.output == output) .unwrap(); - let current = &mut monitors[*active_monitor_idx]; - let ws = current.active_workspace(); - if !ws.has_windows() { + let (mon_idx, ws_idx, col_idx, tile_idx) = if let Some(window) = window { + monitors + .iter() + .enumerate() + .find_map(|(mon_idx, mon)| { + mon.workspaces.iter().enumerate().find_map(|(ws_idx, ws)| { + ws.columns.iter().enumerate().find_map(|(col_idx, col)| { + col.tiles + .iter() + .position(|tile| tile.window().id() == window) + .map(|tile_idx| (mon_idx, ws_idx, col_idx, tile_idx)) + }) + }) + }) + .unwrap() + } else { + let mon_idx = *active_monitor_idx; + let mon = &monitors[mon_idx]; + let ws_idx = mon.active_workspace_idx; + let ws = &mon.workspaces[ws_idx]; + + if ws.columns.is_empty() { + return; + } + + let col_idx = ws.active_column_idx; + let tile_idx = ws.columns[col_idx].active_tile_idx; + (mon_idx, ws_idx, col_idx, tile_idx) + }; + + let workspace_idx = target_ws_idx.unwrap_or(monitors[new_idx].active_workspace_idx); + if mon_idx == new_idx && ws_idx == workspace_idx { return; } - let column = &ws.columns[ws.active_column_idx]; + + let mon = &mut monitors[mon_idx]; + let ws = &mut mon.workspaces[ws_idx]; + let column = &ws.columns[col_idx]; let width = column.width; let is_full_width = column.is_full_width; + let activate = mon_idx == *active_monitor_idx + && ws_idx == mon.active_workspace_idx + && col_idx == ws.active_column_idx + && tile_idx == column.active_tile_idx; + let window = ws - .remove_tile_by_idx( - ws.active_column_idx, - column.active_tile_idx, - Transaction::new(), - None, - ) + .remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None) .into_window(); - let workspace_idx = monitors[new_idx].active_workspace_idx; - self.add_window_by_idx(new_idx, workspace_idx, window, true, width, is_full_width); + self.add_window_by_idx( + new_idx, + workspace_idx, + window, + activate, + width, + is_full_width, + ); + + let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else { + unreachable!() + }; + let mon = &mut monitors[mon_idx]; + if mon.workspace_switch.is_none() { + monitors[mon_idx].clean_up_workspaces(); + } } } @@ -2543,6 +2622,41 @@ impl<W: LayoutElement> Layout<W> { let iter_no_outputs = iter_no_outputs.into_iter().flatten(); iter_normal.chain(iter_no_outputs) } + + pub fn workspaces_mut(&mut self) -> impl Iterator<Item = &mut Workspace<W>> + '_ { + let iter_normal; + let iter_no_outputs; + + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + let it = monitors + .iter_mut() + .flat_map(|mon| mon.workspaces.iter_mut()); + + iter_normal = Some(it); + iter_no_outputs = None; + } + MonitorSet::NoOutputs { workspaces } => { + let it = workspaces.iter_mut(); + + iter_normal = None; + iter_no_outputs = Some(it); + } + } + + let iter_normal = iter_normal.into_iter().flatten(); + let iter_no_outputs = iter_no_outputs.into_iter().flatten(); + iter_normal.chain(iter_no_outputs) + } + + pub fn windows(&self) -> impl Iterator<Item = (Option<&Monitor<W>>, &W)> { + self.workspaces() + .flat_map(|(mon, _, ws)| ws.windows().map(move |win| (mon, win))) + } + + pub fn has_window(&self, window: &W::Id) -> bool { + self.windows().any(|(_, win)| win.id() == window) + } } impl<W: LayoutElement> Default for MonitorSet<W> { @@ -2892,19 +3006,39 @@ mod tests { FocusWorkspacePrevious, MoveWindowToWorkspaceDown, MoveWindowToWorkspaceUp, - MoveWindowToWorkspace(#[proptest(strategy = "0..=4usize")] usize), + MoveWindowToWorkspace { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + window_id: Option<usize>, + #[proptest(strategy = "0..=4usize")] + workspace_idx: usize, + }, MoveColumnToWorkspaceDown, MoveColumnToWorkspaceUp, MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize), MoveWorkspaceDown, MoveWorkspaceUp, - MoveWindowToOutput(#[proptest(strategy = "1..=5u8")] u8), + MoveWindowToOutput { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + window_id: Option<usize>, + #[proptest(strategy = "1..=5u8")] + output_id: u8, + #[proptest(strategy = "proptest::option::of(0..=4usize)")] + target_ws_idx: Option<usize>, + }, MoveColumnToOutput(#[proptest(strategy = "1..=5u8")] u8), SwitchPresetColumnWidth, MaximizeColumn, SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), - SetWindowHeight(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), - ResetWindowHeight, + SetWindowHeight { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + id: Option<usize>, + #[proptest(strategy = "arbitrary_size_change()")] + change: SizeChange, + }, + ResetWindowHeight { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + id: Option<usize>, + }, Communicate(#[proptest(strategy = "1..=5usize")] usize), MoveWorkspaceToOutput(#[proptest(strategy = "1..=5u8")] u8), ViewOffsetGestureBegin { @@ -3279,17 +3413,34 @@ mod tests { Op::FocusWorkspacePrevious => layout.switch_workspace_previous(), Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(), Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(), - Op::MoveWindowToWorkspace(idx) => layout.move_to_workspace(idx), + Op::MoveWindowToWorkspace { + window_id, + workspace_idx, + } => { + let window_id = window_id.filter(|id| { + layout + .active_monitor() + .map_or(false, |mon| mon.has_window(id)) + }); + layout.move_to_workspace(window_id.as_ref(), workspace_idx); + } Op::MoveColumnToWorkspaceDown => layout.move_column_to_workspace_down(), Op::MoveColumnToWorkspaceUp => layout.move_column_to_workspace_up(), Op::MoveColumnToWorkspace(idx) => layout.move_column_to_workspace(idx), - Op::MoveWindowToOutput(id) => { + Op::MoveWindowToOutput { + window_id, + output_id: id, + target_ws_idx, + } => { let name = format!("output{id}"); let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { return; }; + let mon = layout.monitor_for_output(&output).unwrap(); - layout.move_to_output(&output); + let window_id = window_id.filter(|id| layout.has_window(id)); + let target_ws_idx = target_ws_idx.filter(|idx| mon.workspaces.len() > *idx); + layout.move_to_output(window_id.as_ref(), &output, target_ws_idx); } Op::MoveColumnToOutput(id) => { let name = format!("output{id}"); @@ -3304,8 +3455,14 @@ mod tests { Op::SwitchPresetColumnWidth => layout.toggle_width(), Op::MaximizeColumn => layout.toggle_full_width(), Op::SetColumnWidth(change) => layout.set_column_width(change), - Op::SetWindowHeight(change) => layout.set_window_height(change), - Op::ResetWindowHeight => layout.reset_window_height(), + Op::SetWindowHeight { id, change } => { + let id = id.filter(|id| layout.has_window(id)); + layout.set_window_height(id.as_ref(), change); + } + Op::ResetWindowHeight { id } => { + let id = id.filter(|id| layout.has_window(id)); + layout.reset_window_height(id.as_ref()); + } Op::Communicate(id) => { let mut update = false; match &mut layout.monitor_set { @@ -3502,8 +3659,14 @@ mod tests { Op::FocusWorkspace(2), Op::MoveWindowToWorkspaceDown, Op::MoveWindowToWorkspaceUp, - Op::MoveWindowToWorkspace(1), - Op::MoveWindowToWorkspace(2), + Op::MoveWindowToWorkspace { + window_id: None, + workspace_idx: 1, + }, + Op::MoveWindowToWorkspace { + window_id: None, + workspace_idx: 2, + }, Op::MoveColumnToWorkspaceDown, Op::MoveColumnToWorkspaceUp, Op::MoveColumnToWorkspace(1), @@ -3575,7 +3738,11 @@ mod tests { bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), min_max_size: Default::default(), }, - Op::MoveWindowToOutput(2), + Op::MoveWindowToOutput { + window_id: None, + output_id: 2, + target_ws_idx: None, + }, Op::FocusOutput(1), Op::Communicate(1), Op::Communicate(2), @@ -3684,9 +3851,18 @@ mod tests { Op::FocusWorkspace(3), Op::MoveWindowToWorkspaceDown, |
