Implement by-id window addressing in IPC and CLI, fix move-column-to-workspace

This is a JSON-breaking change for the IPC actions that changed from
unit variants to struct variants. Unfortunately, I couldn't find a way
with serde to both preserve a single variant, and make it serialize to
the old value when the new field is None. I don't think anyone is using
these actions from JSON at the moment, so this breaking change is fine.
This commit is contained in:
Ivan Molodetskikh
2024-09-06 15:10:01 +03:00
parent cb5d97f600
commit dcb29efce5
8 changed files with 598 additions and 133 deletions
+45 -8
View File
@@ -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),
+70 -10
View File
@@ -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.
+1 -3
View File
@@ -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();
+144 -14
View File
@@ -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();
+260 -52
View File
@@ -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,
Op::MoveWindowToWorkspaceUp,
Op::MoveWindowToWorkspace(1),
Op::MoveWindowToWorkspace(2),
Op::MoveWindowToWorkspace(3),
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 1,
},
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 2,
},
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 3,
},
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
@@ -3798,7 +3974,18 @@ mod tests {
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::MoveWindowToWorkspace(2),
Op::AddOutput(2),
Op::FocusOutput(2),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::RemoveOutput(1),
Op::MoveWindowToWorkspace {
window_id: Some(0),
workspace_idx: 2,
},
];
let mut layout = Layout::default();
@@ -3810,7 +3997,7 @@ mod tests {
unreachable!()
};
assert!(monitors[0].workspaces[0].has_windows());
assert!(monitors[0].workspaces[1].has_windows());
}
#[test]
@@ -3847,7 +4034,10 @@ mod tests {
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::SetWindowHeight(SizeChange::AdjustProportion(-1e129)),
Op::SetWindowHeight {
id: None,
change: SizeChange::AdjustProportion(-1e129),
},
];
let mut options = Options::default();
@@ -4311,9 +4501,15 @@ mod tests {
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft,
Op::SetWindowHeight(SizeChange::SetFixed(100)),
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(100),
},
Op::FocusWindowUp,
Op::SetWindowHeight(SizeChange::SetFixed(200)),
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(200),
},
];
check_ops(&ops);
@@ -4340,10 +4536,16 @@ mod tests {
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft,
Op::SetWindowHeight(SizeChange::SetFixed(100)),
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(100),
},
Op::Communicate(2),
Op::FocusWindowUp,
Op::SetWindowHeight(SizeChange::SetFixed(200)),
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(200),
},
Op::Communicate(1),
Op::CloseWindow(0),
Op::CloseWindow(1),
@@ -4373,10 +4575,16 @@ mod tests {
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft,
Op::SetWindowHeight(SizeChange::SetFixed(100)),
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(100),
},
Op::Communicate(2),
Op::FocusWindowUp,
Op::SetWindowHeight(SizeChange::SetFixed(200)),
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(200),
},
Op::Communicate(1),
Op::CloseWindow(0),
Op::FullscreenWindow(1),
+44 -31
View File
@@ -133,6 +133,14 @@ impl<W: LayoutElement> Monitor<W> {
&mut self.workspaces[self.active_workspace_idx]
}
pub fn windows(&self) -> impl Iterator<Item = &W> {
self.workspaces.iter().flat_map(|ws| ws.windows())
}
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|win| win.id() == window)
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
@@ -489,8 +497,32 @@ impl<W: LayoutElement> Monitor<W> {
self.add_window(new_idx, window, true, width, is_full_width);
}
pub fn move_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
let (source_workspace_idx, col_idx, tile_idx) = if let Some(window) = window {
self.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| (ws_idx, col_idx, tile_idx))
})
})
.unwrap()
} else {
let ws_idx = self.active_workspace_idx;
let ws = &self.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;
(ws_idx, col_idx, tile_idx)
};
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
@@ -498,28 +530,22 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let column = &workspace.columns[col_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let activate = source_workspace_idx == self.active_workspace_idx
&& col_idx == workspace.active_column_idx
&& tile_idx == column.active_tile_idx;
let window = workspace
.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
)
.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None)
.into_window();
self.add_window(new_idx, window, true, width, is_full_width);
self.add_window(new_idx, window, activate, width, is_full_width);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
if self.workspace_switch.is_none() {
self.clean_up_workspaces();
}
}
pub fn move_column_to_workspace_up(&mut self) {
@@ -571,11 +597,6 @@ impl<W: LayoutElement> Monitor<W> {
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn switch_workspace_up(&mut self) {
@@ -727,14 +748,6 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().set_column_width(change);
}
pub fn set_window_height(&mut self, change: SizeChange) {
self.active_workspace().set_window_height(change);
}
pub fn reset_window_height(&mut self) {
self.active_workspace().reset_window_height();
}
pub fn move_workspace_down(&mut self) {
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
+32 -6
View File
@@ -2303,24 +2303,50 @@ impl<W: LayoutElement> Workspace<W> {
cancel_resize_for_column(&mut self.interactive_resize, col);
}
pub fn set_window_height(&mut self, change: SizeChange) {
pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) {
if self.columns.is_empty() {
return;
}
let col = &mut self.columns[self.active_column_idx];
col.set_window_height(change, None, true);
let (col, tile_idx) = if let Some(window) = window {
self.columns
.iter_mut()
.find_map(|col| {
col.tiles
.iter()
.position(|tile| tile.window().id() == window)
.map(|tile_idx| (col, Some(tile_idx)))
})
.unwrap()
} else {
(&mut self.columns[self.active_column_idx], None)
};
col.set_window_height(change, tile_idx, true);
cancel_resize_for_column(&mut self.interactive_resize, col);
}
pub fn reset_window_height(&mut self) {
pub fn reset_window_height(&mut self, window: Option<&W::Id>) {
if self.columns.is_empty() {
return;
}
let col = &mut self.columns[self.active_column_idx];
col.reset_window_height(None, true);
let (col, tile_idx) = if let Some(window) = window {
self.columns
.iter_mut()
.find_map(|col| {
col.tiles
.iter()
.position(|tile| tile.window().id() == window)
.map(|tile_idx| (col, Some(tile_idx)))
})
.unwrap()
} else {
(&mut self.columns[self.active_column_idx], None)
};
col.reset_window_height(tile_idx, true);
cancel_resize_for_column(&mut self.interactive_resize, col);
}
+2 -9
View File
@@ -2447,15 +2447,8 @@ impl Niri {
}
};
// FIXME: when we do fixes for no connected outputs, this will need fixing too.
let active_workspace = self.layout.active_workspace()?;
if target_workspace.current_output() == active_workspace.current_output() {
return Some((None, target_workspace_index));
}
let target_output = target_workspace.current_output()?;
Some((Some(target_output.clone()), target_workspace_index))
let target_output = target_workspace.current_output();
Some((target_output.cloned(), target_workspace_index))
}
pub fn output_down(&self) -> Option<Output> {