From d0e624e6153e1a22ea0737e6830ed4edd7333304 Mon Sep 17 00:00:00 2001 From: Christian Rieger Date: Thu, 5 Sep 2024 23:37:10 +0200 Subject: Implement preset window heights --- src/input/mod.rs | 3 + src/layout/mod.rs | 84 +++++++++++++++++++++----- src/layout/monitor.rs | 4 ++ src/layout/workspace.rs | 152 +++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 204 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/input/mod.rs b/src/input/mod.rs index e5820495..6211535d 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1035,6 +1035,9 @@ impl State { Action::SwitchPresetColumnWidth => { self.niri.layout.toggle_width(); } + Action::SwitchPresetWindowHeight => { + self.niri.layout.toggle_window_height(); + } Action::CenterColumn => { self.niri.layout.center_column(); // FIXME: granular diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 35f4a2c7..ea29311e 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -34,7 +34,9 @@ use std::mem; use std::rc::Rc; use std::time::Duration; -use niri_config::{CenterFocusedColumn, Config, FloatOrInt, Struts, Workspace as WorkspaceConfig}; +use niri_config::{ + CenterFocusedColumn, Config, FloatOrInt, PresetSize, Struts, Workspace as WorkspaceConfig, +}; use niri_ipc::SizeChange; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Id; @@ -238,11 +240,12 @@ pub struct Options { pub center_focused_column: CenterFocusedColumn, pub always_center_single_column: bool, /// Column widths that `toggle_width()` switches between. - pub preset_widths: Vec, + pub preset_column_widths: Vec, /// Initial width for new columns. - pub default_width: Option, + pub default_column_width: Option, + /// Window height that `toggle_window_height()` switches between. + pub preset_window_heights: Vec, pub animations: niri_config::Animations, - // Debug flags. pub disable_resize_throttling: bool, pub disable_transactions: bool, @@ -257,15 +260,20 @@ impl Default for Options { border: Default::default(), center_focused_column: Default::default(), always_center_single_column: false, - preset_widths: vec![ + preset_column_widths: vec![ ColumnWidth::Proportion(1. / 3.), ColumnWidth::Proportion(0.5), ColumnWidth::Proportion(2. / 3.), ], - default_width: None, + default_column_width: None, animations: Default::default(), disable_resize_throttling: false, disable_transactions: false, + preset_window_heights: vec![ + PresetSize::Proportion(1. / 3.), + PresetSize::Proportion(0.5), + PresetSize::Proportion(2. / 3.), + ], } } } @@ -273,21 +281,26 @@ impl Default for Options { impl Options { fn from_config(config: &Config) -> Self { let layout = &config.layout; - let preset_column_widths = &layout.preset_column_widths; - let preset_widths = if preset_column_widths.is_empty() { - Options::default().preset_widths + let preset_column_widths = if layout.preset_column_widths.is_empty() { + Options::default().preset_column_widths } else { - preset_column_widths + layout + .preset_column_widths .iter() .copied() .map(ColumnWidth::from) .collect() }; + let preset_window_heights = if layout.preset_window_heights.is_empty() { + Options::default().preset_window_heights + } else { + layout.preset_window_heights.clone() + }; // Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)), // while present, but empty, maps to None. - let default_width = layout + let default_column_width = layout .default_column_width .as_ref() .map(|w| w.0.map(ColumnWidth::from)) @@ -300,11 +313,12 @@ impl Options { border: layout.border, center_focused_column: layout.center_focused_column, always_center_single_column: layout.always_center_single_column, - preset_widths, - default_width, + preset_column_widths, + default_column_width, animations: config.animations.clone(), disable_resize_throttling: config.debug.disable_resize_throttling, disable_transactions: config.debug.disable_transactions, + preset_window_heights, } } @@ -1937,6 +1951,13 @@ impl Layout { monitor.toggle_width(); } + pub fn toggle_window_height(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.toggle_window_height(); + } + pub fn toggle_full_width(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -3027,6 +3048,7 @@ mod tests { }, MoveColumnToOutput(#[proptest(strategy = "1..=5u8")] u8), SwitchPresetColumnWidth, + SwitchPresetWindowHeight, MaximizeColumn, SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), SetWindowHeight { @@ -3453,6 +3475,7 @@ mod tests { Op::MoveWorkspaceDown => layout.move_workspace_down(), Op::MoveWorkspaceUp => layout.move_workspace_up(), Op::SwitchPresetColumnWidth => layout.toggle_width(), + Op::SwitchPresetWindowHeight => layout.toggle_window_height(), Op::MaximizeColumn => layout.toggle_full_width(), Op::SetColumnWidth(change) => layout.set_column_width(change), Op::SetWindowHeight { id, change } => { @@ -4415,6 +4438,41 @@ mod tests { layout.verify_invariants(); } + #[test] + fn preset_height_change_removes_preset() { + let mut config = Config::default(); + config.layout.preset_window_heights = vec![PresetSize::Fixed(1), PresetSize::Fixed(2)]; + + let mut layout = Layout::new(&config); + + let ops = [ + Op::AddOutput(1), + Op::AddWindow { + id: 1, + bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)), + min_max_size: Default::default(), + }, + Op::AddWindow { + id: 2, + bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)), + min_max_size: Default::default(), + }, + Op::ConsumeOrExpelWindowLeft, + Op::SwitchPresetWindowHeight, + Op::SwitchPresetWindowHeight, + ]; + for op in ops { + op.apply(&mut layout); + } + + // Leave only one. + config.layout.preset_window_heights = vec![PresetSize::Fixed(1)]; + + layout.update_config(&config); + + layout.verify_invariants(); + } + #[test] fn working_area_starts_at_physical_pixel() { let struts = Struts { diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 3227c9c4..b712d8e9 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -740,6 +740,10 @@ impl Monitor { self.active_workspace().toggle_width(); } + pub fn toggle_window_height(&mut self) { + self.active_workspace().toggle_window_height(); + } + pub fn toggle_full_width(&mut self) { self.active_workspace().toggle_full_width(); } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 1ec54b1a..5a0ce052 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::time::Duration; use niri_config::{ - CenterFocusedColumn, OutputName, PresetWidth, Struts, Workspace as WorkspaceConfig, + CenterFocusedColumn, OutputName, PresetSize, Struts, Workspace as WorkspaceConfig, }; use niri_ipc::SizeChange; use ordered_float::NotNan; @@ -202,16 +202,17 @@ pub enum ColumnWidth { /// 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. +/// Every window but one in a column must be `Auto`-sized so that the total height can add up to +/// the workspace height. Resizing a window converts all other windows to `Auto`, weighted to +/// preserve their visual heights at the moment of the conversion. /// -/// 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. +/// In contrast to column widths, proportional height changes are converted to, and stored as, +/// fixed height right away. 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 main 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. #[derive(Debug, Clone, Copy, PartialEq)] pub enum WindowHeight { /// Automatically computed *tile* height, distributed across the column according to weights. @@ -221,6 +222,16 @@ pub enum WindowHeight { Auto { weight: f64 }, /// Fixed *window* height in logical pixels. Fixed(f64), + /// One of the *tile* height proportion presets. + Preset(usize), +} + +#[derive(Debug, Clone, Copy)] +pub enum ResolvedSize { + /// Size of the tile including borders. + Tile(f64), + /// Size of the window excluding borders. + Window(f64), } #[derive(Debug)] @@ -319,18 +330,29 @@ impl ColumnWidth { ColumnWidth::Proportion(proportion) => { (view_width - options.gaps) * proportion - options.gaps } - ColumnWidth::Preset(idx) => options.preset_widths[idx].resolve(options, view_width), + ColumnWidth::Preset(idx) => { + options.preset_column_widths[idx].resolve(options, view_width) + } ColumnWidth::Fixed(width) => width, } } } -impl From for ColumnWidth { - fn from(value: PresetWidth) -> Self { +impl From for ColumnWidth { + fn from(value: PresetSize) -> Self { match value { - PresetWidth::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)), - PresetWidth::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))), + PresetSize::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)), + PresetSize::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))), + } + } +} + +fn resolve_preset_size(preset: PresetSize, options: &Options, view_size: f64) -> ResolvedSize { + match preset { + PresetSize::Proportion(proportion) => { + ResolvedSize::Tile((view_size - options.gaps) * proportion - options.gaps) } + PresetSize::Fixed(width) => ResolvedSize::Window(f64::from(width)), } } @@ -650,7 +672,7 @@ impl Workspace { match default_width { Some(Some(width)) => Some(width), Some(None) => None, - None => self.options.default_width, + None => self.options.default_column_width, } } @@ -2351,6 +2373,17 @@ impl Workspace { cancel_resize_for_column(&mut self.interactive_resize, col); } + pub fn toggle_window_height(&mut self) { + if self.columns.is_empty() { + return; + } + + let col = &mut self.columns[self.active_column_idx]; + col.toggle_window_height(None, true); + + cancel_resize_for_column(&mut self.interactive_resize, col); + } + pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) { let (mut col_idx, tile_idx) = self .columns @@ -3016,12 +3049,18 @@ impl Column { let mut update_sizes = false; // If preset widths changed, make our width non-preset. - if self.options.preset_widths != options.preset_widths { + if self.options.preset_column_widths != options.preset_column_widths { if let ColumnWidth::Preset(idx) = self.width { - self.width = self.options.preset_widths[idx]; + self.width = self.options.preset_column_widths[idx]; } } + // If preset heights changed, make our heights non-preset. + if self.options.preset_window_heights != options.preset_window_heights { + self.convert_heights_to_auto(); + update_sizes = true; + } + if self.options.gaps != options.gaps { update_sizes = true; } @@ -3220,6 +3259,7 @@ impl Column { let width = width.resolve(&self.options, self.working_area.size.w); let width = f64::max(f64::min(width, max_width), min_width); + let height = self.working_area.size.h; // Compute the tile heights. Start by converting window heights to tile heights. let mut heights = zip(&self.tiles, &self.data) @@ -3228,8 +3268,19 @@ impl Column { WindowHeight::Fixed(height) => { WindowHeight::Fixed(tile.tile_height_for_window_height(height.round().max(1.))) } + WindowHeight::Preset(idx) => { + let preset = self.options.preset_window_heights[idx]; + let window_height = match resolve_preset_size(preset, &self.options, height) { + ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h), + ResolvedSize::Window(h) => h, + }; + let tile_height = tile + .tile_height_for_window_height(window_height.round().clamp(1., 100000.)); + WindowHeight::Fixed(tile_height) + } }) .collect::>(); + let gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64; let mut height_left = self.working_area.size.h - gaps_left; let mut auto_tiles_left = self.tiles.len(); @@ -3283,6 +3334,7 @@ impl Column { let weight = match *h { WindowHeight::Auto { weight } => weight, WindowHeight::Fixed(_) => continue, + WindowHeight::Preset(_) => unreachable!(), }; let factor = weight / total_weight_2; @@ -3321,6 +3373,7 @@ impl Column { let weight = match *h { WindowHeight::Auto { weight } => weight, WindowHeight::Fixed(_) => continue, + WindowHeight::Preset(_) => unreachable!(), }; let factor = weight / total_weight; @@ -3456,6 +3509,10 @@ impl Column { ); found_fixed = true; } + + if let WindowHeight::Preset(idx) = data.height { + assert!(self.options.preset_window_heights.len() > idx); + } } } @@ -3467,11 +3524,11 @@ impl Column { }; let idx = match width { - ColumnWidth::Preset(idx) => (idx + 1) % self.options.preset_widths.len(), + ColumnWidth::Preset(idx) => (idx + 1) % self.options.preset_column_widths.len(), _ => { let current = self.width(); self.options - .preset_widths + .preset_column_widths .iter() .position(|prop| { let resolved = prop.resolve(&self.options, self.working_area.size.w); @@ -3500,7 +3557,7 @@ impl Column { let current_px = width.resolve(&self.options, self.working_area.size.w); let current = match width { - ColumnWidth::Preset(idx) => self.options.preset_widths[idx], + ColumnWidth::Preset(idx) => self.options.preset_column_widths[idx], current => current, }; @@ -3551,17 +3608,18 @@ impl Column { let tile_idx = tile_idx.unwrap_or(self.active_tile_idx); // Start by converting all heights to automatic, since only one window in the column can be - // fixed-height. If the current tile is already fixed, however, we can skip that step. - // Which is not only for optimization, but also preserves automatic weights in case one - // window is resized in such a way that other windows hit their min size, and then back. - if !matches!(self.data[tile_idx].height, WindowHeight::Fixed(_)) { + // non-auto-height. If the current tile is already non-auto, however, we can skip that + // step. Which is not only for optimization, but also preserves automatic weights in case + // one window is resized in such a way that other windows hit their min size, and then + // back. + if matches!(self.data[tile_idx].height, WindowHeight::Auto { .. }) { self.convert_heights_to_auto(); } let current = self.data[tile_idx].height; let tile = &self.tiles[tile_idx]; let current_window_px = match current { - WindowHeight::Auto { .. } => tile.window_size().h, + WindowHeight::Auto { .. } | WindowHeight::Preset(_) => tile.window_size().h, WindowHeight::Fixed(height) => height, }; let current_tile_px = tile.tile_height_for_window_height(current_window_px); @@ -3631,6 +3689,48 @@ impl Column { self.update_tile_sizes(animate); } + fn toggle_window_height(&mut self, tile_idx: Option, animate: bool) { + let tile_idx = tile_idx.unwrap_or(self.active_tile_idx); + + // Start by converting all heights to automatic, since only one window in the column can be + // non-auto-height. If the current tile is already non-auto, however, we can skip that + // step. Which is not only for optimization, but also preserves automatic weights in case + // one window is resized in such a way that other windows hit their min size, and then + // back. + if matches!(self.data[tile_idx].height, WindowHeight::Auto { .. }) { + self.convert_heights_to_auto(); + } + + let preset_idx = match self.data[tile_idx].height { + WindowHeight::Preset(idx) => (idx + 1) % self.options.preset_window_heights.len(), + _ => { + let current = self.data[tile_idx].size.h; + let tile = &self.tiles[tile_idx]; + self.options + .preset_window_heights + .iter() + .copied() + .position(|preset| { + let resolved = + resolve_preset_size(preset, &self.options, self.working_area.size.h); + let window_height = match resolved { + ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h), + ResolvedSize::Window(h) => h, + }; + let resolved = tile.tile_height_for_window_height( + window_height.round().clamp(1., 100000.), + ); + + // Some allowance for fractional scaling purposes. + current + 1. < resolved + }) + .unwrap_or(0) + } + }; + self.data[tile_idx].height = WindowHeight::Preset(preset_idx); + self.update_tile_sizes(animate); + } + /// Converts all heights in the column to automatic, preserving the apparent heights. /// /// All weights are recomputed to preserve the current tile heights while "centering" the -- cgit