aboutsummaryrefslogtreecommitdiff
path: root/src/layout/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout/mod.rs')
-rw-r--r--src/layout/mod.rs4025
1 files changed, 4025 insertions, 0 deletions
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<R> where R: ImportAll;
+ Wayland = WaylandSurfaceRenderElement<R>,
+ FocusRing = SolidColorRenderElement,
+}
+pub type MonitorRenderElement<R> =
+ RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
+
+pub trait LayoutElement: SpaceElement + PartialEq + Clone {
+ fn request_size(&self, size: Size<i32, Logical>);
+ fn request_fullscreen(&self, size: Size<i32, Logical>);
+ fn min_size(&self) -> Size<i32, Logical>;
+ fn max_size(&self) -> Size<i32, Logical>;
+ 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<W: LayoutElement> {
+ /// Monitors and workspaes in the layout.
+ monitor_set: MonitorSet<W>,
+ /// Configurable properties of the layout.
+ options: Rc<Options>,
+}
+
+#[derive(Debug)]
+enum MonitorSet<W: LayoutElement> {
+ /// At least one output is connected.
+ Normal {
+ /// Connected monitors.
+ monitors: Vec<Monitor<W>>,
+ /// 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<Workspace<W>>,
+ },
+}
+
+#[derive(Debug)]
+pub struct Monitor<W: LayoutElement> {
+ /// Output for this monitor.
+ output: Output,
+ // Must always contain at least one.
+ workspaces: Vec<Workspace<W>>,
+ /// Index of the currently active workspace.
+ active_workspace_idx: usize,
+ /// In-progress switch between workspaces.
+ workspace_switch: Option<WorkspaceSwitch>,
+ /// Configurable properties of the layout.
+ options: Rc<Options>,
+}
+
+#[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<W: LayoutElement> {
+ /// 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<Output>,
+
+ /// 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<i32, Logical>,
+
+ /// 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<i32, Logical>,
+
+ /// Columns of windows on this workspace.
+ columns: Vec<Column<W>>,
+
+ /// 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<Animation>,
+
+ /// 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<Options>,
+}
+
+#[derive(Debug)]
+struct FocusRing {
+ buffers: [SolidColorBuffer; 4],
+ locations: [Point<i32, Logical>; 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<ColumnWidth>,
+ /// Initial width for new windows.
+ default_width: Option<ColumnWidth>,
+}
+
+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<PresetWidth> 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<W: LayoutElement> {
+ /// Windows in this column.
+ ///
+ /// Must be non-empty.
+ windows: Vec<W>,
+
+ /// Heights of the windows.
+ ///
+ /// Must have the same number of elements as `windows`.
+ heights: Vec<WindowHeight>,
+
+ /// 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<i32, Logical>,
+
+ /// Latest known working area for this column's workspace.
+ working_area: Rectangle<i32, Logical>,
+
+ /// Configurable properties of the layout.
+ options: Rc<Options>,
+}
+
+impl OutputId {
+ pub fn new(output: &Output) -> Self {
+ Self(output.name())
+ }
+}
+
+impl LayoutElement for Window {
+ fn request_size(&self, size: Size<i32, Logical>) {
+ self.toplevel().with_pending_state(|state| {
+ state.size = Some(size);
+ state.states.unset(xdg_toplevel::State::Fullscreen);
+ });
+ }
+
+ fn request_fullscreen(&self, size: Size<i32, Logical>) {
+ self.toplevel().with_pending_state(|state| {
+ state.size = Some(size);
+ state.states.set(xdg_toplevel::State::Fullscreen);
+ });
+ }
+
+ fn min_size(&self) -> Size<i32, Logical> {
+ with_states(self.toplevel().wl_surface(), |state| {
+ let curr = state.cached_state.current::<SurfaceCachedState>();
+ curr.min_size
+ })
+ }
+
+ fn max_size(&self) -> Size<i32, Logical> {
+ with_states(self.toplevel().wl_surface(), |state| {
+ let curr = state.cached_state.current::<SurfaceCachedState>();
+ 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<i32, Logical>,
+ win_size: Size<i32, Logical>,
+ 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<f64>) -> impl Iterator<Item = SolidColorRenderElement> {
+ let mut rv = ArrayVec::<_, 4>::new();
+
+ if self.is_off {
+ return rv.into_iter();
+ }
+
+ let mut push = |buffer, location: Point<i32, Logical>| {
+ 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<W: LayoutElement> Layout<W> {
+ 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<ColumnWidth>,
+ 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<i32> {
+ 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<W>> {
+ 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<Item = &W> + '_ {
+ 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<W>> {
+ 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<W>> {
+ let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
+ return None;
+ };
+
+ monitors.iter().find(|monitor| &monitor.output == output)
+ }
+
+ pub fn outputs(&self) -> impl Iterator<Item = &Output> + '_ {
+ 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) {