From 624c799ebf4470ca6dfc80592029132071730c57 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sun, 24 Dec 2023 14:20:50 +0400 Subject: Move layout.rs into its own module --- src/layout/mod.rs | 4025 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4025 insertions(+) create mode 100644 src/layout/mod.rs (limited to 'src/layout') diff --git a/src/layout/mod.rs b/src/layout/mod.rs new file mode 100644 index 00000000..bf012311 --- /dev/null +++ b/src/layout/mod.rs @@ -0,0 +1,4025 @@ +//! Window layout logic. +//! +//! Niri implements scrollable tiling with workspaces. There's one primary output, and potentially +//! multiple other outputs. +//! +//! Our layout has the following invariants: +//! +//! 1. Disconnecting and reconnecting the same output must not change the layout. +//! * This includes both secondary outputs and the primary output. +//! 2. Connecting an output must not change the layout for any workspaces that were never on that +//! output. +//! +//! Therefore, we implement the following logic: every workspace keeps track of which output it +//! originated on. When an output disconnects, its workspace (or workspaces, in case of the primary +//! output disconnecting) are appended to the (potentially new) primary output, but remember their +//! original output. Then, if the original output connects again, all workspaces originally from +//! there move back to that output. +//! +//! In order to avoid surprising behavior, if the user creates or moves any new windows onto a +//! workspace, it forgets its original output, and its current output becomes its original output. +//! Imagine a scenario: the user works with a laptop and a monitor at home, then takes their laptop +//! with them, disconnecting the monitor, and keeps working as normal, using the second monitor's +//! workspace just like any other. Then they come back, reconnect the second monitor, and now we +//! don't want an unassuming workspace to end up on it. +//! +//! ## Workspaces-only-on-primary considerations +//! +//! If this logic results in more than one workspace present on a secondary output, then as a +//! compromise we only keep the first workspace there, and move the rest to the primary output, +//! making the primary output their original output. + +use std::cmp::{max, min}; +use std::iter::zip; +use std::mem; +use std::rc::Rc; +use std::time::Duration; + +use arrayvec::ArrayVec; +use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; +use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; +use smithay::backend::renderer::element::utils::{ + CropRenderElement, Relocate, RelocateRenderElement, +}; +use smithay::backend::renderer::element::{AsRenderElements, Kind}; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::backend::renderer::ImportAll; +use smithay::desktop::space::SpaceElement; +use smithay::desktop::{layer_map_for_output, Window}; +use smithay::output::Output; +use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::render_elements; +use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform}; +use smithay::wayland::compositor::{send_surface_state, with_states}; +use smithay::wayland::shell::xdg::SurfaceCachedState; + +use crate::animation::Animation; +use crate::config::{self, Color, Config, PresetWidth, SizeChange, Struts}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutputId(String); + +render_elements! { + #[derive(Debug)] + pub WorkspaceRenderElement where R: ImportAll; + Wayland = WaylandSurfaceRenderElement, + FocusRing = SolidColorRenderElement, +} +pub type MonitorRenderElement = + RelocateRenderElement>>; + +pub trait LayoutElement: SpaceElement + PartialEq + Clone { + fn request_size(&self, size: Size); + fn request_fullscreen(&self, size: Size); + fn min_size(&self) -> Size; + fn max_size(&self) -> Size; + fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool; + fn has_ssd(&self) -> bool; + fn set_preferred_scale_transform(&self, scale: i32, transform: Transform); +} + +#[derive(Debug)] +pub struct Layout { + /// Monitors and workspaes in the layout. + monitor_set: MonitorSet, + /// Configurable properties of the layout. + options: Rc, +} + +#[derive(Debug)] +enum MonitorSet { + /// At least one output is connected. + Normal { + /// Connected monitors. + monitors: Vec>, + /// Index of the primary monitor. + primary_idx: usize, + /// Index of the active monitor. + active_monitor_idx: usize, + }, + /// No outputs are connected, and these are the workspaces. + NoOutputs { + /// The workspaces. + workspaces: Vec>, + }, +} + +#[derive(Debug)] +pub struct Monitor { + /// Output for this monitor. + output: Output, + // Must always contain at least one. + workspaces: Vec>, + /// Index of the currently active workspace. + active_workspace_idx: usize, + /// In-progress switch between workspaces. + workspace_switch: Option, + /// Configurable properties of the layout. + options: Rc, +} + +#[derive(Debug)] +enum WorkspaceSwitch { + Animation(Animation), + Gesture(WorkspaceSwitchGesture), +} + +#[derive(Debug)] +struct WorkspaceSwitchGesture { + /// Index of the workspace where the gesture was started. + center_idx: usize, + /// Current, fractional workspace index. + current_idx: f64, +} + +#[derive(Debug)] +pub struct Workspace { + /// The original output of this workspace. + /// + /// Most of the time this will be the workspace's current output, however, after an output + /// disconnection, it may remain pointing to the disconnected output. + original_output: OutputId, + + /// Current output of this workspace. + output: Option, + + /// Latest known view size for this workspace. + /// + /// This should be computed from the current workspace output size, or, if all outputs have + /// been disconnected, preserved until a new output is connected. + view_size: Size, + + /// Latest known working area for this workspace. + /// + /// This is similar to view size, but takes into account things like layer shell exclusive + /// zones. + working_area: Rectangle, + + /// Columns of windows on this workspace. + columns: Vec>, + + /// Index of the currently active column, if any. + active_column_idx: usize, + + /// Focus ring buffer and parameters. + focus_ring: FocusRing, + + /// Offset of the view computed from the active column. + /// + /// Any gaps, including left padding from work area left exclusive zone, is handled + /// with this view offset (rather than added as a constant elsewhere in the code). This allows + /// for natural handling of fullscreen windows, which must ignore work area padding. + view_offset: i32, + + /// Animation of the view offset, if one is currently ongoing. + view_offset_anim: Option, + + /// Whether to activate the previous, rather than the next, column upon column removal. + /// + /// When a new column is created and removed with no focus changes in-between, it is more + /// natural to activate the previously-focused column. This variable tracks that. + /// + /// Since we only create-and-activate columns immediately to the right of the active column (in + /// contrast to tabs in Firefox, for example), we can track this as a bool, rather than an + /// index of the previous column to activate. + activate_prev_column_on_removal: bool, + + /// Configurable properties of the layout. + options: Rc, +} + +#[derive(Debug)] +struct FocusRing { + buffers: [SolidColorBuffer; 4], + locations: [Point; 4], + is_off: bool, + is_border: bool, + width: i32, + active_color: Color, + inactive_color: Color, +} + +#[derive(Debug, PartialEq)] +struct Options { + /// Padding around windows in logical pixels. + gaps: i32, + /// Extra padding around the working area in logical pixels. + struts: Struts, + focus_ring: config::FocusRing, + /// Column widths that `toggle_width()` switches between. + preset_widths: Vec, + /// Initial width for new windows. + default_width: Option, +} + +impl Default for Options { + fn default() -> Self { + Self { + gaps: 16, + struts: Default::default(), + focus_ring: Default::default(), + preset_widths: vec![ + ColumnWidth::Proportion(1. / 3.), + ColumnWidth::Proportion(0.5), + ColumnWidth::Proportion(2. / 3.), + ], + default_width: None, + } + } +} + +impl Options { + fn from_config(config: &Config) -> Self { + let preset_column_widths = &config.preset_column_widths; + + let preset_widths = if preset_column_widths.is_empty() { + Options::default().preset_widths + } else { + preset_column_widths + .iter() + .copied() + .map(ColumnWidth::from) + .collect() + }; + + // Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)), + // while present, but empty, maps to None. + let default_width = config + .default_column_width + .as_ref() + .map(|w| w.0.first().copied().map(ColumnWidth::from)) + .unwrap_or(Some(ColumnWidth::Proportion(0.5))); + + Self { + gaps: config.gaps.into(), + struts: config.struts, + focus_ring: config.focus_ring, + preset_widths, + default_width, + } + } +} + +/// Width of a column. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ColumnWidth { + /// Proportion of the current view width. + Proportion(f64), + /// One of the proportion presets. + /// + /// This is separate from Proportion in order to be able to reliably cycle between preset + /// proportions. + Preset(usize), + /// Fixed width in logical pixels. + Fixed(i32), +} + +impl From for ColumnWidth { + fn from(value: PresetWidth) -> Self { + match value { + PresetWidth::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)), + PresetWidth::Fixed(f) => Self::Fixed(f.clamp(1, 100000)), + } + } +} + +/// Height of a window in a column. +/// +/// Proportional height is intentionally omitted. With column widths you frequently want e.g. two +/// columns side-by-side with 50% width each, and you want them to remain this way when moving to a +/// differently sized monitor. Windows in a column, however, already auto-size to fill the available +/// height, giving you this behavior. The only reason to set a different window height, then, is +/// when you want something in the window to fit exactly, e.g. to fit 30 lines in a terminal, which +/// corresponds to the `Fixed` variant. +/// +/// This does not preclude the usual set of binds to set or resize a window proportionally. Just, +/// they are converted to, and stored as fixed height right away, so that once you resize a window +/// to fit the desired content, it can never become smaller than that when moving between monitors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowHeight { + /// Automatically computed height, evenly distributed across the column. + Auto, + /// Fixed height in logical pixels. + Fixed(i32), +} + +#[derive(Debug)] +struct Column { + /// Windows in this column. + /// + /// Must be non-empty. + windows: Vec, + + /// Heights of the windows. + /// + /// Must have the same number of elements as `windows`. + heights: Vec, + + /// Index of the currently active window. + active_window_idx: usize, + + /// Desired width of this column. + /// + /// If the column is full-width or full-screened, this is the width that should be restored + /// upon unfullscreening and untoggling full-width. + width: ColumnWidth, + + /// Whether this column is full-width. + is_full_width: bool, + + /// Whether this column contains a single full-screened window. + is_fullscreen: bool, + + /// Latest known view size for this column's workspace. + view_size: Size, + + /// Latest known working area for this column's workspace. + working_area: Rectangle, + + /// Configurable properties of the layout. + options: Rc, +} + +impl OutputId { + pub fn new(output: &Output) -> Self { + Self(output.name()) + } +} + +impl LayoutElement for Window { + fn request_size(&self, size: Size) { + self.toplevel().with_pending_state(|state| { + state.size = Some(size); + state.states.unset(xdg_toplevel::State::Fullscreen); + }); + } + + fn request_fullscreen(&self, size: Size) { + self.toplevel().with_pending_state(|state| { + state.size = Some(size); + state.states.set(xdg_toplevel::State::Fullscreen); + }); + } + + fn min_size(&self) -> Size { + with_states(self.toplevel().wl_surface(), |state| { + let curr = state.cached_state.current::(); + curr.min_size + }) + } + + fn max_size(&self) -> Size { + with_states(self.toplevel().wl_surface(), |state| { + let curr = state.cached_state.current::(); + curr.max_size + }) + } + + fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool { + self.toplevel().wl_surface() == wl_surface + } + + fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) { + self.with_surfaces(|surface, data| { + send_surface_state(surface, data, scale, transform); + }); + } + + fn has_ssd(&self) -> bool { + self.toplevel().current_state().decoration_mode + == Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) + } +} + +impl FocusRing { + fn update( + &mut self, + win_pos: Point, + win_size: Size, + is_border: bool, + ) { + if is_border { + self.buffers[0].resize((win_size.w + self.width * 2, self.width)); + self.buffers[1].resize((win_size.w + self.width * 2, self.width)); + self.buffers[2].resize((self.width, win_size.h)); + self.buffers[3].resize((self.width, win_size.h)); + + self.locations[0] = win_pos + Point::from((-self.width, -self.width)); + self.locations[1] = win_pos + Point::from((-self.width, win_size.h)); + self.locations[2] = win_pos + Point::from((-self.width, 0)); + self.locations[3] = win_pos + Point::from((win_size.w, 0)); + } else { + let size = win_size + Size::from((self.width * 2, self.width * 2)); + self.buffers[0].resize(size); + self.locations[0] = win_pos - Point::from((self.width, self.width)); + } + + self.is_border = is_border; + } + + fn set_active(&mut self, is_active: bool) { + let color = if is_active { + self.active_color.into() + } else { + self.inactive_color.into() + }; + + for buf in &mut self.buffers { + buf.set_color(color); + } + } + + fn render(&self, scale: Scale) -> impl Iterator { + let mut rv = ArrayVec::<_, 4>::new(); + + if self.is_off { + return rv.into_iter(); + } + + let mut push = |buffer, location: Point| { + let elem = SolidColorRenderElement::from_buffer( + buffer, + location.to_physical_precise_round(scale), + scale, + 1., + Kind::Unspecified, + ); + rv.push(elem); + }; + + if self.is_border { + for (buf, loc) in zip(&self.buffers, self.locations) { + push(buf, loc); + } + } else { + push(&self.buffers[0], self.locations[0]); + } + + rv.into_iter() + } +} + +impl FocusRing { + fn new(config: config::FocusRing) -> Self { + Self { + buffers: Default::default(), + locations: Default::default(), + is_off: config.off, + is_border: false, + width: config.width.into(), + active_color: config.active_color, + inactive_color: config.inactive_color, + } + } +} + +impl WorkspaceSwitch { + fn current_idx(&self) -> f64 { + match self { + WorkspaceSwitch::Animation(anim) => anim.value(), + WorkspaceSwitch::Gesture(gesture) => gesture.current_idx, + } + } + + /// Returns `true` if the workspace switch is [`Animation`]. + /// + /// [`Animation`]: WorkspaceSwitch::Animation + #[must_use] + fn is_animation(&self) -> bool { + matches!(self, Self::Animation(..)) + } +} + +impl ColumnWidth { + fn resolve(self, options: &Options, view_width: i32) -> i32 { + match self { + ColumnWidth::Proportion(proportion) => { + ((view_width - options.gaps) as f64 * proportion).floor() as i32 - options.gaps + } + ColumnWidth::Preset(idx) => options.preset_widths[idx].resolve(options, view_width), + ColumnWidth::Fixed(width) => width, + } + } +} + +impl Layout { + pub fn new(config: &Config) -> Self { + Self { + monitor_set: MonitorSet::NoOutputs { workspaces: vec![] }, + options: Rc::new(Options::from_config(config)), + } + } + + pub fn add_output(&mut self, output: Output) { + let id = OutputId::new(&output); + + self.monitor_set = match mem::take(&mut self.monitor_set) { + MonitorSet::Normal { + mut monitors, + primary_idx, + active_monitor_idx, + } => { + let primary = &mut monitors[primary_idx]; + + let mut workspaces = vec![]; + for i in (0..primary.workspaces.len()).rev() { + if primary.workspaces[i].original_output == id { + let ws = primary.workspaces.remove(i); + + // 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() { + workspaces.push(ws); + } + + if i <= primary.active_workspace_idx { + primary.active_workspace_idx = + primary.active_workspace_idx.saturating_sub(1); + } + } + } + workspaces.reverse(); + + // Make sure there's always an empty workspace. + workspaces.push(Workspace::new(output.clone(), self.options.clone())); + + for ws in &mut workspaces { + ws.set_output(Some(output.clone())); + } + + monitors.push(Monitor::new(output, workspaces, self.options.clone())); + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } + } + MonitorSet::NoOutputs { mut workspaces } => { + // We know there are no empty workspaces there, so add one. + workspaces.push(Workspace::new(output.clone(), self.options.clone())); + + for workspace in &mut workspaces { + workspace.set_output(Some(output.clone())); + } + + let monitor = Monitor::new(output, workspaces, self.options.clone()); + + MonitorSet::Normal { + monitors: vec![monitor], + primary_idx: 0, + active_monitor_idx: 0, + } + } + } + } + + pub fn remove_output(&mut self, output: &Output) { + self.monitor_set = match mem::take(&mut self.monitor_set) { + MonitorSet::Normal { + mut monitors, + mut primary_idx, + mut active_monitor_idx, + } => { + let idx = monitors + .iter() + .position(|mon| &mon.output == output) + .expect("trying to remove non-existing output"); + let monitor = monitors.remove(idx); + let mut workspaces = monitor.workspaces; + + for ws in &mut workspaces { + ws.set_output(None); + } + + // Get rid of empty workspaces. + workspaces.retain(|ws| ws.has_windows()); + + if monitors.is_empty() { + // Removed the last monitor. + MonitorSet::NoOutputs { workspaces } + } else { + if primary_idx >= idx { + // Update primary_idx to either still point at the same monitor, or at some + // other monitor if the primary has been removed. + primary_idx = primary_idx.saturating_sub(1); + } + if active_monitor_idx >= idx { + // Update active_monitor_idx to either still point at the same monitor, or + // at some other monitor if the active monitor has + // been removed. + active_monitor_idx = active_monitor_idx.saturating_sub(1); + } + + let primary = &mut monitors[primary_idx]; + for ws in &mut workspaces { + ws.set_output(Some(primary.output.clone())); + } + + let empty_was_focused = + primary.active_workspace_idx == primary.workspaces.len() - 1; + + // Push the workspaces from the removed monitor in the end, right before the + // last, empty, workspace. + let empty = primary.workspaces.remove(primary.workspaces.len() - 1); + primary.workspaces.extend(workspaces); + primary.workspaces.push(empty); + + // If the empty workspace was focused on the primary monitor, keep it focused. + if empty_was_focused { + primary.active_workspace_idx = primary.workspaces.len() - 1; + } + + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } + } + } + MonitorSet::NoOutputs { .. } => { + panic!("tried to remove output when there were already none") + } + } + } + + pub fn add_window_by_idx( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + window: W, + activate: bool, + width: ColumnWidth, + is_full_width: bool, + ) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + else { + panic!() + }; + + monitors[monitor_idx].add_window(workspace_idx, window, activate, width, is_full_width); + + if activate { + *active_monitor_idx = monitor_idx; + } + } + + /// Adds a new window to the layout. + /// + /// Returns an output that the window was added to, if there were any outputs. + pub fn add_window( + &mut self, + window: W, + width: Option, + is_full_width: bool, + ) -> Option<&Output> { + let width = width + .or(self.options.default_width) + .unwrap_or_else(|| ColumnWidth::Fixed(window.geometry().size.w)); + + match &mut self.monitor_set { + MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } => { + let mon = &mut monitors[*active_monitor_idx]; + + // Don't steal focus from an active fullscreen window. + let mut activate = true; + let ws = &mon.workspaces[mon.active_workspace_idx]; + if !ws.columns.is_empty() && ws.columns[ws.active_column_idx].is_fullscreen { + activate = false; + } + + mon.add_window( + mon.active_workspace_idx, + window, + activate, + width, + is_full_width, + ); + Some(&mon.output) + } + MonitorSet::NoOutputs { workspaces } => { + let ws = if let Some(ws) = workspaces.get_mut(0) { + ws + } else { + workspaces.push(Workspace::new_no_outputs(self.options.clone())); + &mut workspaces[0] + }; + ws.add_window(window, true, width, is_full_width); + None + } + } + } + + pub fn remove_window(&mut self, window: &W) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for (idx, ws) in mon.workspaces.iter_mut().enumerate() { + if ws.has_window(window) { + ws.remove_window(window); + + // Clean up empty workspaces that are not active and not last. + if !ws.has_windows() + && idx != mon.active_workspace_idx + && idx != mon.workspaces.len() - 1 + { + mon.workspaces.remove(idx); + + if idx < mon.active_workspace_idx { + mon.active_workspace_idx -= 1; + } + } + + break; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for (idx, ws) in workspaces.iter_mut().enumerate() { + if ws.has_window(window) { + ws.remove_window(window); + + // Clean up empty workspaces. + if !ws.has_windows() { + workspaces.remove(idx); + } + + break; + } + } + } + } + } + + pub fn update_window(&mut self, window: &W) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + ws.update_window(window); + return; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + ws.update_window(window); + return; + } + } + } + } + } + + pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(W, Output)> { + if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { + for mon in monitors { + for ws in &mon.workspaces { + if let Some(window) = ws.find_wl_surface(wl_surface) { + return Some((window.clone(), mon.output.clone())); + } + } + } + } + + None + } + + pub fn window_y(&self, window: &W) -> Option { + match &self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mon.workspaces { + for col in &ws.columns { + if let Some(idx) = col.windows.iter().position(|w| w == window) { + return Some(col.window_y(idx)); + } + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + for col in &ws.columns { + if let Some(idx) = col.windows.iter().position(|w| w == window) { + return Some(col.window_y(idx)); + } + } + } + } + } + + None + } + + pub fn update_output_size(&mut self, output: &Output) { + let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else { + panic!() + }; + + for mon in monitors { + if &mon.output == output { + let view_size = output_size(output); + let working_area = compute_working_area(output, self.options.struts); + + for ws in &mut mon.workspaces { + ws.set_view_size(view_size, working_area); + } + + break; + } + } + } + + pub fn activate_window(&mut self, window: &W) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + else { + todo!() + }; + + for (monitor_idx, mon) in monitors.iter_mut().enumerate() { + for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() { + if ws.has_window(window) { + *active_monitor_idx = monitor_idx; + ws.activate_window(window); + + // Switch to that workspace if not already during a transition. + if mon.workspace_switch.is_none() { + mon.switch_workspace(workspace_idx); + } + + break; + } + } + } + } + + pub fn activate_output(&mut self, output: &Output) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + else { + return; + }; + + let idx = monitors + .iter() + .position(|mon| &mon.output == output) + .unwrap(); + *active_monitor_idx = idx; + } + + pub fn active_output(&self) -> Option<&Output> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &self.monitor_set + else { + return None; + }; + + Some(&monitors[*active_monitor_idx].output) + } + + pub fn active_workspace(&self) -> Option<&Workspace> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &self.monitor_set + else { + return None; + }; + + let mon = &monitors[*active_monitor_idx]; + Some(&mon.workspaces[mon.active_workspace_idx]) + } + + pub fn active_window(&self) -> Option<(W, Output)> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &self.monitor_set + else { + return None; + }; + + let mon = &monitors[*active_monitor_idx]; + let ws = &mon.workspaces[mon.active_workspace_idx]; + + if ws.columns.is_empty() { + return None; + } + + let col = &ws.columns[ws.active_column_idx]; + Some(( + col.windows[col.active_window_idx].clone(), + mon.output.clone(), + )) + } + + pub fn windows_for_output(&self, output: &Output) -> impl Iterator + '_ { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { + panic!() + }; + + let mon = monitors.iter().find(|mon| &mon.output == output).unwrap(); + mon.workspaces.iter().flat_map(|ws| ws.windows()) + } + + fn active_monitor(&mut self) -> Option<&mut Monitor> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + else { + return None; + }; + + Some(&mut monitors[*active_monitor_idx]) + } + + pub fn monitor_for_output(&self, output: &Output) -> Option<&Monitor> { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { + return None; + }; + + monitors.iter().find(|monitor| &monitor.output == output) + } + + pub fn outputs(&self) -> impl Iterator + '_ { + let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { + &monitors[..] + } else { + &[][..] + }; + + monitors.iter().map(|mon| &mon.output) + } + + pub fn move_left(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_left(); + } + + pub fn move_right(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_right(); + } + + pub fn move_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_down(); + } + + pub fn move_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_up(); + } + + pub fn move_down_or_to_workspace_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_down_or_to_workspace_down(); + } + + pub fn move_up_or_to_workspace_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_up_or_to_workspace_up(); + } + + pub fn focus_left(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.focus_left(); + } + + pub fn focus_right(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.focus_right(); + } + + pub fn focus_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.focus_down(); + } + + pub fn focus_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.focus_up(); + } + + pub fn focus_window_or_workspace_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.focus_window_or_workspace_down(); + } + + pub fn focus_window_or_workspace_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.focus_window_or_workspace_up(); + } + + pub fn move_to_workspace_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_to_workspace_up(); + } + + pub fn move_to_workspace_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_to_workspace_down(); + } + + pub fn move_to_workspace(&mut self, idx: usize) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_to_workspace(idx); + } + + pub fn switch_workspace_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.switch_workspace_up(); + } + + pub fn switch_workspace_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.switch_workspace_down(); + } + + pub fn switch_workspace(&mut self, idx: usize) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.switch_workspace(idx); + } + + pub fn consume_into_column(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.consume_into_column(); + } + + pub fn expel_from_column(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.expel_from_column(); + } + + pub fn center_column(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.center_column(); + } + + pub fn focus(&self) -> Option<&W> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &self.monitor_set + else { + return None; + }; + + monitors[*active_monitor_idx].focus() + } + + pub fn window_under( + &self, + output: &Output, + pos_within_output: Point, + ) -> Option<(&W, Point)> { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { + return None; + }; + + let mon = monitors.iter().find(|mon| &mon.output == output)?; + mon.window_under(pos_within_output) + } + + #[cfg(test)] + fn verify_invariants(&self) { + let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set { + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } => (monitors, primary_idx, active_monitor_idx), + MonitorSet::NoOutputs { workspaces } => { + for workspace in workspaces { + assert!( + workspace.has_windows(), + "with no outputs there cannot be empty workspaces" + ); + + assert_eq!( + workspace.options, self.options, + "workspace options must be synchronized with layout" + ); + + workspace.verify_invariants(); + } + + return; + } + }; + + assert!(primary_idx < monitors.len()); + assert!(active_monitor_idx < monitors.len()); + + for (idx, monitor) in monitors.iter().enumerate() { + assert!( + !monitor.workspaces.is_empty(), + "monitor must have at least one workspace" + ); + assert!(monitor.active_workspace_idx < monitor.workspaces.len()); + + assert_eq!( + monitor.options, self.options, + "monitor options must be synchronized with layout" + ); + + let monitor_id = OutputId::new(&monitor.output); + + if idx == primary_idx { + for ws in &monitor.workspaces { + if ws.original_output == monitor_id { + // This is the primary monitor's own workspace. + continue; + } + + let own_monitor_exists = monitors + .iter() + .any(|m| OutputId::new(&m.output) == ws.original_output); + assert!( + !own_monitor_exists, + "primary monitor cannot have workspaces for which their own monitor exists" + ); + } + } else { + assert!( + monitor + .workspaces + .iter() + .any(|workspace| workspace.original_output == monitor_id), + "secondary monitor must not have any non-own workspaces" + ); + } + + assert!( + monitor.workspaces.last().unwrap().columns.is_empty(), + "monitor must have an empty 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" + ); + } + } + } + + // FIXME: verify that primary doesn't have any workspaces for which their own monitor + // exists. + + for workspace in &monitor.workspaces { + assert_eq!( + workspace.options, self.options, + "workspace options must be synchronized with layout" + ); + + workspace.verify_invariants(); + } + } + } + + pub fn advance_animations(&mut self, current_time: Duration) { + let _span = tracy_client::span!("Layout::advance_animations"); + + match &mut self.monitor_set { + MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } => { + for (idx, mon) in monitors.iter_mut().enumerate() { + mon.advance_animations(current_time, idx == *active_monitor_idx); + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + ws.advance_animations(current_time, false); + } + } + } + } + + pub fn update_config(&mut self, config: &Config) { + let options = Rc::new(Options::from_config(config)); + + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + mon.update_config(options.clone()); + } + } + MonitorSet::NoOutputs { workspaces } => { + for ws in workspaces { + ws.update_config(options.clone()); + } + } + } + + self.options = options; + } + + pub fn toggle_width(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.toggle_width(); + } + + pub fn toggle_full_width(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.toggle_full_width(); + } + + pub fn set_column_width(&mut self, change: SizeChange) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.set_column_width(change); + } + + pub fn set_window_height(&mut self, change: SizeChange) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.set_window_height(change); + } + + pub fn focus_output(&mut self, output: &Output) { + if let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + { + for (idx, mon) in monitors.iter().enumerate() { + if &mon.output == output { + *active_monitor_idx = idx; + return; + } + } + } + } + + pub fn move_to_output(&mut self, output: &Output) { + if let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + { + let new_idx = monitors + .iter() + .position(|mon| &mon.output == output) + .unwrap(); + + let current = &mut monitors[*active_monitor_idx]; + let ws = current.active_workspace(); + if !ws.has_windows() { + return; + } + let column = &ws.columns[ws.active_column_idx]; + let window = column.windows[column.active_window_idx].clone(); + let width = column.width; + let is_full_width = column.is_full_width; + ws.remove_window(&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); + } + } + + pub fn move_window_to_output(&mut self, window: W, output: &Output) { + if !matches!(&self.monitor_set, MonitorSet::Normal { .. }) { + return; + } + + self.remove_window(&window); + + if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set { + let mut width = None; + let mut is_full_width = false; + for mon in &*monitors { + for ws in &mon.workspaces { + for col in &ws.columns { + if col.windows.contains(&window) { + width = Some(col.width); + is_full_width = col.is_full_width; + break; + } + } + } + } + let Some(width) = width else { return }; + + let new_idx = monitors + .iter() + .position(|mon| &mon.output == output) + .unwrap(); + + let workspace_idx = monitors[new_idx].active_workspace_idx; + // FIXME: activate only if it was already active and focused. + self.add_window_by_idx(new_idx, workspace_idx, window, true, width, is_full_width); + } + } + + pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + ws.set_fullscreen(window, is_fullscreen); + return; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + ws.set_fullscreen(window, is_fullscreen); + return; + } + } + } + } + } + + pub fn toggle_fullscreen(&mut self, window: &W) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + ws.toggle_fullscreen(window); + return; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + ws.toggle_fullscreen(window); + return; + } + } + } + } + } + + pub fn workspace_switch_gesture_begin(&mut self, output: &Output) { + let monitors = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors, + MonitorSet::NoOutputs { .. } => unreachable!(), + }; + + for monitor in monitors { + // Cancel the gesture on other outputs. + if &monitor.output != output { + if let Some(WorkspaceSwitch::Gesture(_)) = monitor.workspace_switch { + monitor.workspace_switch = None; + } + continue; + } + + let center_idx = monitor.active_workspace_idx; + let current_idx = monitor + .workspace_switch + .as_ref() + .map(|s| s.current_idx()) + .unwrap_or(center_idx as f64); + + let gesture = WorkspaceSwitchGesture { + center_idx, + current_idx, + }; + monitor.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture)); + } + } + + pub fn workspace_switch_gesture_update(&mut self, delta_y: f64) -> Option> { + let monitors = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors, + MonitorSet::NoOutputs { .. } => return None, + }; + + for monitor in monitors { + if let Some(WorkspaceSwitch::Gesture(gesture)) = &mut monitor.workspace_switch { + // Normalize like GNOME Shell's workspace switching. + let delta_y = -delta_y / 400.; + + let min = gesture.center_idx.saturating_sub(1) as f64; + let max = (gesture.center_idx + 1).min(monitor.workspaces.len() - 1) as f64; + let new_idx = (gesture.current_idx + delta_y).clamp(min, max); + + if gesture.current_idx == new_idx { + return Some(None); + } + + gesture.current_idx = new_idx; + return Some(Some(monitor.output.clone())); + } + } + + None + } + + pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> Option { + let monitors = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors, + MonitorSet::NoOutputs { .. } => return None, + }; + + for monitor in monitors { + if let Some(WorkspaceSwitch::Gesture(gesture)) = &mut monitor.workspace_switch { + if cancelled { + monitor.workspace_switch = None; + return Some(monitor.output.clone()); + } + + // FIXME: keep track of gesture velocity and use it to compute the final point and + // to animate to it. + let current_idx = gesture.current_idx; + let idx = current_idx.round() as usize; + + monitor.active_workspace_idx = idx; + monitor.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( + current_idx, + idx as f64, + Duration::from_millis(250), + ))); + + return Some(monitor.output.clone()); + } + } + + None + } + + pub fn move_workspace_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_workspace_down(); + } + + pub fn move_workspace_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.move_workspace_up(); + } +} + +impl Layout { + pub fn refresh(&self) { + let _span = tracy_client::span!("MonitorSet::refresh"); + + match &self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mon.workspaces { + ws.refresh(); + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + ws.refresh(); + } + } + } + } +} + +impl Default for MonitorSet { + fn default() -> Self { + Self::NoOutputs { workspaces: vec![] } + } +} + +impl Monitor { + fn new(output: Output, workspaces: Vec>, options: Rc) -> Self { + Self { + output, + workspaces, + active_workspace_idx: 0, + workspace_switch: None, + options, + } + } + + fn active_workspace(&mut self) -> &mut Workspace { + &mut self.workspaces[self.active_workspace_idx] + } + + fn activate_workspace(&mut self, idx: usize) { + if self.active_workspace_idx == idx { + return; + } + + let current_idx = self + .workspace_switch + .as_ref() + .map(|s| s.current_idx()) + .unwrap_or(self.active_workspace_idx as f64); + + self.active_workspace_idx = idx; + + self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( + current_idx, + idx as f64, + Duration::from_millis(250), + ))); + } + + pub fn add_window( + &mut self, + workspace_idx: usize, + window: W, + activate: bool, + width: ColumnWidth, + is_full_width: bool, + ) { + let workspace = &mut self.workspaces[workspace_idx]; + + workspace.add_window(window.clone(), activate, width, is_full_width); + + // After adding a new window, workspace becomes this output's own. + workspace.original_output = OutputId::new(&self.output); + + if workspace_idx == self.workspaces.len() - 1 { + // Insert a new empty workspace. + let ws = Workspace::new(self.output.clone(), self.options.clone()); + self.workspaces.push(ws); + } + + if activate { + self.activate_workspace(workspace_idx); + } + } + + fn clean_up_workspaces(&mut self) { + assert!(self.workspace_switch.is_none()); + + for idx in (0..self.workspaces.len() - 1).rev() { + if self.active_workspace_idx == idx { + continue; + } + + if !self.workspaces[idx].has_windows() { + self.workspaces.remove(idx); + if self.active_workspace_idx > idx { + self.active_workspace_idx -= 1; + } + } + } + } + + pub fn move_left(&mut self) { + self.active_workspace().move_left(); + } + + pub fn move_right(&mut self) { + self.active_workspace().move_right(); + } + + pub fn move_down(&mut self) { + self.active_workspace().move_down(); + } + + pub fn move_up(&mut self) { + self.active_workspace().move_up(); + } + + pub fn move_down_or_to_workspace_down(&mut self) { + let workspace = self.active_workspace(); + if workspace.columns.is_empty() { + return; + } + let column = &mut workspace.columns[workspace.active_column_idx]; + let curr_idx = column.active_window_idx; + let new_idx = min(column.active_window_idx + 1, column.windows.len() - 1); + if curr_idx == new_idx { + self.move_to_workspace_down(); + } else { + workspace.move_down(); + } + } + + pub fn move_up_or_to_workspace_up(&mut self) { + let workspace = self.active_workspace(); + if workspace.columns.is_empty() { + return; + } + let curr_idx = workspace.columns[workspace.active_column_idx].active_window_idx; + let new_idx = curr_idx.saturating_sub(1); + if curr_idx == new_idx { + self.move_to_workspace_up(); + } else { + workspace.move_up(); + } + } + + pub fn focus_left(&mut self) { + self.active_workspace().focus_left(); + } + + pub fn focus_right(&mut self) { + self.active_workspace().focus_right(); + } + + pub fn focus_down(&mut self) { + self.active_workspace().focus_down(); + } + + pub fn focus_up(&mut self) { + self.active_workspace().focus_up(); + } + + pub fn focus_window_or_workspace_down(&mut self) { + let workspace = self.active_workspace(); + if workspace.columns.is_empty() { + self.switch_workspace_down(); + } else { + let column = &workspace.columns[workspace.active_column_idx]; + let curr_idx = column.active_window_idx; + let new_idx = min(column.active_window_idx + 1, column.windows.len() - 1); + if curr_idx == new_idx { + self.switch_workspace_down(); + } else { + workspace.focus_down(); + } + } + } + + pub fn focus_window_or_workspace_up(&mut self) { + let workspace = self.active_workspace(); + if workspace.columns.is_empty() { + self.switch_workspace_up(); + } else { + let curr_idx = workspace.columns[workspace.active_column_idx].active_window_idx; + let new_idx = curr_idx.saturating_sub(1); + if curr_idx == new_idx { + self.switch_workspace_up(); + } else { + workspace.focus_up(); + } + } + } + + pub fn move_to_workspace_up(&mut self) { + let source_workspace_idx = self.active_workspace_idx; + + let new_idx = source_workspace_idx.saturating_sub(1); + if new_idx == source_workspace_idx { + return; + } + + let workspace = &mut self.workspaces[source_workspace_idx]; + if workspace.columns.is_empty() { + return; + } + + let column = &mut workspace.columns[workspace.active_column_idx]; + let width = column.width; + let is_full_width = column.is_full_width; + let window = column.windows[column.active_window_idx].clone(); + workspace.remove_window(&window); + + self.add_window(new_idx, window, true, width, is_full_width); + } + + pub fn move_to_workspace_down(&mut self) { + let source_workspace_idx = self.active_workspace_idx; + + let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1); + if new_idx == source_workspace_idx { + return; + } + + let workspace = &mut self.workspaces[source_workspace_idx]; + if workspace.columns.is_empty() { + return; + } + + let column = &mut workspace.columns[workspace.active_column_idx]; + let width = column.width; + let is_full_width = column.is_full_width; + let window = column.windows[column.active_window_idx].clone(); + workspace.remove_window(&window); + + 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; + + let new_idx = min(idx, self.workspaces.len() - 1); + if new_idx == source_workspace_idx { + return; + } + + let workspace = &mut self.workspaces[source_workspace_idx]; + if workspace.columns.is_empty() { + return; + } + + let column = &mut workspace.columns[workspace.active_column_idx]; + let width = column.width; + let is_full_width = column.is_full_width; + let window = column.windows[column.active_window_idx].clone(); + workspace.remove_window(&window); + + self.add_window(new_idx, window, true, width, is_full_width); + + // Don't animate this action. + self.workspace_switch = None; + + self.clean_up_workspaces(); + } + + pub fn switch_workspace_up(&mut self) { + self.activate_workspace(self.active_workspace_idx.saturating_sub(1)); + } + + pub fn switch_workspace_down(&mut self) { + self.activate_workspace(min( + self.active_workspace_idx + 1, + self.workspaces.len() - 1, + )); + } + + pub fn switch_workspace(&mut self, idx: usize) { + self.activate_workspace(min(idx, self.workspaces.len() - 1)); + // Don't animate this action. + self.workspace_switch = None; + + self.clean_up_workspaces(); + } + + pub fn consume_into_column(&mut self) { + self.active_workspace().consume_into_column(); + } + + pub fn expel_from_column(&mut self) { + self.active_workspace().expel_from_column(); + } + + pub fn center_column(&mut self) { + self.active_workspace().center_column(); + } + + pub fn focus(&self) -> Option<&W> { + let workspace = &self.workspaces[self.active_workspace_idx]; + if !workspace.has_windows() { + return None; + } + + let column = &workspace.columns[workspace.active_column_idx]; + Some(&column.windows[column.active_window_idx]) + } + + pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) { + if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch { + anim.set_current_time(current_time); + if anim.is_done() { + self.workspace_switch = None; + self.clean_up_workspaces(); + } + } + + for ws in &mut self.workspaces { + ws.advance_animations(current_time, is_active); + } + } + + pub fn are_animations_ongoing(&self) -> bool { + self.workspace_switch + .as_ref() + .is_some_and(|s| s.is_animation()) + || self.workspaces.iter().any(|ws| ws.are_animations_ongoing()) + } + + pub fn are_transitions_ongoing(&self) -> bool { + self.workspace_switch.is_some() + || self.workspaces.iter().any(|ws| ws.are_animations_ongoing()) + } + + fn update_config(&mut self, options: Rc) { + for ws in &mut self.workspaces { + ws.update_config(options.clone()); + } + + if self.options.struts != options.struts { + let view_size = output_size(&self.output); + let working_area = compute_working_area(&self.output, options.struts); + + for ws in &mut self.workspaces { + ws.set_view_size(view_size, working_area); + } + } + + self.options = options; + } + + fn toggle_width(&mut self) { + self.active_workspace().toggle_width(); + } + + fn toggle_full_width(&mut self) { + self.active_workspace().toggle_full_width(); + } + + fn set_column_width(&mut self, change: SizeChange) { + self.active_workspace().set_column_width(change); + } + + fn set_window_height(&mut self, change: SizeChange) { + self.active_workspace().set_window_height(change); + } + + 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 { + return; + } + + self.workspaces.swap(self.active_workspace_idx, new_idx); + + if new_idx == self.workspaces.len() - 1 { + // Insert a new empty workspace. + let ws = Workspace::new(self.output.clone(), self.options.clone()); + self.workspaces.push(ws); + } + + self.activate_workspace(new_idx); + self.workspace_switch = None; + + self.clean_up_workspaces(); + } + + fn move_workspace_up(&mut self) { + let new_id