diff options
| -rw-r--r-- | niri-config/src/lib.rs | 170 | ||||
| -rw-r--r-- | niri-ipc/src/lib.rs | 45 | ||||
| -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 |
11 files changed, 784 insertions, 99 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index be93f9a7..355007f6 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -11,7 +11,7 @@ use bitflags::bitflags; use knuffel::errors::DecodeError; use knuffel::Decode as _; use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler}; -use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform}; +use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg}; use regex::Regex; use smithay::input::keyboard::keysyms::KEY_NoSymbol; use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE}; @@ -52,6 +52,8 @@ pub struct Config { pub binds: Binds, #[knuffel(child, default)] pub debug: DebugConfig, + #[knuffel(children(name = "workspace"))] + pub workspaces: Vec<Workspace>, } // FIXME: Add other devices. @@ -693,6 +695,17 @@ pub struct EnvironmentVariable { pub value: Option<String>, } +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +pub struct Workspace { + #[knuffel(argument)] + pub name: WorkspaceName, + #[knuffel(child, unwrap(argument))] + pub open_on_output: Option<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WorkspaceName(pub String); + #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] pub struct WindowRule { #[knuffel(children(name = "match"))] @@ -706,6 +719,8 @@ pub struct WindowRule { #[knuffel(child, unwrap(argument))] pub open_on_output: Option<String>, #[knuffel(child, unwrap(argument))] + pub open_on_workspace: Option<String>, + #[knuffel(child, unwrap(argument))] pub open_maximized: Option<bool>, #[knuffel(child, unwrap(argument))] pub open_fullscreen: Option<bool>, @@ -890,14 +905,14 @@ pub enum Action { CenterColumn, FocusWorkspaceDown, FocusWorkspaceUp, - FocusWorkspace(#[knuffel(argument)] u8), + FocusWorkspace(#[knuffel(argument)] WorkspaceReference), FocusWorkspacePrevious, MoveWindowToWorkspaceDown, MoveWindowToWorkspaceUp, - MoveWindowToWorkspace(#[knuffel(argument)] u8), + MoveWindowToWorkspace(#[knuffel(argument)] WorkspaceReference), MoveColumnToWorkspaceDown, MoveColumnToWorkspaceUp, - MoveColumnToWorkspace(#[knuffel(argument)] u8), + MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference), MoveWorkspaceDown, MoveWorkspaceUp, FocusMonitorLeft, @@ -962,14 +977,20 @@ impl From<niri_ipc::Action> for Action { niri_ipc::Action::CenterColumn => Self::CenterColumn, niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown, niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp, - niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index), + niri_ipc::Action::FocusWorkspace { reference } => { + Self::FocusWorkspace(WorkspaceReference::from(reference)) + } niri_ipc::Action::FocusWorkspacePrevious => Self::FocusWorkspacePrevious, niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown, niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp, - niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index), + niri_ipc::Action::MoveWindowToWorkspace { reference } => { + Self::MoveWindowToWorkspace(WorkspaceReference::from(reference)) + } niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown, niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp, - niri_ipc::Action::MoveColumnToWorkspace { index } => Self::MoveColumnToWorkspace(index), + niri_ipc::Action::MoveColumnToWorkspace { reference } => { + Self::MoveColumnToWorkspace(WorkspaceReference::from(reference)) + } niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown, niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp, niri_ipc::Action::FocusMonitorLeft => Self::FocusMonitorLeft, @@ -1002,6 +1023,59 @@ impl From<niri_ipc::Action> for Action { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum WorkspaceReference { + Index(u8), + Name(String), +} + +impl From<WorkspaceReferenceArg> for WorkspaceReference { + fn from(reference: WorkspaceReferenceArg) -> WorkspaceReference { + match reference { + WorkspaceReferenceArg::Index(i) => Self::Index(i), + WorkspaceReferenceArg::Name(n) => Self::Name(n), + } + } +} + +impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceReference { + fn type_check( + type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>, + ctx: &mut knuffel::decode::Context<S>, + ) { + if let Some(type_name) = &type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + } + + fn raw_decode( + val: &knuffel::span::Spanned<knuffel::ast::Literal, S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<WorkspaceReference, DecodeError<S>> { + match &**val { + knuffel::ast::Literal::String(ref s) => Ok(WorkspaceReference::Name(s.clone().into())), + knuffel::ast::Literal::Int(ref value) => match value.try_into() { + Ok(v) => Ok(WorkspaceReference::Index(v)), + Err(e) => { + ctx.emit_error(DecodeError::conversion(val, e)); + Ok(WorkspaceReference::Index(0)) + } + }, + _ => { + ctx.emit_error(DecodeError::unsupported( + val, + "Unsupported value, only numbers and strings are recognized", + )); + Ok(WorkspaceReference::Index(0)) + } + } + } +} + #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct DebugConfig { #[knuffel(child, unwrap(argument))] @@ -1409,6 +1483,54 @@ where } } +impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceName { + fn type_check( + type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>, + ctx: &mut knuffel::decode::Context<S>, + ) { + if let Some(type_name) = &type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + } + + fn raw_decode( + val: &knuffel::span::Spanned<knuffel::ast::Literal, S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<WorkspaceName, DecodeError<S>> { + #[derive(Debug)] + struct WorkspaceNameSet(HashSet<String>); + match &**val { + knuffel::ast::Literal::String(ref s) => { + let mut name_set: HashSet<String> = match ctx.get::<WorkspaceNameSet>() { + Some(h) => h.0.clone(), + None => HashSet::new(), + }; + if !name_set.insert(s.clone().to_string()) { + ctx.emit_error(DecodeError::unexpected( + val, + "named workspace", + format!("duplicate named workspace: {}", s), + )); + return Ok(Self(String::new())); + } + ctx.set(WorkspaceNameSet(name_set)); + Ok(Self(s.clone().into())) + } + _ => { + ctx.emit_error(DecodeError::unsupported( + val, + "workspace names must be strings", + )); + Ok(Self(String::new())) + } + } + } +} + impl<S> knuffel::Decode<S> for WindowOpenAnim where S: knuffel::traits::ErrorSpan, @@ -2278,6 +2400,7 @@ mod tests { Mod+Ctrl+Shift+L { move-window-to-monitor-right; } Mod+Comma { consume-window-into-column; } Mod+1 { focus-workspace 1; } + Mod+Shift+1 { focus-workspace "workspace-1"; } Mod+Shift+E { quit skip-confirmation=true; } Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } } @@ -2285,6 +2408,12 @@ mod tests { debug { render-drm-device "/dev/dri/renderD129" } + + workspace "workspace-1" { + open-on-output "eDP-1" + } + workspace "workspace-2" + workspace "workspace-3" "##, Config { input: Input { @@ -2489,6 +2618,20 @@ mod tests { }, ..Default::default() }], + workspaces: vec![ + Workspace { + name: WorkspaceName("workspace-1".to_string()), + open_on_output: Some("eDP-1".to_string()), + }, + Workspace { + name: WorkspaceName("workspace-2".to_string()), + open_on_output: None, + }, + Workspace { + name: WorkspaceName("workspace-3".to_string()), + open_on_output: None, + }, + ], binds: Binds(vec![ Bind { key: Key { @@ -2540,7 +2683,18 @@ mod tests { trigger: Trigger::Keysym(Keysym::_1), modifiers: Modifiers::COMPOSITOR, }, - action: Action::FocusWorkspace(1), + action: Action::FocusWorkspace(WorkspaceReference::Index(1)), + cooldown: None, + allow_when_locked: false, + }, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::_1), + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + action: Action::FocusWorkspace(WorkspaceReference::Name( + "workspace-1".to_string(), + )), cooldown: None, allow_when_locked: false, }, diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index beabfcbc..b0f124f9 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -146,11 +146,11 @@ pub enum Action { FocusWorkspaceDown, /// Focus the workspace above. FocusWorkspaceUp, - /// Focus a workspace by index. + /// Focus a workspace by reference (index or name). FocusWorkspace { - /// Index of the workspace to focus. + /// Reference (index or name) of the workspace to focus. #[cfg_attr(feature = "clap", arg())] - index: u8, + reference: WorkspaceReferenceArg, }, /// Focus the previous workspace. FocusWorkspacePrevious, @@ -158,21 +158,21 @@ pub enum Action { MoveWindowToWorkspaceDown, /// Move the focused window to the workspace above. MoveWindowToWorkspaceUp, - /// Move the focused window to a workspace by index. + /// Move the focused window to a workspace by reference (index or name). MoveWindowToWorkspace { - /// Index of the target workspace. + /// Reference (index or name) of the workspace to move the window to. #[cfg_attr(feature = "clap", arg())] - index: u8, + reference: WorkspaceReferenceArg, }, /// Move the focused column to the workspace below. MoveColumnToWorkspaceDown, /// Move the focused column to the workspace above. MoveColumnToWorkspaceUp, - /// Move the focused column to a workspace by index. + /// Move the focused column to a workspace by reference (index or name). MoveColumnToWorkspace { - /// Index of the target workspace. + /// Reference (index or name) of the workspace to move the column to. #[cfg_attr(feature = "clap", arg())] - index: u8, + reference: WorkspaceReferenceArg, }, /// Move the focused workspace down. MoveWorkspaceDown, @@ -257,6 +257,15 @@ pub enum SizeChange { AdjustProportion(f64), } +/// Workspace reference (index or name) to operate on. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub enum WorkspaceReferenceArg { + /// Index of the workspace. + Index(u8), + /// Name of the workspace. + Name(String), +} + /// Layout to switch to. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] pub enum LayoutSwitchTarget { @@ -475,6 +484,24 @@ pub enum OutputConfigChanged { OutputWasMissing, } +impl FromStr for WorkspaceReferenceArg { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let reference = if let Ok(index) = s.parse::<i32>() { + if let Ok(idx) = u8::try_from(index) { + Self::Index(idx) + } else { + return Err("workspace indexes must be between 0 and 255"); + } + } else { + Self::Name(s.to_string()) + }; + + Ok(reference) + } +} + impl FromStr for SizeChange { type Err = &'static str; 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, out |
