diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/compositor.rs | 18 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 88 | ||||
| -rw-r--r-- | src/input/mod.rs | 77 | ||||
| -rw-r--r-- | src/layout/mod.rs | 349 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 24 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 48 | ||||
| -rw-r--r-- | src/niri.rs | 50 | ||||
| -rw-r--r-- | src/window/mod.rs | 10 | ||||
| -rw-r--r-- | src/window/unmapped.rs | 4 |
9 files changed, 586 insertions, 82 deletions
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 7d3f0419..2afb8fe8 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -119,22 +119,27 @@ impl CompositorHandler for State { let toplevel = window.toplevel().expect("no X11 support"); - let (rules, width, is_full_width, output) = + let (rules, width, is_full_width, output, workspace_name) = if let InitialConfigureState::Configured { rules, width, is_full_width, output, + workspace_name, } = state { // Check that the output is still connected. let output = output.filter(|o| self.niri.layout.monitor_for_output(o).is_some()); - (rules, width, is_full_width, output) + // Chech that the workspace still exists. + let workspace_name = workspace_name + .filter(|n| self.niri.layout.find_workspace_by_name(n).is_some()); + + (rules, width, is_full_width, output, workspace_name) } else { error!("window map must happen after initial configure"); - (ResolvedWindowRules::empty(), None, false, None) + (ResolvedWindowRules::empty(), None, false, None, None) }; let parent = toplevel @@ -160,6 +165,13 @@ impl CompositorHandler for State { self.niri .layout .add_window_right_of(&p, mapped, width, is_full_width) + } else if let Some(workspace_name) = &workspace_name { + self.niri.layout.add_window_to_named_workspace( + workspace_name, + mapped, + width, + is_full_width, + ) } else if let Some(output) = &output { self.niri .layout diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 44f07839..30df1d5f 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -369,38 +369,52 @@ impl XdgShellHandler for State { width, is_full_width, output, + workspace_name, } => { // Figure out the monitor following a similar logic to initial configure. // FIXME: deduplicate. - let mon = output - .as_ref() - .and_then(|o| self.niri.layout.monitor_for_output(o)) - .map(|mon| (mon, false)) - // If not, check if we have a parent with a monitor. - .or_else(|| { - toplevel - .parent() - .and_then(|parent| self.niri.layout.find_window_and_output(&parent)) - .map(|(_win, output)| output) - .and_then(|o| self.niri.layout.monitor_for_output(o)) - .map(|mon| (mon, true)) - }) - // If not, fall back to the active monitor. - .or_else(|| { - self.niri - .layout - .active_monitor_ref() - .map(|mon| (mon, false)) - }); + let mon = workspace_name + .as_deref() + .and_then(|name| self.niri.layout.monitor_for_workspace(name)) + .map(|mon| (mon, false)); + + let mon = mon.or_else(|| { + output + .as_ref() + .and_then(|o| self.niri.layout.monitor_for_output(o)) + .map(|mon| (mon, false)) + // If not, check if we have a parent with a monitor. + .or_else(|| { + toplevel + .parent() + .and_then(|parent| { + self.niri.layout.find_window_and_output(&parent) + }) + .map(|(_win, output)| output) + .and_then(|o| self.niri.layout.monitor_for_output(o)) + .map(|mon| (mon, true)) + }) + // If not, fall back to the active monitor. + .or_else(|| { + self.niri + .layout + .active_monitor_ref() + .map(|mon| (mon, false)) + }) + }); *output = mon .filter(|(_, parent)| !parent) .map(|(mon, _)| mon.output.clone()); let mon = mon.map(|(mon, _)| mon); - let ws = mon - .map(|mon| mon.active_workspace_ref()) - .or_else(|| self.niri.layout.active_workspace()); + let ws = workspace_name + .as_deref() + .and_then(|name| mon.map(|mon| mon.find_named_workspace(name))) + .unwrap_or_else(|| { + mon.map(|mon| mon.active_workspace_ref()) + .or_else(|| self.niri.layout.active_workspace()) + }); if let Some(ws) = ws { toplevel.with_pending_state(|state| { @@ -577,12 +591,20 @@ impl State { return; }; - // Pick the target monitor. First, check if we had an output set in the window rules. + // Pick the target monitor. First, check if we had a workspace set in the window rules. let mon = rules - .open_on_output + .open_on_workspace .as_deref() - .and_then(|name| self.niri.output_by_name.get(name)) - .and_then(|o| self.niri.layout.monitor_for_output(o)); + .and_then(|name| self.niri.layout.monitor_for_workspace(name)); + + // If not, check if we had an output set in the window rules. + let mon = mon.or_else(|| { + rules + .open_on_output + .as_deref() + .and_then(|name| self.niri.output_by_name.get(name)) + .and_then(|o| self.niri.layout.monitor_for_output(o)) + }); // If not, check if the window requested one for fullscreen. let mon = mon.or_else(|| { @@ -622,9 +644,14 @@ impl State { let is_full_width = rules.open_maximized.unwrap_or(false); // Tell the surface the preferred size and bounds for its likely output. - let ws = mon - .map(|mon| mon.active_workspace_ref()) - .or_else(|| self.niri.layout.active_workspace()); + let ws = rules + .open_on_workspace + .as_deref() + .and_then(|name| mon.map(|mon| mon.find_named_workspace(name))) + .unwrap_or_else(|| { + mon.map(|mon| mon.active_workspace_ref()) + .or_else(|| self.niri.layout.active_workspace()) + }); if let Some(ws) = ws { // Set a fullscreen state based on window request and window rule. @@ -663,6 +690,7 @@ impl State { width, is_full_width, output, + workspace_name: ws.and_then(|w| w.name.clone()), }; toplevel.send_configure(); diff --git a/src/input/mod.rs b/src/input/mod.rs index 8206890c..1984b23c 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -587,12 +587,22 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveWindowToWorkspace(idx) => { - let idx = idx.saturating_sub(1) as usize; - self.niri.layout.move_to_workspace(idx); - self.maybe_warp_cursor_to_focus(); - // FIXME: granular - self.niri.queue_redraw_all(); + Action::MoveWindowToWorkspace(reference) => { + if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference) + { + if let Some(output) = output { + self.niri.layout.move_to_workspace_on_output(&output, index); + if !self.maybe_warp_cursor_to_focus_centered() { + self.move_cursor_to_output(&output); + } + } else { + self.niri.layout.move_to_workspace(index); + self.maybe_warp_cursor_to_focus(); + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } } Action::MoveColumnToWorkspaceDown => { self.niri.layout.move_column_to_workspace_down(); @@ -606,12 +616,24 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveColumnToWorkspace(idx) => { - let idx = idx.saturating_sub(1) as usize; - self.niri.layout.move_column_to_workspace(idx); - self.maybe_warp_cursor_to_focus(); - // FIXME: granular - self.niri.queue_redraw_all(); + Action::MoveColumnToWorkspace(reference) => { + if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference) + { + if let Some(output) = output { + self.niri + .layout + .move_column_to_workspace_on_output(&output, index); + if !self.maybe_warp_cursor_to_focus_centered() { + self.move_cursor_to_output(&output); + } + } else { + self.niri.layout.move_column_to_workspace(index); + self.maybe_warp_cursor_to_focus(); + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } } Action::FocusWorkspaceDown => { self.niri.layout.switch_workspace_down(); @@ -625,19 +647,28 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } - Action::FocusWorkspace(idx) => { - let idx = idx.saturating_sub(1) as usize; + Action::FocusWorkspace(reference) => { + if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference) + { + if let Some(output) = output { + self.niri.layout.focus_output(&output); + self.niri.layout.switch_workspace(index); + if !self.maybe_warp_cursor_to_focus_centered() { + self.move_cursor_to_output(&output); + } + } else { + let config = &self.niri.config; + if config.borrow().input.workspace_auto_back_and_forth { + self.niri.layout.switch_workspace_auto_back_and_forth(index); + } else { + self.niri.layout.switch_workspace(index); + } + self.maybe_warp_cursor_to_focus(); + } - let config = &self.niri.config; - if config.borrow().input.workspace_auto_back_and_forth { - self.niri.layout.switch_workspace_auto_back_and_forth(idx); - } else { - self.niri.layout.switch_workspace(idx); + // FIXME: granular + self.niri.queue_redraw_all(); } - - self.maybe_warp_cursor_to_focus(); - // FIXME: granular - self.niri.queue_redraw_all(); } Action::FocusWorkspacePrevious => { self.niri.layout.switch_workspace_previous(); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index d091c024..0eb451e6 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -34,7 +34,7 @@ use std::mem; use std::rc::Rc; use std::time::Duration; -use niri_config::{CenterFocusedColumn, Config, Struts}; +use niri_config::{CenterFocusedColumn, Config, Struts, Workspace as WorkspaceConfig}; use niri_ipc::SizeChange; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; @@ -279,7 +279,7 @@ impl Options { impl<W: LayoutElement> Layout<W> { pub fn new(config: &Config) -> Self { - Self::with_options(Options::from_config(config)) + Self::with_options_and_workspaces(config, Options::from_config(config)) } pub fn with_options(options: Options) -> Self { @@ -289,6 +289,21 @@ impl<W: LayoutElement> Layout<W> { } } + fn with_options_and_workspaces(config: &Config, options: Options) -> Self { + let opts = Rc::new(options); + + let workspaces = config + .workspaces + .iter() + .map(|ws| Workspace::new_with_config_no_outputs(Some(ws.clone()), opts.clone())) + .collect(); + + Self { + monitor_set: MonitorSet::NoOutputs { workspaces }, + options: opts, + } + } + pub fn add_output(&mut self, output: Output) { let id = OutputId::new(&output); @@ -318,7 +333,7 @@ impl<W: LayoutElement> Layout<W> { // The user could've closed a window while remaining on this workspace, on // another monitor. However, we will add an empty workspace in the end // instead. - if ws.has_windows() { + if ws.has_windows() || ws.name.is_some() { workspaces.push(ws); } @@ -463,6 +478,67 @@ impl<W: LayoutElement> Layout<W> { } } + /// Adds a new window to the layout on a specific workspace. + pub fn add_window_to_named_workspace( + &mut self, + workspace_name: &str, + window: W, + width: Option<ColumnWidth>, + is_full_width: bool, + ) -> Option<&Output> { + let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + if let ColumnWidth::Fixed(w) = &mut width { + let rules = window.rules(); + let border_config = rules.border.resolve_against(self.options.border); + if !border_config.off { + *w += border_config.width as i32 * 2; + } + } + + match &mut self.monitor_set { + MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } => { + let (mon_idx, mon, ws_idx) = monitors + .iter_mut() + .enumerate() + .find_map(|(mon_idx, mon)| { + mon.find_named_workspace_index(workspace_name) + .map(move |ws_idx| (mon_idx, mon, ws_idx)) + }) + .unwrap(); + + // Don't steal focus from an active fullscreen window. + let mut activate = true; + let ws = &mon.workspaces[ws_idx]; + if mon_idx == *active_monitor_idx + && !ws.columns.is_empty() + && ws.columns[ws.active_column_idx].is_fullscreen + { + activate = false; + } + + // Don't activate if on a different workspace. + if mon.active_workspace_idx != ws_idx { + activate = false; + } + + mon.add_window(ws_idx, window, activate, width, is_full_width); + Some(&mon.output) + } + MonitorSet::NoOutputs { workspaces } => { + let ws = workspaces + .iter_mut() + .find(|ws| ws.name.as_deref() == Some(workspace_name)) + .unwrap(); + ws.add_window(window, true, width, is_full_width); + None + } + } + } + pub fn add_column_by_idx( &mut self, monitor_idx: usize, @@ -649,6 +725,7 @@ impl<W: LayoutElement> Layout<W> { && idx != mon.active_workspace_idx && idx != mon.workspaces.len() - 1 && mon.workspace_switch.is_none() + && mon.workspaces[idx].name.is_none() { mon.workspaces.remove(idx); @@ -668,7 +745,7 @@ impl<W: LayoutElement> Layout<W> { rv = Some(ws.remove_window(window)); // Clean up empty workspaces. - if !ws.has_windows() { + if !ws.has_windows() && workspaces[idx].name.is_none() { workspaces.remove(idx); } @@ -718,6 +795,63 @@ impl<W: LayoutElement> Layout<W> { None } + pub fn find_workspace_by_name(&self, workspace_name: &str) -> Option<(usize, &Workspace<W>)> { + match &self.monitor_set { + MonitorSet::Normal { ref monitors, .. } => { + for mon in monitors { + if let Some((index, workspace)) = mon + .workspaces + .iter() + .enumerate() + .find(|(_, w)| w.name.as_deref() == Some(workspace_name)) + { + return Some((index, workspace)); + } + } + } + MonitorSet::NoOutputs { workspaces } => { + if let Some((index, workspace)) = workspaces + .iter() + .enumerate() + .find(|(_, w)| w.name.as_deref() == Some(workspace_name)) + { + return Some((index, workspace)); + } + } + } + + None + } + + pub fn unname_workspace(&mut self, workspace_name: &str) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + if mon.unname_workspace(workspace_name) { + if mon.workspace_switch.is_none() { + mon.clean_up_workspaces(); + } + return; + } + } + } + MonitorSet::NoOutputs { workspaces } => { + for (idx, ws) in workspaces.iter_mut().enumerate() { + if ws.name.as_deref() == Some(workspace_name) { + ws.unname(); + + // Clean up empty workspaces. + if !ws.has_windows() { + workspaces.remove(idx); + } + + return; + } + } + } + } + } + pub fn find_window_and_output_mut( &mut self, wl_surface: &WlSurface, @@ -970,6 +1104,19 @@ impl<W: LayoutElement> Layout<W> { monitors.iter().find(|monitor| &monitor.output == output) } + pub fn monitor_for_workspace(&self, workspace_name: &str) -> Option<&Monitor<W>> { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { + return None; + }; + + monitors.iter().find(|monitor| { + monitor + .workspaces + .iter() + .any(|ws| ws.name.as_deref() == Some(workspace_name)) + }) + } + pub fn outputs(&self) -> impl Iterator<Item = &Output> + '_ { let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { &monitors[..] @@ -1127,6 +1274,12 @@ impl<W: LayoutElement> Layout<W> { 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); + } + pub fn move_column_to_workspace_up(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -1148,6 +1301,12 @@ impl<W: LayoutElement> Layout<W> { monitor.move_column_to_workspace(idx); } + pub fn move_column_to_workspace_on_output(&mut self, output: &Output, idx: usize) { + self.move_to_output(output); + self.focus_output(output); + self.move_column_to_workspace(idx); + } + pub fn switch_workspace_up(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -1257,6 +1416,7 @@ impl<W: LayoutElement> Layout<W> { use crate::layout::monitor::WorkspaceSwitch; let mut seen_workspace_id = HashSet::new(); + let mut seen_workspace_name = HashSet::new(); let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set { MonitorSet::Normal { @@ -1267,8 +1427,8 @@ impl<W: LayoutElement> Layout<W> { MonitorSet::NoOutputs { workspaces } => { for workspace in workspaces { assert!( - workspace.has_windows(), - "with no outputs there cannot be empty workspaces" + workspace.has_windows() || workspace.name.is_some(), + "with no outputs there cannot be empty unnamed workspaces" ); assert_eq!( @@ -1281,6 +1441,13 @@ impl<W: LayoutElement> Layout<W> { "workspace id must be unique" ); + if let Some(name) = &workspace.name { + assert!( + seen_workspace_name.insert(name), + "workspace name must be unique" + ); + } + workspace.verify_invariants(); } @@ -1343,14 +1510,19 @@ impl<W: LayoutElement> Layout<W> { "monitor must have an empty workspace in the end" ); + assert!( + monitor.workspaces.last().unwrap().name.is_none(), + "monitor must have an unnamed workspace in the end" + ); + // If there's no workspace switch in progress, there can't be any non-last non-active // empty workspaces. if monitor.workspace_switch.is_none() { for (idx, ws) in monitor.workspaces.iter().enumerate().rev().skip(1) { if idx != monitor.active_workspace_idx { assert!( - !ws.columns.is_empty(), - "non-active workspace can't be empty except the last one" + !ws.columns.is_empty() || ws.name.is_some(), + "non-active workspace can't be empty and unnamed except the last one" ); } } @@ -1370,6 +1542,13 @@ impl<W: LayoutElement> Layout<W> { "workspace id must be unique" ); + if let Some(name) = &workspace.name { + assert!( + seen_workspace_name.insert(name), + "workspace name must be unique" + ); + } + workspace.verify_invariants(); } } @@ -1448,6 +1627,48 @@ impl<W: LayoutElement> Layout<W> { } } + pub fn ensure_named_workspace(&mut self, ws_config: &WorkspaceConfig) { + if self.find_workspace_by_name(&ws_config.name.0).is_some() { + return; + } + + let options = self.options.clone(); + + match &mut self.monitor_set { + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } => { + let mon_idx = ws_config + .open_on_output + .as_deref() + .map(|name| { + monitors + .iter_mut() + .position(|monitor| monitor.output.name().eq_ignore_ascii_case(name)) + .unwrap_or(*primary_idx) + }) + .unwrap_or(*active_monitor_idx); + let mon = &mut monitors[mon_idx]; + + let ws = Workspace::new_with_config( + mon.output.clone(), + Some(ws_config.clone()), + options, + ); + mon.workspaces.insert(0, ws); + mon.active_workspace_idx += 1; + mon.workspace_switch = None; + mon.clean_up_workspaces(); + } + MonitorSet::NoOutputs { workspaces } => { + let ws = Workspace::new_with_config_no_outputs(Some(ws_config.clone()), options); + workspaces.insert(0, ws); + } + } + } + pub fn update_config(&mut self, config: &Config) { let options = Rc::new(Options::from_config(config)); @@ -2053,6 +2274,7 @@ impl<W: LayoutElement> Default for MonitorSet<W> { mod tests { use std::cell::Cell; + use niri_config::WorkspaceName; use proptest::prelude::*; use proptest_derive::Arbitrary; use smithay::output::{Mode, PhysicalProperties, Subpixel}; @@ -2284,6 +2506,16 @@ mod tests { AddOutput(#[proptest(strategy = "1..=5usize")] usize), RemoveOutput(#[proptest(strategy = "1..=5usize")] usize), FocusOutput(#[proptest(strategy = "1..=5usize")] usize), + AddNamedWorkspace { + #[proptest(strategy = "1..=5usize")] + ws_name: usize, + #[proptest(strategy = "prop::option::of(1..=5usize)")] + output_name: Option<usize>, + }, + UnnameWorkspace { + #[proptest(strategy = "1..=5usize")] + ws_name: usize, + }, AddWindow { #[proptest(strategy = "1..=5usize")] id: usize, @@ -2302,6 +2534,16 @@ mod tests { #[proptest(strategy = "arbitrary_min_max_size()")] min_max_size: (Size<i32, Logical>, Size<i32, Logical>), }, + AddWindowToNamedWorkspace { + #[proptest(strategy = "1..=5usize")] + id: usize, + #[proptest(strategy = "1..=5usize")] + ws_name: usize, + #[proptest(strategy = "arbitrary_bbox()")] + bbox: Rectangle<i32, Logical>, + #[proptest(strategy = "arbitrary_min_max_size()")] + min_max_size: (Size<i32, Logical>, Size<i32, Logical>), + }, CloseWindow(#[proptest(strategy = "1..=5usize")] usize), FullscreenWindow(#[proptest(strategy = "1..=5usize")] usize), FocusColumnLeft, @@ -2438,6 +2680,18 @@ mod tests { layout.focus_output(&output); } + Op::AddNamedWorkspace { + ws_name, + output_name, + } => { + layout.ensure_named_workspace(&WorkspaceConfig { + name: WorkspaceName(format!("ws{ws_name}")), + open_on_output: output_name.map(|name| format!("output{name}")), + }); + } + Op::UnnameWorkspace { ws_name } => { + layout.unname_workspace(&format!("ws{ws_name}")); + } Op::AddWindow { id, bbox, @@ -2515,6 +2769,53 @@ mod tests { let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1); layout.add_window_right_of(&right_of_id, win, None, false); } + Op::AddWindowToNamedWorkspace { + id, + ws_name, + bbox, + min_max_size, + } => { + let ws_name = format!("ws{ws_name}"); + let mut found_workspace = false; + + match &mut layout.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + for win in ws.windows() { + if win.0.id == id { + return; + } + } + + if ws.name.as_deref() == Some(&ws_name) { + found_workspace = true; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + for win in ws.windows() { + if win.0.id == id { + return; + } + } + + if ws.name.as_deref() == Some(&ws_name) { + found_workspace = true; + } + } + } + } + + if !found_workspace { + return; + } + + let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1); + layout.add_window_to_named_workspace(&ws_name, win, None, false); + } Op::CloseWindow(id) => { layout.remove_window(&id); } @@ -2702,6 +3003,11 @@ mod tests { Op::FocusOutput(0), Op::FocusOutput(1), Op::FocusOutput(2), + Op::AddNamedWorkspace { + ws_name: 1, + output_name: Some(1), + }, + Op::UnnameWorkspace { ws_name: 1 }, Op::AddWindow { id: 0, bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), @@ -2712,20 +3018,15 @@ mod tests { bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), min_max_size: Default::default(), }, - Op::AddWindow { + Op::AddWindowRightOf { id: 2, + right_of_id: 1, bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), min_max_size: Default::default(), }, - Op::AddWindowRightOf { + Op::AddWindowToNamedWorkspace { id: 3, - right_of_id: 0, - bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), - min_max_size: Default::default(), - }, - Op::AddWindowRightOf { - id: 4, - right_of_id: 1, + ws_name: 1, bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), min_max_size: Default::default(), }, @@ -2750,17 +3051,14 @@ mod tests { Op::FocusWorkspaceUp, Op::FocusWorkspace(1), Op::FocusWorkspace(2), - Op::FocusWorkspace(3), Op::MoveWindowToWorkspaceDown, Op::MoveWindowToWorkspaceUp, Op::MoveWindowToWorkspace(1), Op::MoveWindowToWorkspace(2), - Op::MoveWindowToWorkspace(3), Op::MoveColumnToWorkspaceDown, Op::MoveColumnToWorkspaceUp, Op::MoveColumnToWorkspace(1), Op::MoveColumnToWorkspace(2), - Op::MoveColumnToWorkspace(3), Op::MoveWindowDown, Op::MoveWindowDownOrToWorkspaceDown, Op::MoveWindowUp, @@ -2847,6 +3145,11 @@ mod tests { Op::FocusOutput(0), Op::FocusOutput(1), Op::FocusOutput(2), + Op::AddNamedWorkspace { + ws_name: 1, + output_name: Some(1), + }, + Op::UnnameWorkspace { ws_name: 1 }, Op::AddWindow { id: 0, bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), @@ -2874,6 +3177,12 @@ mod tests { bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), min_max_size: Default::default(), }, + Op::AddWindowToNamedWorkspace { + id: 5, + ws_name: 1, + bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), + min_max_size: Default::default(), + }, Op::CloseWindow(0), Op::CloseWindow(1), Op::CloseWindow(2), diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 390abf0c..9dcc552e 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -103,6 +103,18 @@ impl<W: LayoutElement> Monitor<W> { &self.workspaces[self.active_workspace_idx] } + pub fn find_named_workspace(&self, workspace_name: &str) -> Option<&Workspace<W>> { + self.workspaces + .iter() + .find(|w| w.name.as_deref() == Some(workspace_name)) + } + + pub fn find_named_workspace_index(&self, workspace_name: &str) -> Option<usize> { + self.workspaces + .iter() + .position(|w| w.name.as_deref() == Some(workspace_name)) + } + pub fn active_workspace(&mut self) -> &mut Workspace<W> { &mut self.workspaces[self.active_workspace_idx] } @@ -204,7 +216,7 @@ impl<W: LayoutElement> Monitor<W> { continue; } - if !self.workspaces[idx].has_windows() { + if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() { self.workspaces.remove(idx); if self.active_workspace_idx > idx { self.active_workspace_idx -= 1; @@ -213,6 +225,16 @@ impl<W: LayoutElement> Monitor<W> { } } + pub fn unname_workspace(&mut self, workspace_name: &str) -> bool { + for ws in &mut self.workspaces { + if ws.name.as_deref() == Some(workspace_name) { + ws.unname(); + return true; + }< |
