diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index dedc656f..cf55ecf1 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -392,9 +392,11 @@ pub struct Layout { #[knuffel(child, default)] pub border: Border, #[knuffel(child, unwrap(children), default)] - pub preset_column_widths: Vec, + pub preset_column_widths: Vec, #[knuffel(child)] - pub default_column_width: Option, + pub default_column_width: Option, + #[knuffel(child, unwrap(children), default)] + pub preset_window_heights: Vec, #[knuffel(child, unwrap(argument), default)] pub center_focused_column: CenterFocusedColumn, #[knuffel(child)] @@ -416,6 +418,7 @@ impl Default for Layout { always_center_single_column: false, gaps: FloatOrInt(16.), struts: Default::default(), + preset_window_heights: Default::default(), } } } @@ -624,13 +627,13 @@ impl Default for Cursor { } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] -pub enum PresetWidth { +pub enum PresetSize { Proportion(#[knuffel(argument)] f64), Fixed(#[knuffel(argument)] i32), } #[derive(Debug, Clone, PartialEq)] -pub struct DefaultColumnWidth(pub Option); +pub struct DefaultPresetSize(pub Option); #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct Struts { @@ -898,7 +901,7 @@ pub struct WindowRule { // Rules applied at initial configure. #[knuffel(child)] - pub default_column_width: Option, + pub default_column_width: Option, #[knuffel(child, unwrap(argument))] pub open_on_output: Option, #[knuffel(child, unwrap(argument))] @@ -1155,6 +1158,9 @@ pub enum Action { #[knuffel(skip)] ResetWindowHeightById(u64), SwitchPresetColumnWidth, + SwitchPresetWindowHeight, + #[knuffel(skip)] + SwitchPresetWindowHeightById(u64), MaximizeColumn, SetColumnWidth(#[knuffel(argument, str)] SizeChange), SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget), @@ -1266,6 +1272,12 @@ impl From for Action { niri_ipc::Action::ResetWindowHeight { id: None } => Self::ResetWindowHeight, niri_ipc::Action::ResetWindowHeight { id: Some(id) } => Self::ResetWindowHeightById(id), niri_ipc::Action::SwitchPresetColumnWidth {} => Self::SwitchPresetColumnWidth, + niri_ipc::Action::SwitchPresetWindowHeight { id: None } => { + Self::SwitchPresetWindowHeight + } + niri_ipc::Action::SwitchPresetWindowHeight { id: Some(id) } => { + Self::SwitchPresetWindowHeightById(id) + } niri_ipc::Action::MaximizeColumn {} => Self::MaximizeColumn, niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change), niri_ipc::Action::SwitchLayout { layout } => Self::SwitchLayout(layout), @@ -1882,7 +1894,7 @@ impl OutputName { } } -impl knuffel::Decode for DefaultColumnWidth +impl knuffel::Decode for DefaultPresetSize where S: knuffel::traits::ErrorSpan, { @@ -1902,7 +1914,7 @@ where "expected no more than one child", )); } - PresetWidth::decode_node(child, ctx).map(Some).map(Self) + PresetSize::decode_node(child, ctx).map(Some).map(Self) } else { Ok(Self(None)) } @@ -2914,6 +2926,13 @@ mod tests { fixed 1280 } + preset-window-heights { + proportion 0.25 + proportion 0.5 + fixed 960 + fixed 1280 + } + default-column-width { proportion 0.25; } gaps 8 @@ -3104,14 +3123,20 @@ mod tests { inactive_gradient: None, }, preset_column_widths: vec![ - PresetWidth::Proportion(0.25), - PresetWidth::Proportion(0.5), - PresetWidth::Fixed(960), - PresetWidth::Fixed(1280), + PresetSize::Proportion(0.25), + PresetSize::Proportion(0.5), + PresetSize::Fixed(960), + PresetSize::Fixed(1280), ], - default_column_width: Some(DefaultColumnWidth(Some(PresetWidth::Proportion( + default_column_width: Some(DefaultPresetSize(Some(PresetSize::Proportion( 0.25, )))), + preset_window_heights: vec![ + PresetSize::Proportion(0.25), + PresetSize::Proportion(0.5), + PresetSize::Fixed(960), + PresetSize::Fixed(1280), + ], gaps: FloatOrInt(8.), struts: Struts { left: FloatOrInt(1.), diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index ca72db1a..13365522 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -350,6 +350,14 @@ pub enum Action { }, /// Switch between preset column widths. SwitchPresetColumnWidth {}, + /// Switch between preset window heights. + SwitchPresetWindowHeight { + /// Id of the window whose height to switch. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, /// Toggle the maximized state of the focused column. MaximizeColumn {}, /// Change the width of the focused column. diff --git a/resources/default-config.kdl b/resources/default-config.kdl index ed2d4f0e..d514cc48 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -113,6 +113,9 @@ layout { // fixed 1920 } + // You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between. + // preset-window-heights { } + // You can change the default width of the new windows. default-column-width { proportion 0.5; } // If you leave the brackets empty, the windows themselves will decide their initial width. @@ -427,7 +430,8 @@ binds { // Mod+BracketRight { consume-or-expel-window-right; } Mod+R { switch-preset-column-width; } - Mod+Shift+R { reset-window-height; } + Mod+Shift+R { switch-preset-window-height; } + Mod+Ctrl+R { reset-window-height; } Mod+F { maximize-column; } Mod+Shift+F { fullscreen-window; } Mod+C { center-column; } diff --git a/src/input/mod.rs b/src/input/mod.rs index e5820495..44832911 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1035,6 +1035,16 @@ impl State { Action::SwitchPresetColumnWidth => { self.niri.layout.toggle_width(); } + Action::SwitchPresetWindowHeight => { + self.niri.layout.toggle_window_height(None); + } + Action::SwitchPresetWindowHeightById(id) => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + self.niri.layout.toggle_window_height(Some(&window)); + } + } Action::CenterColumn => { self.niri.layout.center_column(); // FIXME: granular diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 35f4a2c7..5ab81f03 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,23 @@ impl Layout { monitor.toggle_width(); } + pub fn toggle_window_height(&mut self, window: Option<&W::Id>) { + let workspace = if let Some(window) = window { + Some( + self.workspaces_mut() + .find(|ws| ws.has_window(window)) + .unwrap(), + ) + } else { + self.active_workspace_mut() + }; + + let Some(workspace) = workspace else { + return; + }; + workspace.toggle_window_height(window); + } + pub fn toggle_full_width(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -3027,6 +3058,10 @@ mod tests { }, MoveColumnToOutput(#[proptest(strategy = "1..=5u8")] u8), SwitchPresetColumnWidth, + SwitchPresetWindowHeight { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + id: Option, + }, MaximizeColumn, SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), SetWindowHeight { @@ -3453,6 +3488,10 @@ mod tests { Op::MoveWorkspaceDown => layout.move_workspace_down(), Op::MoveWorkspaceUp => layout.move_workspace_up(), Op::SwitchPresetColumnWidth => layout.toggle_width(), + Op::SwitchPresetWindowHeight { id } => { + let id = id.filter(|id| layout.has_window(id)); + layout.toggle_window_height(id.as_ref()); + } Op::MaximizeColumn => layout.toggle_full_width(), Op::SetColumnWidth(change) => layout.set_column_width(change), Op::SetWindowHeight { id, change } => { @@ -4415,6 +4454,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 { id: None }, + Op::SwitchPresetWindowHeight { id: None }, + ]; + 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/workspace.rs b/src/layout/workspace.rs index 1ec54b1a..f6a659f8 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,30 @@ impl Workspace { cancel_resize_for_column(&mut self.interactive_resize, col); } + pub fn toggle_window_height(&mut self, window: Option<&W::Id>) { + if self.columns.is_empty() { + return; + } + + let (col, tile_idx) = if let Some(window) = window { + self.columns + .iter_mut() + .find_map(|col| { + col.tiles + .iter() + .position(|tile| tile.window().id() == window) + .map(|tile_idx| (col, Some(tile_idx))) + }) + .unwrap() + } else { + (&mut self.columns[self.active_column_idx], None) + }; + + col.toggle_window_height(tile_idx, 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 +3062,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 +3272,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 +3281,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 +3347,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 +3386,7 @@ impl Column { let weight = match *h { WindowHeight::Auto { weight } => weight, WindowHeight::Fixed(_) => continue, + WindowHeight::Preset(_) => unreachable!(), }; let factor = weight / total_weight; @@ -3456,6 +3522,10 @@ impl Column { ); found_fixed = true; } + + if let WindowHeight::Preset(idx) = data.height { + assert!(self.options.preset_window_heights.len() > idx); + } } } @@ -3467,11 +3537,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 +3570,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 +3621,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 +3702,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 diff --git a/wiki/Configuration:-Layout.md b/wiki/Configuration:-Layout.md index 1b5c4ef9..47be65f8 100644 --- a/wiki/Configuration:-Layout.md +++ b/wiki/Configuration:-Layout.md @@ -17,6 +17,12 @@ layout { default-column-width { proportion 0.5; } + preset-window-heights { + proportion 0.33333 + proportion 0.5 + proportion 0.66667 + } + focus-ring { // off width 4 @@ -134,6 +140,29 @@ layout { > > Either way, `default-column-width {}` is most useful for specific windows, in form of a [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules) with the same syntax. +### `preset-window-heights` + +Since: 0.1.9 + +Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between. + +`proportion` sets the height as a fraction of the output height, taking gaps into account. +The default preset heights are 13, 12 and 23 of the output. + +`fixed` sets the height in logical pixels exactly. + +```kdl +layout { + // Cycle between 1/3, 1/2, 2/3 of the output, and a fixed 720 logical pixels. + preset-window-heights { + proportion 0.33333 + proportion 0.5 + proportion 0.66667 + fixed 720 + } +} +``` + ### `focus-ring` and `border` Focus ring and border are drawn around windows and indicate the active window.