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.rs | 4025 ----------------------------------------------------- src/layout/mod.rs | 4025 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 4025 insertions(+), 4025 deletions(-) delete mode 100644 src/layout.rs create mode 100644 src/layout/mod.rs (limited to 'src') diff --git a/src/layout.rs b/src/layout.rs deleted file mode 100644 index bf012311..00000000 --- a/src/layout.rs +++ /dev/null @@ -1,4025 +0,0 @@ -//! 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 =