From 6fcdb4192275a8112fd45a91eb3e4b70c5ac684f Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 5 Oct 2023 09:25:07 +0400 Subject: Refactor layout for configurability, add preset-column-widths option layout.rs finally gets a struct actually named Layout. --- src/config.rs | 23 ++- src/handlers/compositor.rs | 14 +- src/handlers/layer_shell.rs | 6 +- src/handlers/mod.rs | 2 +- src/handlers/xdg_shell.rs | 25 ++- src/input.rs | 74 ++++--- src/layout.rs | 456 +++++++++++++++++++++++++++++--------------- src/main.rs | 2 +- src/niri.rs | 47 ++--- 9 files changed, 411 insertions(+), 238 deletions(-) (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 730a0f4b..bb8cfcf6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,8 @@ pub struct Config { pub prefer_no_csd: bool, #[knuffel(child, default)] pub cursor: Cursor, + #[knuffel(child, unwrap(children), default)] + pub preset_column_widths: Vec, #[knuffel(child, default)] pub binds: Binds, #[knuffel(child, default)] @@ -125,7 +127,7 @@ pub struct SpawnAtStartup { pub command: Vec, } -#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct FocusRing { #[knuffel(child)] pub off: bool, @@ -189,6 +191,12 @@ impl Default for Cursor { } } +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub enum PresetWidth { + Proportion(#[knuffel(argument)] f64), + Fixed(#[knuffel(argument)] i32), +} + #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Binds(#[knuffel(children)] pub Vec); @@ -513,6 +521,13 @@ mod tests { xcursor-size 16 } + preset-column-widths { + proportion 0.25 + proportion 0.5 + fixed 960 + fixed 1280 + } + binds { Mod+T { spawn "alacritty"; } Mod+Q { close-window; } @@ -580,6 +595,12 @@ mod tests { xcursor_theme: String::from("breeze_cursors"), xcursor_size: 16, }, + preset_column_widths: vec![ + PresetWidth::Proportion(0.25), + PresetWidth::Proportion(0.5), + PresetWidth::Fixed(960), + PresetWidth::Fixed(1280), + ], binds: Binds(vec![ Bind { key: Key { diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index b77ef829..d8d93d9c 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -87,7 +87,7 @@ impl CompositorHandler for State { let window = entry.remove(); window.on_commit(); - if let Some(output) = self.niri.monitor_set.add_window(window, true).cloned() { + if let Some(output) = self.niri.layout.add_window(window, true).cloned() { self.niri.queue_redraw(output); } return; @@ -100,7 +100,7 @@ impl CompositorHandler for State { } // This is a commit of a previously-mapped root or a non-toplevel root. - if let Some((window, output)) = self.niri.monitor_set.find_window_and_output(surface) { + if let Some((window, output)) = self.niri.layout.find_window_and_output(surface) { // This is a commit of a previously-mapped toplevel. window.on_commit(); @@ -110,14 +110,14 @@ impl CompositorHandler for State { if !is_mapped { // The toplevel got unmapped. - self.niri.monitor_set.remove_window(&window); + self.niri.layout.remove_window(&window); self.niri.unmapped_windows.insert(surface.clone(), window); self.niri.queue_redraw(output); return; } // The toplevel remains mapped. - self.niri.monitor_set.update_window(&window); + self.niri.layout.update_window(&window); self.niri.queue_redraw(output); return; @@ -127,10 +127,10 @@ impl CompositorHandler for State { } // This is a commit of a non-root or a non-toplevel root. - let root_window_output = self.niri.monitor_set.find_window_and_output(&root_surface); + let root_window_output = self.niri.layout.find_window_and_output(&root_surface); if let Some((window, output)) = root_window_output { window.on_commit(); - self.niri.monitor_set.update_window(&window); + self.niri.layout.update_window(&window); self.niri.queue_redraw(output); return; } @@ -139,7 +139,7 @@ impl CompositorHandler for State { self.popups_handle_commit(surface); if let Some(popup) = self.niri.popups.find_popup(surface) { if let Ok(root) = find_popup_root_surface(&popup) { - let root_window_output = self.niri.monitor_set.find_window_and_output(&root); + let root_window_output = self.niri.layout.find_window_and_output(&root); if let Some((_window, output)) = root_window_output { self.niri.queue_redraw(output); } diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index c426d321..6e0ef318 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -26,7 +26,7 @@ impl WlrLayerShellHandler for State { let output = wl_output .as_ref() .and_then(Output::from_resource) - .or_else(|| self.niri.monitor_set.active_output().cloned()) + .or_else(|| self.niri.layout.active_output().cloned()) .unwrap(); let mut map = layer_map_for_output(&output); map.map_layer(&LayerSurface::new(surface, namespace)) @@ -35,7 +35,7 @@ impl WlrLayerShellHandler for State { fn layer_destroyed(&mut self, surface: WlrLayerSurface) { let output = if let Some((output, mut map, layer)) = - self.niri.monitor_set.outputs().find_map(|o| { + self.niri.layout.outputs().find_map(|o| { let map = layer_map_for_output(o); let layer = map .layers() @@ -59,7 +59,7 @@ impl State { pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) { let Some(output) = self .niri - .monitor_set + .layout .outputs() .find(|o| { let map = layer_map_for_output(o); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 546d22e0..87d4af4d 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -65,7 +65,7 @@ impl InputMethodHandler for State { } fn parent_geometry(&self, parent: &WlSurface) -> Rectangle { self.niri - .monitor_set + .layout .find_window_and_output(parent) .map(|(window, _)| window.geometry()) .unwrap_or_default() diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index e8cee1b5..c5af09a9 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -1,4 +1,4 @@ -use smithay::desktop::{find_popup_root_surface, layer_map_for_output, PopupKind, Window}; +use smithay::desktop::{find_popup_root_surface, PopupKind, 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::{self, ResizeEdge}; @@ -15,7 +15,6 @@ use smithay::wayland::shell::xdg::{ }; use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell}; -use crate::layout::configure_new_window; use crate::niri::State; impl XdgShellHandler for State { @@ -28,9 +27,9 @@ impl XdgShellHandler for State { let window = Window::new(surface); // Tell the surface the preferred size and bounds for its likely output. - let output = self.niri.monitor_set.active_output().unwrap(); - let working_area = layer_map_for_output(output).non_exclusive_zone(); - configure_new_window(working_area, &window); + if let Some(ws) = self.niri.layout.active_workspace() { + ws.configure_new_window(&window); + } // At the moment of creation, xdg toplevels must have no buffer. let existing = self.niri.unmapped_windows.insert(wl_surface, window); @@ -106,18 +105,18 @@ impl XdgShellHandler for State { // independently from its buffer size if let Some((window, current_output)) = self .niri - .monitor_set + .layout .find_window_and_output(surface.wl_surface()) { if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) { if requested_output != current_output { self.niri - .monitor_set + .layout .move_window_to_output(window.clone(), &requested_output); } } - self.niri.monitor_set.set_fullscreen(&window, true); + self.niri.layout.set_fullscreen(&window, true); } } @@ -129,10 +128,10 @@ impl XdgShellHandler for State { fn unfullscreen_request(&mut self, surface: ToplevelSurface) { if let Some((window, _)) = self .niri - .monitor_set + .layout .find_window_and_output(surface.wl_surface()) { - self.niri.monitor_set.set_fullscreen(&window, false); + self.niri.layout.set_fullscreen(&window, false); } } @@ -149,16 +148,16 @@ impl XdgShellHandler for State { let (window, output) = self .niri - .monitor_set + .layout .find_window_and_output(surface.wl_surface()) .unwrap(); - self.niri.monitor_set.remove_window(&window); + self.niri.layout.remove_window(&window); self.niri.queue_redraw(output); } fn popup_destroyed(&mut self, surface: PopupSurface) { if let Ok(root) = find_popup_root_surface(&surface.into()) { - let root_window_output = self.niri.monitor_set.find_window_and_output(&root); + let root_window_output = self.niri.layout.find_window_and_output(&root); if let Some((_window, output)) = root_window_output { self.niri.queue_redraw(output); } diff --git a/src/input.rs b/src/input.rs index b76a98af..033b90f4 100644 --- a/src/input.rs +++ b/src/input.rs @@ -101,9 +101,7 @@ impl State { // doesn't always trigger due to damage, etc. So run it here right before it might prove // important. Besides, animations affect the input, so it's best to have up-to-date values // here. - self.niri - .monitor_set - .advance_animations(get_monotonic_time()); + self.niri.layout.advance_animations(get_monotonic_time()); let comp_mod = self.backend.mod_key(); @@ -150,7 +148,7 @@ impl State { } } Action::Screenshot => { - let active = self.niri.monitor_set.active_output().cloned(); + let active = self.niri.layout.active_output().cloned(); if let Some(active) = active { if let Some(renderer) = self.backend.renderer() { if let Err(err) = self.niri.screenshot(renderer, &active) { @@ -160,144 +158,144 @@ impl State { } } Action::CloseWindow => { - if let Some(window) = self.niri.monitor_set.focus() { + if let Some(window) = self.niri.layout.focus() { window.toplevel().send_close(); } } Action::FullscreenWindow => { - let focus = self.niri.monitor_set.focus().cloned(); + let focus = self.niri.layout.focus().cloned(); if let Some(window) = focus { - self.niri.monitor_set.toggle_fullscreen(&window); + self.niri.layout.toggle_fullscreen(&window); } } Action::MoveColumnLeft => { - self.niri.monitor_set.move_left(); + self.niri.layout.move_left(); // FIXME: granular self.niri.queue_redraw_all(); } Action::MoveColumnRight => { - self.niri.monitor_set.move_right(); + self.niri.layout.move_right(); // FIXME: granular self.niri.queue_redraw_all(); } Action::MoveWindowDown => { - self.niri.monitor_set.move_down(); + self.niri.layout.move_down(); // FIXME: granular self.niri.queue_redraw_all(); } Action::MoveWindowUp => { - self.niri.monitor_set.move_up(); + self.niri.layout.move_up(); // FIXME: granular self.niri.queue_redraw_all(); } Action::FocusColumnLeft => { - self.niri.monitor_set.focus_left(); + self.niri.layout.focus_left(); } Action::FocusColumnRight => { - self.niri.monitor_set.focus_right(); + self.niri.layout.focus_right(); } Action::FocusWindowDown => { - self.niri.monitor_set.focus_down(); + self.niri.layout.focus_down(); } Action::FocusWindowUp => { - self.niri.monitor_set.focus_up(); + self.niri.layout.focus_up(); } Action::MoveWindowToWorkspaceDown => { - self.niri.monitor_set.move_to_workspace_down(); + self.niri.layout.move_to_workspace_down(); // FIXME: granular self.niri.queue_redraw_all(); } Action::MoveWindowToWorkspaceUp => { - self.niri.monitor_set.move_to_workspace_up(); + self.niri.layout.move_to_workspace_up(); // FIXME: granular self.niri.queue_redraw_all(); } Action::MoveWindowToWorkspace(idx) => { - self.niri.monitor_set.move_to_workspace(idx); + self.niri.layout.move_to_workspace(idx); // FIXME: granular self.niri.queue_redraw_all(); } Action::FocusWorkspaceDown => { - self.niri.monitor_set.switch_workspace_down(); + self.niri.layout.switch_workspace_down(); // FIXME: granular self.niri.queue_redraw_all(); } Action::FocusWorkspaceUp => { - self.niri.monitor_set.switch_workspace_up(); + self.niri.layout.switch_workspace_up(); // FIXME: granular self.niri.queue_redraw_all(); } Action::FocusWorkspace(idx) => { - self.niri.monitor_set.switch_workspace(idx); + self.niri.layout.switch_workspace(idx); // FIXME: granular self.niri.queue_redraw_all(); } Action::ConsumeWindowIntoColumn => { - self.niri.monitor_set.consume_into_column(); + self.niri.layout.consume_into_column(); // FIXME: granular self.niri.queue_redraw_all(); } Action::ExpelWindowFromColumn => { - self.niri.monitor_set.expel_from_column(); + self.niri.layout.expel_from_column(); // FIXME: granular self.niri.queue_redraw_all(); } Action::SwitchPresetColumnWidth => { - self.niri.monitor_set.toggle_width(); + self.niri.layout.toggle_width(); } Action::MaximizeColumn => { - self.niri.monitor_set.toggle_full_width(); + self.niri.layout.toggle_full_width(); } Action::FocusMonitorLeft => { if let Some(output) = self.niri.output_left() { - self.niri.monitor_set.focus_output(&output); + self.niri.layout.focus_output(&output); self.move_cursor_to_output(&output); } } Action::FocusMonitorRight => { if let Some(output) = self.niri.output_right() { - self.niri.monitor_set.focus_output(&output); + self.niri.layout.focus_output(&output); self.move_cursor_to_output(&output); } } Action::FocusMonitorDown => { if let Some(output) = self.niri.output_down() { - self.niri.monitor_set.focus_output(&output); + self.niri.layout.focus_output(&output); self.move_cursor_to_output(&output); } } Action::FocusMonitorUp => { if let Some(output) = self.niri.output_up() { - self.niri.monitor_set.focus_output(&output); + self.niri.layout.focus_output(&output); self.move_cursor_to_output(&output); } } Action::MoveWindowToMonitorLeft => { if let Some(output) = self.niri.output_left() { - self.niri.monitor_set.move_to_output(&output); + self.niri.layout.move_to_output(&output); self.move_cursor_to_output(&output); } } Action::MoveWindowToMonitorRight => { if let Some(output) = self.niri.output_right() { - self.niri.monitor_set.move_to_output(&output); + self.niri.layout.move_to_output(&output); self.move_cursor_to_output(&output); } } Action::MoveWindowToMonitorDown => { if let Some(output) = self.niri.output_down() { - self.niri.monitor_set.move_to_output(&output); + self.niri.layout.move_to_output(&output); self.move_cursor_to_output(&output); } } Action::MoveWindowToMonitorUp => { if let Some(output) = self.niri.output_up() { - self.niri.monitor_set.move_to_output(&output); + self.niri.layout.move_to_output(&output); self.move_cursor_to_output(&output); } } Action::SetColumnWidth(change) => { - self.niri.monitor_set.set_column_width(change); + self.niri.layout.set_column_width(change); } } } @@ -415,9 +413,9 @@ impl State { if ButtonState::Pressed == button_state && !pointer.is_grabbed() { if let Some(window) = self.niri.window_under_cursor() { let window = window.clone(); - self.niri.monitor_set.activate_window(&window); + self.niri.layout.activate_window(&window); } else if let Some(output) = self.niri.output_under_cursor() { - self.niri.monitor_set.activate_output(&output); + self.niri.layout.activate_output(&output); } }; @@ -546,9 +544,9 @@ impl State { if !pointer.is_grabbed() { if let Some(window) = self.niri.window_under_cursor() { let window = window.clone(); - self.niri.monitor_set.activate_window(&window); + self.niri.layout.activate_window(&window); } else if let Some(output) = self.niri.output_under_cursor() { - self.niri.monitor_set.activate_output(&output); + self.niri.layout.activate_output(&output); } }; } diff --git a/src/layout.rs b/src/layout.rs index 308af207..a8a935f2 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -31,6 +31,7 @@ use std::cmp::{max, min}; use std::mem; +use std::rc::Rc; use std::time::Duration; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; @@ -52,14 +53,9 @@ use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::SurfaceCachedState; use crate::animation::Animation; -use crate::config::{Color, Config, SizeChange}; +use crate::config::{self, Color, Config, PresetWidth, SizeChange}; const PADDING: i32 = 16; -const WIDTH_PROPORTIONS: [ColumnWidth; 3] = [ - ColumnWidth::Proportion(1. / 3.), - ColumnWidth::Proportion(0.5), - ColumnWidth::Proportion(2. / 3.), -]; #[derive(Debug, Clone, PartialEq, Eq)] pub struct OutputId(String); @@ -82,7 +78,15 @@ pub trait LayoutElement: SpaceElement + PartialEq + Clone { } #[derive(Debug)] -pub enum MonitorSet { +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. @@ -93,7 +97,10 @@ pub enum MonitorSet { active_monitor_idx: usize, }, /// No outputs are connected, and these are the workspaces. - NoOutputs(Vec>), + NoOutputs { + /// The workspaces. + workspaces: Vec>, + }, } #[derive(Debug)] @@ -106,6 +113,8 @@ pub struct Monitor { active_workspace_idx: usize, /// Animation for workspace switching. workspace_idx_anim: Option, + /// Configurable properties of the layout. + options: Rc, } #[derive(Debug)] @@ -155,6 +164,9 @@ pub struct Workspace { /// 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)] @@ -166,8 +178,49 @@ struct FocusRing { inactive_color: Color, } +#[derive(Debug, PartialEq)] +struct Options { + focus_ring: config::FocusRing, + /// Column widths that `toggle_width()` switches between. + preset_widths: Vec, +} + +impl Default for Options { + fn default() -> Self { + Self { + focus_ring: Default::default(), + preset_widths: vec![ + ColumnWidth::Proportion(1. / 3.), + ColumnWidth::Proportion(0.5), + ColumnWidth::Proportion(2. / 3.), + ], + } + } +} + +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() + }; + + Self { + focus_ring: config.focus_ring, + preset_widths, + } + } +} + /// Width of a column. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] enum ColumnWidth { /// Proportion of the current view width. Proportion(f64), @@ -175,11 +228,20 @@ enum ColumnWidth { /// /// This is separate from Proportion in order to be able to reliably cycle between preset /// proportions. - PresetProportion(usize), + 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)), + } + } +} + #[derive(Debug)] struct Column { /// Windows in this column. @@ -201,6 +263,9 @@ struct Column { /// Latest known working area for this column's workspace. working_area: Rectangle, + + /// Configurable properties of the layout. + options: Rc, } impl OutputId { @@ -277,23 +342,23 @@ impl FocusRing { } } -impl Default for FocusRing { - fn default() -> Self { +impl FocusRing { + fn new(config: config::FocusRing) -> Self { Self { buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.]), - is_off: true, - width: 0, - active_color: Color::default(), - inactive_color: Color::default(), + is_off: config.off, + width: config.width.into(), + active_color: config.active_color, + inactive_color: config.inactive_color, } } } impl ColumnWidth { - fn resolve(self, view_width: i32) -> i32 { + fn resolve(self, options: &Options, view_width: i32) -> i32 { match self { ColumnWidth::Proportion(proportion) => (view_width as f64 * proportion).floor() as i32, - ColumnWidth::PresetProportion(idx) => WIDTH_PROPORTIONS[idx].resolve(view_width), + ColumnWidth::Preset(idx) => options.preset_widths[idx].resolve(options, view_width), // FIXME: remove this PADDING from here after redesigning how padding works. ColumnWidth::Fixed(width) => width + PADDING, } @@ -306,15 +371,18 @@ impl Default for ColumnWidth { } } -impl MonitorSet { - pub fn new() -> Self { - Self::NoOutputs(vec![]) +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 = match mem::take(self) { + self.monitor_set = match mem::take(&mut self.monitor_set) { MonitorSet::Normal { mut monitors, primary_idx, @@ -337,29 +405,30 @@ impl MonitorSet { workspaces.reverse(); if workspaces.iter().all(|ws| ws.has_windows()) { // Make sure there's always an empty workspace. - workspaces.push(Workspace::new(output.clone())); + 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)); + monitors.push(Monitor::new(output, workspaces, self.options.clone())); MonitorSet::Normal { monitors, primary_idx, active_monitor_idx, } } - MonitorSet::NoOutputs(mut workspaces) => { + MonitorSet::NoOutputs { mut workspaces } => { // We know there are no empty workspaces there, so add one. - workspaces.push(Workspace::new(output.clone())); + 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); + let monitor = Monitor::new(output, workspaces, self.options.clone()); + MonitorSet::Normal { monitors: vec![monitor], primary_idx: 0, @@ -370,7 +439,7 @@ impl MonitorSet { } pub fn remove_output(&mut self, output: &Output) { - *self = match mem::take(self) { + self.monitor_set = match mem::take(&mut self.monitor_set) { MonitorSet::Normal { mut monitors, mut primary_idx, @@ -392,7 +461,7 @@ impl MonitorSet { if monitors.is_empty() { // Removed the last monitor. - MonitorSet::NoOutputs(workspaces) + MonitorSet::NoOutputs { workspaces } } else { if primary_idx >= idx { // Update primary_idx to either still point at the same monitor, or at some @@ -422,7 +491,7 @@ impl MonitorSet { } } } - MonitorSet::NoOutputs(_) => { + MonitorSet::NoOutputs { .. } => { panic!("tried to remove output when there were already none") } } @@ -439,7 +508,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &mut self.monitor_set else { panic!() }; @@ -455,7 +524,7 @@ impl MonitorSet { /// /// Returns an output that the window was added to, if there were any outputs. pub fn add_window(&mut self, window: W, activate: bool) -> Option<&Output> { - match self { + match &mut self.monitor_set { MonitorSet::Normal { monitors, active_monitor_idx, @@ -465,11 +534,11 @@ impl MonitorSet { mon.add_window(mon.active_workspace_idx, window, activate); Some(&mon.output) } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces } => { let ws = if let Some(ws) = workspaces.get_mut(0) { ws } else { - workspaces.push(Workspace::new_no_outputs()); + workspaces.push(Workspace::new_no_outputs(self.options.clone())); &mut workspaces[0] }; ws.add_window(window, activate); @@ -479,7 +548,7 @@ impl MonitorSet { } pub fn remove_window(&mut self, window: &W) { - match self { + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for (idx, ws) in mon.workspaces.iter_mut().enumerate() { @@ -503,7 +572,7 @@ impl MonitorSet { } } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { for (idx, ws) in workspaces.iter_mut().enumerate() { if ws.has_window(window) { ws.remove_window(window); @@ -521,7 +590,7 @@ impl MonitorSet { } pub fn update_window(&mut self, window: &W) { - match self { + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mut mon.workspaces { @@ -532,7 +601,7 @@ impl MonitorSet { } } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { if ws.has_window(window) { ws.update_window(window); @@ -544,7 +613,7 @@ impl MonitorSet { } pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(W, Output)> { - if let MonitorSet::Normal { monitors, .. } = self { + 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) { @@ -558,7 +627,7 @@ impl MonitorSet { } pub fn update_output_size(&mut self, output: &Output) { - let MonitorSet::Normal { monitors, .. } = self else { + let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else { panic!() }; @@ -581,7 +650,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &mut self.monitor_set else { todo!() }; @@ -604,7 +673,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &mut self.monitor_set else { return; }; @@ -621,7 +690,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &self.monitor_set else { return None; }; @@ -629,8 +698,22 @@ impl MonitorSet { 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 workspace_for_output(&self, output: &Output) -> Option<&Workspace> { - let MonitorSet::Normal { monitors, .. } = self else { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { return None; }; @@ -644,7 +727,7 @@ impl MonitorSet { } pub fn windows_for_output(&self, output: &Output) -> impl Iterator + '_ { - let MonitorSet::Normal { monitors, .. } = self else { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { panic!() }; @@ -657,7 +740,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &mut self.monitor_set else { return None; }; @@ -666,7 +749,7 @@ impl MonitorSet { } pub fn monitor_for_output(&self, output: &Output) -> Option<&Monitor> { - let MonitorSet::Normal { monitors, .. } = self else { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { return None; }; @@ -674,7 +757,7 @@ impl MonitorSet { } pub fn outputs(&self) -> impl Iterator + '_ { - let monitors = if let MonitorSet::Normal { monitors, .. } = self { + let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { &monitors[..] } else { &[][..] @@ -800,7 +883,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &self.monitor_set else { return None; }; @@ -819,19 +902,24 @@ impl MonitorSet { #[cfg(test)] fn verify_invariants(&self) { - let (monitors, &primary_idx, &active_monitor_idx) = match &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) => { + 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(); } @@ -849,6 +937,11 @@ impl MonitorSet { ); 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 { @@ -866,13 +959,18 @@ impl MonitorSet { // 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) { - match self { + match &mut self.monitor_set { MonitorSet::Normal { monitors, active_monitor_idx, @@ -882,7 +980,7 @@ impl MonitorSet { mon.advance_animations(current_time, idx == *active_monitor_idx); } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { ws.advance_animations(current_time, false); } @@ -891,18 +989,22 @@ impl MonitorSet { } pub fn update_config(&mut self, config: &Config) { - match self { + let options = Rc::new(Options::from_config(config)); + + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { - mon.update_config(config); + mon.update_config(options.clone()); } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces } => { for ws in workspaces { - ws.update_config(config); + ws.update_config(options.clone()); } } } + + self.options = options; } pub fn toggle_width(&mut self) { @@ -931,7 +1033,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &mut self.monitor_set { for (idx, mon) in monitors.iter().enumerate() { if &mon.output == output { @@ -947,7 +1049,7 @@ impl MonitorSet { monitors, active_monitor_idx, .. - } = self + } = &mut self.monitor_set { let new_idx = monitors .iter() @@ -971,7 +1073,7 @@ impl MonitorSet { pub fn move_window_to_output(&mut self, window: W, output: &Output) { self.remove_window(&window); - if let MonitorSet::Normal { monitors, .. } = self { + if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set { let new_idx = monitors .iter() .position(|mon| &mon.output == output) @@ -984,7 +1086,7 @@ impl MonitorSet { } pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) { - match self { + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mut mon.workspaces { @@ -995,7 +1097,7 @@ impl MonitorSet { } } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { if ws.has_window(window) { ws.set_fullscreen(window, is_fullscreen); @@ -1007,7 +1109,7 @@ impl MonitorSet { } pub fn toggle_fullscreen(&mut self, window: &W) { - match self { + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mut mon.workspaces { @@ -1018,7 +1120,7 @@ impl MonitorSet { } } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { if ws.has_window(window) { ws.toggle_fullscreen(window); @@ -1030,11 +1132,11 @@ impl MonitorSet { } } -impl MonitorSet { +impl Layout { pub fn refresh(&self) { let _span = tracy_client::span!("MonitorSet::refresh"); - match self { + match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mon.workspaces { @@ -1042,7 +1144,7 @@ impl MonitorSet { } } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { ws.refresh(); } @@ -1053,17 +1155,18 @@ impl MonitorSet { impl Default for MonitorSet { fn default() -> Self { - Self::new() + Self::NoOutputs { workspaces: vec![] } } } impl Monitor { - fn new(output: Output, workspaces: Vec>) -> Self { + fn new(output: Output, workspaces: Vec>, options: Rc) -> Self { Self { output, workspaces, active_workspace_idx: 0, workspace_idx_anim: None, + options, } } @@ -1101,7 +1204,7 @@ impl Monitor { if workspace_idx == self.workspaces.len() - 1 { // Insert a new empty workspace. - let ws = Workspace::new(self.output.clone()); + let ws = Workspace::new(self.output.clone(), self.options.clone()); self.workspaces.push(ws); } @@ -1279,10 +1382,12 @@ impl Monitor { || self.workspaces.iter().any(|ws| ws.are_animations_ongoing()) } - pub fn update_config(&mut self, config: &Config) { + fn update_config(&mut self, options: Rc) { for ws in &mut self.workspaces { - ws.update_config(config); + ws.update_config(options.clone()); } + + self.options = options; } fn toggle_width(&mut self) { @@ -1366,7 +1471,7 @@ impl Monitor { } impl Workspace { - fn new(output: Output) -> Self { + fn new(output: Output, options: Rc) -> Self { let working_area = layer_map_for_output(&output).non_exclusive_zone(); Self { original_output: OutputId::new(&output), @@ -1375,14 +1480,15 @@ impl Workspace { output: Some(output), columns: vec![], active_column_idx: 0, - focus_ring: FocusRing::default(), + focus_ring: FocusRing::new(options.focus_ring), view_offset: 0, view_offset_anim: None, activate_prev_column_on_removal: false, + options, } } - fn new_no_outputs() -> Self { + fn new_no_outputs(options: Rc) -> Self { Self { output: None, original_output: OutputId(String::new()), @@ -1390,10 +1496,11 @@ impl Workspace { working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)), columns: vec![], active_column_idx: 0, - focus_ring: FocusRing::default(), + focus_ring: FocusRing::new(options.focus_ring), view_offset: 0, view_offset_anim: None, activate_prev_column_on_removal: false, + options, } } @@ -1423,13 +1530,19 @@ impl Workspace { self.view_offset_anim.is_some() } - pub fn update_config(&mut self, config: &Config) { - let c = &config.focus_ring; + fn update_config(&mut self, options: Rc) { + let c = &options.focus_ring; self.focus_ring.is_off = c.off; self.focus_ring.width = c.width.into(); self.focus_ring.active_color = c.active_color; self.focus_ring.inactive_color = c.inactive_color; // The focus ring buffer will be updated in a subsequent update_animations call. + + for column in &mut self.columns { + column.update_config(options.clone()); + } + + self.options = options; } fn windows(&self) -> impl Iterator + '_ { @@ -1482,6 +1595,24 @@ impl Workspace { } } + pub fn configure_new_window(&self, window: &Window) { + let width = ColumnWidth::default() + .resolve(&self.options, self.working_area.size.w - PADDING) + - PADDING; + let height = self.working_area.size.h - PADDING * 2; + let size = Size::from((max(width, 1), max(height, 1))); + + let bounds = Size::from(( + self.working_area.size.w - PADDING * 2, + self.working_area.size.h - PADDING * 2, + )); + + window.toplevel().with_pending_state(|state| { + state.size = Some(size); + state.bounds = Some(bounds); + }); + } + fn activate_column(&mut self, idx: usize) { if self.active_column_idx == idx { return; @@ -1549,7 +1680,12 @@ impl Workspace { self.active_column_idx + 1 }; - let column = Column::new(window, self.view_size, self.working_area); + let column = Column::new( + window, + self.view_size, + self.working_area, + self.options.clone(), + ); self.columns.insert(idx, column); if activate { @@ -1923,7 +2059,12 @@ impl Workspace { col_idx += 1; self.columns.insert( col_idx, - Column::new(window, self.view_size, self.working_area), + Column::new( + window, + self.view_size, + self.working_area, + self.options.clone(), + ), ); if self.active_column_idx >= col_idx || target_window_was_focused { self.active_column_idx += 1; @@ -2046,6 +2187,7 @@ impl Column { window: W, view_size: Size, working_area: Rectangle, + options: Rc, ) -> Self { let mut rv = Self { windows: vec![], @@ -2054,6 +2196,7 @@ impl Column { is_fullscreen: false, view_size, working_area, + options, }; rv.add_window(window); @@ -2072,6 +2215,17 @@ impl Column { self.update_window_sizes(); } + fn update_config(&mut self, options: Rc) { + // If preset widths changed, make our width non-preset. + if self.options.preset_widths != options.preset_widths { + if let ColumnWidth::Preset(idx) = self.width { + self.width = self.options.preset_widths[idx]; + } + } + + self.options = options; + } + fn window_count(&self) -> usize { self.windows.len() } @@ -2130,7 +2284,10 @@ impl Column { .unwrap_or(i32::MAX); let max_width = max(max_width, min_width); - let width = self.width.resolve(self.working_area.size.w - PADDING) - PADDING; + let width = self + .width + .resolve(&self.options, self.working_area.size.w - PADDING) + - PADDING; let height = (self.working_area.size.h - PADDING) / self.window_count() as i32 - PADDING; let size = Size::from((max(min(width, max_width), min_width), max(height, 1))); @@ -2192,18 +2349,20 @@ impl Column { fn toggle_width(&mut self) { let idx = match self.width { - ColumnWidth::PresetProportion(idx) => (idx + 1) % WIDTH_PROPORTIONS.len(), + ColumnWidth::Preset(idx) => (idx + 1) % self.options.preset_widths.len(), _ => { let current = self.size().w; - WIDTH_PROPORTIONS - .into_iter() + self.options + .preset_widths + .iter() .position(|prop| { - prop.resolve(self.working_area.size.w - PADDING) - PADDING > current + prop.resolve(&self.options, self.working_area.size.w - PADDING) - PADDING + > current }) .unwrap_or(0) } }; - let width = ColumnWidth::PresetProportion(idx); + let width = ColumnWidth::Preset(idx); self.set_width(width); } @@ -2219,10 +2378,13 @@ impl Column { } fn set_column_width(&mut self, change: SizeChange) { - let current_px = self.width.resolve(self.working_area.size.w - PADDING) - PADDING; + let current_px = self + .width + .resolve(&self.options, self.working_area.size.w - PADDING) + - PADDING; let current = match self.width { - ColumnWidth::PresetProportion(idx) => WIDTH_PROPORTIONS[idx], + ColumnWidth::Preset(idx) => self.options.preset_widths[idx], current => current, }; @@ -2249,7 +2411,7 @@ impl Column { let proportion = (current + delta / 100.).clamp(0., MAX_F); ColumnWidth::Proportion(proportion) } - (ColumnWidth::PresetProportion(_), _) => unreachable!(), + (ColumnWidth::Preset(_), _) => unreachable!(), }; self.set_width(width); @@ -2286,22 +2448,6 @@ pub fn output_size(output: &Output) -> Size { .to_logical(output_scale) } -pub fn configure_new_window(working_area: Rectangle, window: &Window) { - let width = ColumnWidth::default().resolve(working_area.size.w - PADDING) - PADDING; - let height = working_area.size.h - PADDING * 2; - let size = Size::from((max(width, 1), max(height, 1))); - - let bounds = Size::from(( - working_area.size.w - PADDING * 2, - working_area.size.h - PADDING * 2, - )); - - window.toplevel().with_pending_state(|state| { - state.size = Some(size); - state.bounds = Some(bounds); - }); -} - fn compute_new_view_offset(cur_x: i32, view_width: i32, new_x: i32, new_col_width: i32) -> i32 { // If the column is wider than the view, always left-align it. if new_col_width + PADDING * 2 >= view_width { @@ -2335,6 +2481,15 @@ mod tests { use super::*; + impl Default for Layout { + fn default() -> Self { + Self { + monitor_set: MonitorSet::NoOutputs { workspaces: vec![] }, + options: Rc::new(Options::default()), + } + } + } + #[derive(Debug)] struct TestWindowInner { id: usize, @@ -2480,11 +2635,11 @@ mod tests { } impl Op { - fn apply(self, monitor_set: &mut MonitorSet) { + fn apply(self, layout: &mut Layout) { match self { Op::AddOutput(id) => { let name = format!("output{id}"); - if monitor_set.outputs().any(|o| o.name() == name) { + if layout.outputs().any(|o| o.name() == name) { return; } @@ -2506,69 +2661,66 @@ mod tests { None, None, ); - monitor_set.add_output(output.clone()); + layout.add_output(output.clone()); } Op::RemoveOutput(id) => { let name = format!("output{id}"); - let Some(output) = monitor_set.outputs().find(|o| o.name() == name).cloned() - else { + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { return; }; - monitor_set.remove_output(&output); + layout.remove_output(&output); } Op::FocusOutput(id) => { let name = format!("output{id}"); - let Some(output) = monitor_set.outputs().find(|o| o.name() == name).cloned() - else { + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { return; }; - monitor_set.focus_output(&output); + layout.focus_output(&output); } Op::AddWindow { id, bbox, activate } => { let win = TestWindow::new(id, bbox); - monitor_set.add_window(win, activate); + layout.add_window(win, activate); } Op::CloseWindow(id) => { let dummy = TestWindow::new(id, Rectangle::default()); - monitor_set.remove_window(&dummy); + layout.remove_window(&dummy); } Op::FullscreenWindow(id) => { let dummy = TestWindow::new(id, Rectangle::default()); - monitor_set.toggle_fullscreen(&dummy); + layout.toggle_fullscreen(&dummy); } - Op::FocusColumnLeft => monitor_set.focus_left(), - Op::FocusColumnRight => monitor_set.focus_right(), - Op::FocusWindowDown => monitor_set.focus_down(), - Op::FocusWindowUp => monitor_set.focus_up(), - Op::MoveColumnLeft => monitor_set.move_left(), - Op::MoveColumnRight => monitor_set.move_right(), - Op::MoveWindowDown => monitor_set.move_down(), - Op::MoveWindowUp => monitor_set.move_up(), - Op::ConsumeWindowIntoColumn => monitor_set.consume_into_column(), - Op::ExpelWindowFromColumn => monitor_set.expel_from_column(), - Op::FocusWorkspaceDown => monitor_set.switch_workspace_down(), - Op::FocusWorkspaceUp => monitor_set.switch_workspace_up(), - Op::FocusWorkspace(idx) => monitor_set.switch_workspace(idx), - Op::MoveWindowToWorkspaceDown => monitor_set.move_to_workspace_down(), - Op::MoveWindowToWorkspaceUp => monitor_set.move_to_workspace_up(), - Op::MoveWindowToWorkspace(idx) => monitor_set.move_to_workspace(idx), + Op::FocusColumnLeft => layout.focus_left(), + Op::FocusColumnRight => layout.focus_right(), + Op::FocusWindowDown => layout.focus_down(), + Op::FocusWindowUp => layout.focus_up(), + Op::MoveColumnLeft => layout.move_left(), + Op::MoveColumnRight => layout.move_right(), + Op::MoveWindowDown => layout.move_down(), + Op::MoveWindowUp => layout.move_up(), + Op::ConsumeWindowIntoColumn => layout.consume_into_column(), + Op::ExpelWindowFromColumn => layout.expel_from_column(), + Op::FocusWorkspaceDown => layout.switch_workspace_down(), + Op::FocusWorkspaceUp => layout.switch_workspace_up(), + Op::FocusWorkspace(idx) => layout.switch_workspace(idx), + Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(), + Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(), + Op::MoveWindowToWorkspace(idx) => layout.move_to_workspace(idx), Op::MoveWindowToOutput(id) => { let name = format!("output{id}"); - let Some(output) = monitor_set.outputs().find(|o| o.name() == name).cloned() - else { + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { return; }; - monitor_set.move_to_output(&output); + layout.move_to_output(&output); } - Op::SwitchPresetColumnWidth => monitor_set.toggle_width(), - Op::MaximizeColumn => monitor_set.toggle_full_width(), - Op::SetColumnWidth(change) => monitor_set.set_column_width(change), + Op::SwitchPresetColumnWidth => layout.toggle_width(), + Op::MaximizeColumn => layout.toggle_full_width(), + Op::SetColumnWidth(change) => layout.set_column_width(change), Op::Communicate(id) => { let mut window = None; - match monitor_set { + match &mut layout.monitor_set { MonitorSet::Normal { monitors, .. } => { 'outer: for mon in monitors { for ws in &mut mon.workspaces { @@ -2583,7 +2735,7 @@ mod tests { } } } - MonitorSet::NoOutputs(workspaces) => { + MonitorSet::NoOutputs { workspaces, .. } => { 'outer: for ws in workspaces { for win in ws.windows() { if win.0.id == id { @@ -2598,7 +2750,7 @@ mod tests { } if let Some(win) = window { - monitor_set.update_window(&win); + layout.update_window(&win); } } } @@ -2607,10 +2759,10 @@ mod tests { #[track_caller] fn check_ops(ops: &[Op]) { - let mut monitor_set = MonitorSet::default(); + let mut layout = Layout::default(); for op in ops { - op.apply(&mut monitor_set); - monitor_set.verify_invariants(); + op.apply(&mut layout); + layout.verify_invariants(); } } @@ -2667,13 +2819,13 @@ mod tests { for first in every_op { eprintln!("{first:?}, {second:?}, {third:?}"); - let mut monitor_set = MonitorSet::default(); - first.apply(&mut monitor_set); - monitor_set.verify_invariants(); - second.apply(&mut monitor_set); - monitor_set.verify_invariants(); - third.apply(&mut monitor_set); - monitor_set.verify_invariants(); + let mut layout = Layout::default(); + first.apply(&mut layout); + layout.verify_invariants(); + second.apply(&mut layout); + layout.verify_invariants(); + third.apply(&mut layout); + layout.verify_invariants(); } } } diff --git a/src/main.rs b/src/main.rs index e8b4350b..2403a20d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -115,7 +115,7 @@ fn main() { let _span = tracy_client::span!("loop callback"); // These should be called periodically, before flushing the clients. - state.niri.monitor_set.refresh(); + state.niri.layout.refresh(); state.niri.refresh_pointer_outputs(); state.niri.popups.cleanup(); state.update_focus(); diff --git a/src/niri.rs b/src/niri.rs index 68ce44e2..cc0be402 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -82,7 +82,7 @@ use crate::dbus::mutter_display_config::DisplayConfig; use crate::dbus::mutter_screen_cast::{self, ScreenCast, ToNiriMsg}; use crate::dbus::mutter_service_channel::ServiceChannel; use crate::frame_clock::FrameClock; -use crate::layout::{output_size, MonitorRenderElement, MonitorSet}; +use crate::layout::{output_size, Layout, MonitorRenderElement}; use crate::pw_utils::{Cast, PipeWire}; use crate::utils::{center, get_mon