diff options
Diffstat (limited to 'src/layout/mod.rs')
| -rw-r--r-- | src/layout/mod.rs | 4025 |
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) { |
