diff options
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/mod.rs | 349 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 24 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 48 |
3 files changed, 396 insertions, 25 deletions
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; + } + } + false + } + pub fn move_left(&mut self) { self.active_workspace().move_left(); } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 4a6ad91c..827228c6 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -3,7 +3,7 @@ use std::iter::{self, zip}; use std::rc::Rc; use std::time::Duration; -use niri_config::{CenterFocusedColumn, PresetWidth, Struts}; +use niri_config::{CenterFocusedColumn, PresetWidth, Struts, Workspace as WorkspaceConfig}; use niri_ipc::SizeChange; use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{layer_map_for_output, Window}; @@ -94,6 +94,9 @@ pub struct Workspace<W: LayoutElement> { /// Configurable properties of the layout. pub options: Rc<Options>, + /// Optional name of this workspace. + pub name: Option<String>, + /// Unique ID of this workspace. id: WorkspaceId, } @@ -313,9 +316,23 @@ impl TileData { impl<W: LayoutElement> Workspace<W> { pub fn new(output: Output, options: Rc<Options>) -> Self { + Self::new_with_config(output, None, options) + } + + pub fn new_with_config( + output: Output, + config: Option<WorkspaceConfig>, + options: Rc<Options>, + ) -> Self { + let original_output = config + .as_ref() + .and_then(|c| c.open_on_output.clone()) + .map(OutputId) + .unwrap_or(OutputId::new(&output)); + let working_area = compute_working_area(&output, options.struts); Self { - original_output: OutputId::new(&output), + original_output, view_size: output_size(&output), working_area, output: Some(output), @@ -329,14 +346,24 @@ impl<W: LayoutElement> Workspace<W> { view_offset_before_fullscreen: None, closing_windows: vec![], options, + name: config.map(|c| c.name.0), id: WorkspaceId::next(), } } - pub fn new_no_outputs(options: Rc<Options>) -> Self { + pub fn new_with_config_no_outputs( + config: Option<WorkspaceConfig>, + options: Rc<Options>, + ) -> Self { + let original_output = OutputId( + config + .clone() + .and_then(|c| c.open_on_output) + .unwrap_or_default(), + ); Self { output: None, - original_output: OutputId(String::new()), + original_output, view_size: Size::from((1280, 720)), working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)), columns: vec![], @@ -349,14 +376,23 @@ impl<W: LayoutElement> Workspace<W> { view_offset_before_fullscreen: None, closing_windows: vec![], options, + name: config.map(|c| c.name.0), id: WorkspaceId::next(), } } + pub fn new_no_outputs(options: Rc<Options>) -> Self { + Self::new_with_config_no_outputs(None, options) + } + pub fn id(&self) -> WorkspaceId { self.id } + pub fn unname(&mut self) { + self.name = None; + } + pub fn advance_animations(&mut self, current_time: Duration) { if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { anim.set_current_time(current_time); @@ -435,6 +471,10 @@ impl<W: LayoutElement> Workspace<W> { .map(Tile::window_mut) } + pub fn current_output(&self) -> Option<&Output> { + self.output.as_ref() + } + pub fn set_output(&mut self, output: Option<Output>) { if self.output == output { return; |
