aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--niri-config/src/lib.rs53
-rw-r--r--niri-ipc/src/lib.rs80
-rw-r--r--src/handlers/xdg_shell.rs4
-rw-r--r--src/input/mod.rs158
-rw-r--r--src/layout/mod.rs312
-rw-r--r--src/layout/monitor.rs75
-rw-r--r--src/layout/workspace.rs38
-rw-r--r--src/niri.rs11
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,