From 1fa9dd32ed028c88248644a13421c098fef72894 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sat, 20 Sep 2025 12:57:41 +0300 Subject: config: Split Layout from LayoutPart --- niri-config/src/appearance.rs | 214 ++++++------ niri-config/src/layout.rs | 127 +++++-- niri-config/src/lib.rs | 480 ++++++++++++++------------- niri-config/src/macros.rs | 10 + niri-config/src/utils.rs | 15 + niri-visual-tests/src/cases/gradient_area.rs | 4 +- niri-visual-tests/src/cases/layout.rs | 4 +- niri-visual-tests/src/cases/tile.rs | 4 +- src/layer/mapped.rs | 4 +- src/layout/floating.rs | 4 +- src/layout/focus_ring.rs | 4 +- src/layout/insert_hint_element.rs | 6 +- src/layout/mod.rs | 24 +- src/layout/scrolling.rs | 8 +- src/layout/shadow.rs | 4 +- src/layout/tab_indicator.rs | 16 +- src/layout/tests.rs | 33 +- src/layout/tests/fullscreen.rs | 2 +- src/layout/workspace.rs | 4 +- src/niri.rs | 4 +- src/tests/animations.rs | 2 +- 21 files changed, 559 insertions(+), 414 deletions(-) diff --git a/niri-config/src/appearance.rs b/niri-config/src/appearance.rs index dfe4ebbb..bdcbb318 100644 --- a/niri-config/src/appearance.rs +++ b/niri-config/src/appearance.rs @@ -5,7 +5,7 @@ use knuffel::errors::DecodeError; use miette::{miette, IntoDiagnostic as _}; use smithay::backend::renderer::Color32F; -use crate::utils::MergeWith; +use crate::utils::{Flag, MergeWith}; use crate::FloatOrInt; pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]); @@ -222,23 +222,15 @@ impl CornerRadius { } } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct FocusRing { - #[knuffel(child)] pub off: bool, - #[knuffel(child, unwrap(argument), default = Self::default().width)] - pub width: FloatOrInt<0, 65535>, - #[knuffel(child, default = Self::default().active_color)] + pub width: f64, pub active_color: Color, - #[knuffel(child, default = Self::default().inactive_color)] pub inactive_color: Color, - #[knuffel(child, default = Self::default().urgent_color)] pub urgent_color: Color, - #[knuffel(child)] pub active_gradient: Option, - #[knuffel(child)] pub inactive_gradient: Option, - #[knuffel(child)] pub urgent_gradient: Option, } @@ -246,7 +238,7 @@ impl Default for FocusRing { fn default() -> Self { Self { off: false, - width: FloatOrInt(4.), + width: 4., active_color: Color::from_rgba8_unpremul(127, 200, 255, 255), inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255), urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255), @@ -257,23 +249,15 @@ impl Default for FocusRing { } } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Border { - #[knuffel(child)] pub off: bool, - #[knuffel(child, unwrap(argument), default = Self::default().width)] - pub width: FloatOrInt<0, 65535>, - #[knuffel(child, default = Self::default().active_color)] + pub width: f64, pub active_color: Color, - #[knuffel(child, default = Self::default().inactive_color)] pub inactive_color: Color, - #[knuffel(child, default = Self::default().urgent_color)] pub urgent_color: Color, - #[knuffel(child)] pub active_gradient: Option, - #[knuffel(child)] pub inactive_gradient: Option, - #[knuffel(child)] pub urgent_gradient: Option, } @@ -281,7 +265,7 @@ impl Default for Border { fn default() -> Self { Self { off: true, - width: FloatOrInt(4.), + width: 4., active_color: Color::from_rgba8_unpremul(255, 200, 127, 255), inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255), urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255), @@ -329,7 +313,7 @@ impl MergeWith for Border { self.off = false; } - merge_clone!((self, part), width); + merge!((self, part), width); merge_color_gradient!( (self, part), @@ -348,21 +332,14 @@ impl MergeWith for FocusRing { } } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Shadow { - #[knuffel(child)] pub on: bool, - #[knuffel(child, default = Self::default().offset)] pub offset: ShadowOffset, - #[knuffel(child, unwrap(argument), default = Self::default().softness)] - pub softness: FloatOrInt<0, 1024>, - #[knuffel(child, unwrap(argument), default = Self::default().spread)] - pub spread: FloatOrInt<-1024, 1024>, - #[knuffel(child, unwrap(argument), default = Self::default().draw_behind_window)] + pub softness: f64, + pub spread: f64, pub draw_behind_window: bool, - #[knuffel(child, default = Self::default().color)] pub color: Color, - #[knuffel(child)] pub inactive_color: Option, } @@ -374,8 +351,8 @@ impl Default for Shadow { x: FloatOrInt(0.), y: FloatOrInt(5.), }, - softness: FloatOrInt(30.), - spread: FloatOrInt(5.), + softness: 30., + spread: 5., draw_behind_window: false, color: Color::from_rgba8_unpremul(0, 0, 0, 0x77), inactive_color: None, @@ -390,7 +367,7 @@ impl MergeWith for Shadow { self.on = false; } - merge_clone!((self, part), softness, spread); + merge!((self, part), softness, spread); merge_clone!((self, part), offset, draw_behind_window, color); @@ -440,8 +417,8 @@ impl From for Shadow { Self { on: !value.off, offset: value.offset, - softness: value.softness, - spread: value.spread, + softness: value.softness.0, + spread: value.spread.0, draw_behind_window: false, color: value.color, inactive_color: None, @@ -449,37 +426,22 @@ impl From for Shadow { } } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct TabIndicator { - #[knuffel(child)] pub off: bool, - #[knuffel(child)] pub hide_when_single_tab: bool, - #[knuffel(child)] pub place_within_column: bool, - #[knuffel(child, unwrap(argument), default = Self::default().gap)] - pub gap: FloatOrInt<-65535, 65535>, - #[knuffel(child, unwrap(argument), default = Self::default().width)] - pub width: FloatOrInt<0, 65535>, - #[knuffel(child, default = Self::default().length)] + pub gap: f64, + pub width: f64, pub length: TabIndicatorLength, - #[knuffel(child, unwrap(argument), default = Self::default().position)] pub position: TabIndicatorPosition, - #[knuffel(child, unwrap(argument), default = Self::default().gaps_between_tabs)] - pub gaps_between_tabs: FloatOrInt<0, 65535>, - #[knuffel(child, unwrap(argument), default = Self::default().corner_radius)] - pub corner_radius: FloatOrInt<0, 65535>, - #[knuffel(child)] + pub gaps_between_tabs: f64, + pub corner_radius: f64, pub active_color: Option, - #[knuffel(child)] pub inactive_color: Option, - #[knuffel(child)] pub urgent_color: Option, - #[knuffel(child)] pub active_gradient: Option, - #[knuffel(child)] pub inactive_gradient: Option, - #[knuffel(child)] pub urgent_gradient: Option, } @@ -489,14 +451,14 @@ impl Default for TabIndicator { off: false, hide_when_single_tab: false, place_within_column: false, - gap: FloatOrInt(5.), - width: FloatOrInt(4.), + gap: 5., + width: 4., length: TabIndicatorLength { total_proportion: Some(0.5), }, position: TabIndicatorPosition::Left, - gaps_between_tabs: FloatOrInt(0.), - corner_radius: FloatOrInt(0.), + gaps_between_tabs: 0., + corner_radius: 0., active_color: None, inactive_color: None, urgent_color: None, @@ -507,6 +469,70 @@ impl Default for TabIndicator { } } +impl MergeWith for TabIndicator { + fn merge_with(&mut self, part: &TabIndicatorPart) { + self.off |= part.off; + if part.on { + self.off = false; + } + + merge!( + (self, part), + hide_when_single_tab, + place_within_column, + gap, + width, + gaps_between_tabs, + corner_radius, + ); + + merge_clone!((self, part), length, position); + + merge_color_gradient_opt!( + (self, part), + (active_color, active_gradient), + (inactive_color, inactive_gradient), + (urgent_color, urgent_gradient), + ); + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct TabIndicatorPart { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child)] + pub hide_when_single_tab: Option, + #[knuffel(child)] + pub place_within_column: Option, + #[knuffel(child, unwrap(argument))] + pub gap: Option>, + #[knuffel(child, unwrap(argument))] + pub width: Option>, + #[knuffel(child)] + pub length: Option, + #[knuffel(child, unwrap(argument))] + pub position: Option, + #[knuffel(child, unwrap(argument))] + pub gaps_between_tabs: Option>, + #[knuffel(child, unwrap(argument))] + pub corner_radius: Option>, + #[knuffel(child)] + pub active_color: Option, + #[knuffel(child)] + pub inactive_color: Option, + #[knuffel(child)] + pub urgent_color: Option, + #[knuffel(child)] + pub active_gradient: Option, + #[knuffel(child)] + pub inactive_gradient: Option, + #[knuffel(child)] + pub urgent_gradient: Option, +} + #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct TabIndicatorLength { #[knuffel(property)] @@ -521,13 +547,10 @@ pub enum TabIndicatorPosition { Bottom, } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct InsertHint { - #[knuffel(child)] pub off: bool, - #[knuffel(child, default = Self::default().color)] pub color: Color, - #[knuffel(child)] pub gradient: Option, } @@ -541,6 +564,29 @@ impl Default for InsertHint { } } +impl MergeWith for InsertHint { + fn merge_with(&mut self, part: &InsertHintPart) { + self.off |= part.off; + if part.on { + self.off = false; + } + + merge_color_gradient!((self, part), (color, gradient)); + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct InsertHintPart { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child)] + pub color: Option, + #[knuffel(child)] + pub gradient: Option, +} + #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)] pub enum BlockOutFrom { Screencast, @@ -1016,16 +1062,9 @@ mod tests { #[test] fn test_border_rule_on_off_merging() { fn is_on(config: &str, rules: &[&str]) -> String { - let mut resolved = BorderRule { - off: false, - on: false, - width: None, - active_color: None, - inactive_color: None, - urgent_color: None, - active_gradient: None, - inactive_gradient: None, - urgent_gradient: None, + let mut resolved = Border { + off: config == "off", + ..Default::default() }; for rule in rules.iter().copied() { @@ -1038,14 +1077,7 @@ mod tests { resolved.merge_with(&rule); } - let mut border = Border { - off: config == "off", - ..Default::default() - }; - - border.merge_with(&resolved); - - if border.off { "off" } else { "on" }.to_owned() + if resolved.off { "off" } else { "on" }.to_owned() } assert_snapshot!(is_on("off", &[]), @"off"); @@ -1095,13 +1127,11 @@ mod tests { ) .unwrap(); - let mut border_rule = BorderRule::default(); + let mut border = config.resolve_layout().border; for rule in &config.window_rules { - border_rule.merge_with(&rule.border); + border.merge_with(&rule.border); } - let border = config.layout.border.merged_with(&border_rule); - // Gradient should be None because it's overwritten. assert_debug_snapshot!( ( @@ -1166,15 +1196,13 @@ mod tests { ) .unwrap(); - let mut border_rule = BorderRule::default(); + let mut border = config.resolve_layout().border; let mut tab_indicator_rule = TabIndicatorRule::default(); for rule in &config.window_rules { - border_rule.merge_with(&rule.border); + border.merge_with(&rule.border); tab_indicator_rule.merge_with(&rule.tab_indicator); } - let border = config.layout.border.merged_with(&border_rule); - // Gradient should be None because it's overwritten. assert_debug_snapshot!( ( diff --git a/niri-config/src/layout.rs b/niri-config/src/layout.rs index 768199d5..f84528bd 100644 --- a/niri-config/src/layout.rs +++ b/niri-config/src/layout.rs @@ -4,65 +4,130 @@ use niri_ipc::{ColumnDisplay, SizeChange}; use crate::appearance::{ Border, FocusRing, InsertHint, Shadow, TabIndicator, DEFAULT_BACKGROUND_COLOR, }; -use crate::utils::expect_only_children; -use crate::{Color, FloatOrInt}; +use crate::utils::{expect_only_children, Flag, MergeWith}; +use crate::{BorderRule, Color, FloatOrInt, InsertHintPart, ShadowRule, TabIndicatorPart}; -#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Layout { - #[knuffel(child, default)] pub focus_ring: FocusRing, - #[knuffel(child, default)] pub border: Border, - #[knuffel(child, default)] pub shadow: Shadow, - #[knuffel(child, default)] pub tab_indicator: TabIndicator, - #[knuffel(child, default)] pub insert_hint: InsertHint, - #[knuffel(child, unwrap(children), default)] pub preset_column_widths: Vec, - #[knuffel(child)] - pub default_column_width: Option, - #[knuffel(child, unwrap(children), default)] + pub default_column_width: Option, pub preset_window_heights: Vec, - #[knuffel(child, unwrap(argument), default)] pub center_focused_column: CenterFocusedColumn, - #[knuffel(child)] pub always_center_single_column: bool, - #[knuffel(child)] pub empty_workspace_above_first: bool, - #[knuffel(child, unwrap(argument, str), default = Self::default().default_column_display)] pub default_column_display: ColumnDisplay, - #[knuffel(child, unwrap(argument), default = Self::default().gaps)] - pub gaps: FloatOrInt<0, 65535>, - #[knuffel(child, default)] + pub gaps: f64, pub struts: Struts, - #[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)] pub background_color: Color, } impl Default for Layout { fn default() -> Self { Self { - focus_ring: Default::default(), - border: Default::default(), - shadow: Default::default(), - tab_indicator: Default::default(), - insert_hint: Default::default(), - preset_column_widths: Default::default(), - default_column_width: Default::default(), - center_focused_column: Default::default(), + focus_ring: FocusRing::default(), + border: Border::default(), + shadow: Shadow::default(), + tab_indicator: TabIndicator::default(), + insert_hint: InsertHint::default(), + preset_column_widths: vec![ + PresetSize::Proportion(1. / 3.), + PresetSize::Proportion(0.5), + PresetSize::Proportion(2. / 3.), + ], + default_column_width: Some(PresetSize::Proportion(0.5)), + center_focused_column: CenterFocusedColumn::Never, always_center_single_column: false, empty_workspace_above_first: false, default_column_display: ColumnDisplay::Normal, - gaps: FloatOrInt(16.), - struts: Default::default(), - preset_window_heights: Default::default(), + gaps: 16., + struts: Struts::default(), + preset_window_heights: vec![ + PresetSize::Proportion(1. / 3.), + PresetSize::Proportion(0.5), + PresetSize::Proportion(2. / 3.), + ], background_color: DEFAULT_BACKGROUND_COLOR, } } } +impl MergeWith for Layout { + fn merge_with(&mut self, part: &LayoutPart) { + merge!( + (self, part), + focus_ring, + border, + shadow, + tab_indicator, + insert_hint, + always_center_single_column, + empty_workspace_above_first, + gaps, + ); + + merge_clone!( + (self, part), + preset_column_widths, + preset_window_heights, + center_focused_column, + default_column_display, + struts, + background_color, + ); + + if let Some(x) = part.default_column_width { + self.default_column_width = x.0; + } + + if self.preset_column_widths.is_empty() { + self.preset_column_widths = Layout::default().preset_column_widths; + } + + if self.preset_window_heights.is_empty() { + self.preset_window_heights = Layout::default().preset_window_heights; + } + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] +pub struct LayoutPart { + #[knuffel(child)] + pub focus_ring: Option, + #[knuffel(child)] + pub border: Option, + #[knuffel(child)] + pub shadow: Option, + #[knuffel(child)] + pub tab_indicator: Option, + #[knuffel(child)] + pub insert_hint: Option, + #[knuffel(child, unwrap(children))] + pub preset_column_widths: Option>, + #[knuffel(child)] + pub default_column_width: Option, + #[knuffel(child, unwrap(children))] + pub preset_window_heights: Option>, + #[knuffel(child, unwrap(argument))] + pub center_focused_column: Option, + #[knuffel(child)] + pub always_center_single_column: Option, + #[knuffel(child)] + pub empty_workspace_above_first: Option, + #[knuffel(child, unwrap(argument, str))] + pub default_column_display: Option, + #[knuffel(child, unwrap(argument))] + pub gaps: Option>, + #[knuffel(child)] + pub struts: Option, + #[knuffel(child)] + pub background_color: Option, +} + #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub enum PresetSize { Proportion(#[knuffel(argument)] f64), diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 575be90b..6dd32620 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -36,6 +36,7 @@ pub use crate::layout::*; pub use crate::misc::*; pub use crate::output::{Output, OutputName, Outputs, Position, Vrr}; pub use crate::utils::FloatOrInt; +use crate::utils::MergeWith as _; pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule}; pub use crate::workspace::Workspace; @@ -50,7 +51,7 @@ pub struct Config { #[knuffel(children(name = "spawn-sh-at-startup"))] pub spawn_sh_at_startup: Vec, #[knuffel(child, default)] - pub layout: Layout, + pub layout: LayoutPart, #[knuffel(child, default)] pub prefer_no_csd: bool, #[knuffel(child, default)] @@ -133,6 +134,35 @@ impl Config { let _span = tracy_client::span!("Config::parse"); knuffel::parse(filename, text) } + + pub fn resolve_layout(&self) -> Layout { + let mut rv = Layout::from_part(&self.layout); + + // Preserve the behavior we'd always had for the border section: + // - `layout {}` gives border = off + // - `layout { border {} }` gives border = on + // - `layout { border { off } }` gives border = off + // + // This behavior is inconsistent with the rest of the config where adding an empty section + // generally doesn't change the outcome. Particularly, shadows are also disabled by default + // (like borders), and they always had an `on` instead of an `off` for this reason, so that + // writing `layout { shadow {} }` still results in shadow = off, as it should. + // + // Unfortunately, the default config has always had wording that heavily implies that + // `layout { border {} }` enables the borders. This wording is sure to be present in a lot + // of users' configs by now, which we can't change. + // + // Another way to make things consistent would be to default borders to on. However, that + // is annoying because it would mean changing many tests that rely on borders being off by + // default. This would also contradict the intended default borders value (off). + // + // So, let's just work around the problem here, preserving the original behavior. + if self.layout.border.is_some_and(|x| !x.on && !x.off) { + rv.border.off = false; + } + + rv + } } impl Default for Config { @@ -778,181 +808,182 @@ mod tests { command: "qs -c ~/source/qs/MyAwesomeShell", }, ], - layout: Layout { - focus_ring: FocusRing { - off: false, - width: FloatOrInt( - 5.0, - ), - active_color: Color { - r: 0.0, - g: 0.39215687, - b: 0.78431374, - a: 1.0, - }, - inactive_color: Color { - r: 1.0, - g: 0.78431374, - b: 0.39215687, - a: 0.0, - }, - urgent_color: Color { - r: 0.60784316, - g: 0.0, - b: 0.0, - a: 1.0, - }, - active_gradient: Some( - Gradient { - from: Color { - r: 0.039215688, - g: 0.078431375, - b: 0.11764706, - a: 1.0, - }, - to: Color { + layout: LayoutPart { + focus_ring: Some( + BorderRule { + off: false, + on: false, + width: Some( + FloatOrInt( + 5.0, + ), + ), + active_color: Some( + Color { r: 0.0, - g: 0.5019608, - b: 1.0, + g: 0.39215687, + b: 0.78431374, a: 1.0, }, - angle: 180, - relative_to: WorkspaceView, - in_: GradientInterpolation { - color_space: Srgb, - hue_interpolation: Shorter, + ), + inactive_color: Some( + Color { + r: 1.0, + g: 0.78431374, + b: 0.39215687, + a: 0.0, }, - }, - ), - inactive_gradient: None, - urgent_gradient: None, - }, - border: Border { - off: false, - width: FloatOrInt( - 3.0, - ), - active_color: Color { - r: 1.0, - g: 0.78431374, - b: 0.49803922, - a: 1.0, - }, - inactive_color: Color { - r: 1.0, - g: 0.78431374, - b: 0.39215687, - a: 0.0, - }, - urgent_color: Color { - r: 0.60784316, - g: 0.0, - b: 0.0, - a: 1.0, - }, - active_gradient: None, - inactive_gradient: None, - urgent_gradient: None, - }, - shadow: Shadow { - on: false, - offset: ShadowOffset { - x: FloatOrInt( - 10.0, ), - y: FloatOrInt( - -20.0, + urgent_color: None, + active_gradient: Some( + Gradient { + from: Color { + r: 0.039215688, + g: 0.078431375, + b: 0.11764706, + a: 1.0, + }, + to: Color { + r: 0.0, + g: 0.5019608, + b: 1.0, + a: 1.0, + }, + angle: 180, + relative_to: WorkspaceView, + in_: GradientInterpolation { + color_space: Srgb, + hue_interpolation: Shorter, + }, + }, ), + inactive_gradient: None, + urgent_gradient: None, }, - softness: FloatOrInt( - 30.0, - ), - spread: FloatOrInt( - 5.0, - ), - draw_behind_window: false, - color: Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.46666667, + ), + border: Some( + BorderRule { + off: false, + on: false, + width: Some( + FloatOrInt( + 3.0, + ), + ), + active_color: None, + inactive_color: Some( + Color { + r: 1.0, + g: 0.78431374, + b: 0.39215687, + a: 0.0, + }, + ), + urgent_color: None, + active_gradient: None, + inactive_gradient: None, + urgent_gradient: None, }, - inactive_color: None, - }, - tab_indicator: TabIndicator { - off: false, - hide_when_single_tab: false, - place_within_column: false, - gap: FloatOrInt( - 5.0, - ), - width: FloatOrInt( - 10.0, - ), - length: TabIndicatorLength { - total_proportion: Some( - 0.5, + ), + shadow: Some( + ShadowRule { + off: false, + on: false, + offset: Some( + ShadowOffset { + x: FloatOrInt( + 10.0, + ), + y: FloatOrInt( + -20.0, + ), + }, ), + softness: None, + spread: None, + draw_behind_window: None, + color: None, + inactive_color: None, }, - position: Top, - gaps_between_tabs: FloatOrInt( - 0.0, - ), - corner_radius: FloatOrInt( - 0.0, - ), - active_color: None, - inactive_color: None, - urgent_color: None, - active_gradient: None, - inactive_gradient: None, - urgent_gradient: None, - }, - insert_hint: InsertHint { - off: false, - color: Color { - r: 1.0, - g: 0.78431374, - b: 0.49803922, - a: 1.0, + ), + tab_indicator: Some( + TabIndicatorPart { + off: false, + on: false, + hide_when_single_tab: None, + place_within_column: None, + gap: None, + width: Some( + FloatOrInt( + 10.0, + ), + ), + length: None, + position: Some( + Top, + ), + gaps_between_tabs: None, + corner_radius: None, + active_color: None, + inactive_color: None, + urgent_color: None, + active_gradient: None, + inactive_gradient: None, + urgent_gradient: None, }, - gradient: Some( - Gradient { - from: Color { - r: 0.039215688, - g: 0.078431375, - b: 0.11764706, - a: 1.0, - }, - to: Color { - r: 0.0, - g: 0.5019608, - b: 1.0, + ), + insert_hint: Some( + InsertHintPart { + off: false, + on: false, + color: Some( + Color { + r: 1.0, + g: 0.78431374, + b: 0.49803922, a: 1.0, }, - angle: 180, - relative_to: WorkspaceView, - in_: GradientInterpolation { - color_space: Srgb, - hue_interpolation: Shorter, + ), + gradient: Some( + Gradient { + from: Color { + r: 0.039215688, + g: 0.078431375, + b: 0.11764706, + a: 1.0, + }, + to: Color { + r: 0.0, + g: 0.5019608, + b: 1.0, + a: 1.0, + }, + angle: 180, + relative_to: WorkspaceView, + in_: GradientInterpolation { + color_space: Srgb, + hue_interpolation: Shorter, + }, }, - }, - ), - }, - preset_column_widths: [ - Proportion( - 0.25, - ), - Proportion( - 0.5, - ), - Fixed( - 960, - ), - Fixed( - 1280, - ), - ], + ), + }, + ), + preset_column_widths: Some( + [ + Proportion( + 0.25, + ), + Proportion( + 0.5, + ), + Fixed( + 960, + ), + Fixed( + 1280, + ), + ], + ), default_column_width: Some( DefaultPresetSize( Some( @@ -962,47 +993,52 @@ mod tests { ), ), ), - preset_window_heights: [ - Proportion( - 0.25, - ), - Proportion( - 0.5, - ), - Fixed( - 960, - ), - Fixed( - 1280, - ), - ], - center_focused_column: OnOverflow, - always_center_single_column: false, - empty_workspace_above_first: false, - default_column_display: Tabbed, - gaps: FloatOrInt( - 8.0, + preset_window_heights: Some( + [ + Proportion( + 0.25, + ), + Proportion( + 0.5, + ), + Fixed( + 960, + ), + Fixed( + 1280, + ), + ], ), - struts: Struts { - left: FloatOrInt( - 1.0, - ), - right: FloatOrInt( - 2.0, - ), - top: FloatOrInt( - 3.0, - ), - bottom: FloatOrInt( - 0.0, + center_focused_column: Some( + OnOverflow, + ), + always_center_single_column: None, + empty_workspace_above_first: None, + default_column_display: Some( + Tabbed, + ), + gaps: Some( + FloatOrInt( + 8.0, ), - }, - background_color: Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }, + ), + struts: Some( + Struts { + left: FloatOrInt( + 1.0, + ), + right: FloatOrInt( + 2.0, + ), + top: FloatOrInt( + 3.0, + ), + bottom: FloatOrInt( + 0.0, + ), + }, + ), + background_color: None, }, prefer_no_csd: true, cursor: Cursor { @@ -1823,6 +1859,23 @@ mod tests { default_config.window_rules.clear(); default_config.binds.0.clear(); + let default_layout = default_config.resolve_layout(); + let empty_layout = empty_config.resolve_layout(); + default_config.layout = Default::default(); + assert_snapshot!( + diff_lines( + &format!("{empty_layout:#?}"), + &format!("{default_layout:#?}") + ), + @r" + - 0.3333333333333333, + + 0.33333, + + - 0.6666666666666666, + + 0.66667, + ", + ); + assert_snapshot!( diff_lines( &format!("{empty_config:#?}"), @@ -1846,30 +1899,7 @@ mod tests { + ], + }, + ], - - - preset_column_widths: [], - - default_column_width: None, - + preset_column_widths: [ - + Proportion( - + 0.33333, - + ), - + Proportion( - + 0.5, - + ), - + Proportion( - + 0.66667, - + ), - + ], - + default_column_width: Some( - + DefaultPresetSize( - + Some( - + Proportion( - + 0.5, - + ), - + ), - + ), - + ), - "# + "#, ); } } diff --git a/niri-config/src/macros.rs b/niri-config/src/macros.rs index dccb5db5..164b8dd8 100644 --- a/niri-config/src/macros.rs +++ b/niri-config/src/macros.rs @@ -1,3 +1,13 @@ +macro_rules! merge { + (($self:expr, $part:expr), $($field:ident),+ $(,)*) => { + $( + if let Some(x) = &$part.$field { + $self.$field.merge_with(x); + } + )+ + }; +} + macro_rules! merge_clone { (($self:expr, $part:expr), $($field:ident),+ $(,)*) => { $( diff --git a/niri-config/src/utils.rs b/niri-config/src/utils.rs index e41d7b90..f0318882 100644 --- a/niri-config/src/utils.rs +++ b/niri-config/src/utils.rs @@ -14,6 +14,15 @@ pub struct Percent(pub f64); #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct FloatOrInt(pub f64); +/// Flag, with an optional explicit value. +/// +/// Intended to be used as an `Option` field, as a tri-state: +/// - (missing): unset, `None` +/// - just `field`: set, `Some(true)` +/// - explicitly `field true` or `field false`: set, `Some(true)` or `Some(false)` +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Flag(#[knuffel(argument, default = true)] pub bool); + /// `Regex` that implements `PartialEq` by its string form. #[derive(Debug, Clone)] pub struct RegexEq(pub Regex); @@ -57,6 +66,12 @@ impl MergeWith> for f64 { } } +impl MergeWith for bool { + fn merge_with(&mut self, part: &Flag) { + *self = part.0; + } +} + impl knuffel::DecodeScalar for FloatOrInt { diff --git a/niri-visual-tests/src/cases/gradient_area.rs b/niri-visual-tests/src/cases/gradient_area.rs index 03f206c3..285b5316 100644 --- a/niri-visual-tests/src/cases/gradient_area.rs +++ b/niri-visual-tests/src/cases/gradient_area.rs @@ -3,7 +3,7 @@ use std::time::Duration; use niri::layout::focus_ring::FocusRing; use niri::render_helpers::border::BorderRenderElement; -use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation}; +use niri_config::{Color, CornerRadius, GradientInterpolation}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Physical, Point, Rectangle, Size}; @@ -20,7 +20,7 @@ impl GradientArea { pub fn new(_args: Args) -> Self { let border = FocusRing::new(niri_config::FocusRing { off: false, - width: FloatOrInt(1.), + width: 1., active_color: Color::from_rgba8_unpremul(255, 255, 255, 128), inactive_color: Color::default(), urgent_color: Color::default(), diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index a3c935bf..50a27ae7 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -4,7 +4,7 @@ use std::time::Duration; use niri::animation::Clock; use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options}; use niri::render_helpers::RenderTarget; -use niri_config::{Color, FloatOrInt, OutputName, PresetSize}; +use niri_config::{Color, OutputName, PresetSize}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::layer_map_for_output; @@ -58,7 +58,7 @@ impl Layout { }, border: niri_config::Border { off: false, - width: FloatOrInt(4.), + width: 4., active_color: Color::from_rgba8_unpremul(255, 163, 72, 255), inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255), urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255), diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index 8ab67654..1823fe99 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -3,7 +3,7 @@ use std::time::Duration; use niri::layout::Options; use niri::render_helpers::RenderTarget; -use niri_config::{Color, FloatOrInt}; +use niri_config::Color; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Physical, Point, Rectangle, Size}; @@ -64,7 +64,7 @@ impl Tile { }, border: niri_config::Border { off: false, - width: FloatOrInt(32.), + width: 32., active_color: Color::from_rgba8_unpremul(255, 163, 72, 255), ..Default::default() }, diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index 658a0c15..2e9f6ca8 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -59,7 +59,7 @@ impl MappedLayer { clock: Clock, config: &Config, ) -> Self { - let mut shadow_config = config.layout.shadow; + let mut shadow_config = config.resolve_layout().shadow; // Shadows for layer surfaces need to be explicitly enabled. shadow_config.on = false; shadow_config.merge_with(&rules.shadow); @@ -76,7 +76,7 @@ impl MappedLayer { } pub fn update_config(&mut self, config: &Config) { - let mut shadow_config = config.layout.shadow; + let mut shadow_config = config.resolve_layout().shadow; // Shadows for layer surfaces need to be explicitly enabled. shadow_config.on = false; shadow_config.merge_with(&self.rules.shadow); diff --git a/src/layout/floating.rs b/src/layout/floating.rs index 8854ca17..5f5767ad 100644 --- a/src/layout/floating.rs +++ b/src/layout/floating.rs @@ -1227,7 +1227,7 @@ impl FloatingSpace { let size = match resolve_preset_size(size, working_area_size) { ResolvedSize::Tile(mut size) => { if !border.off { - size -= border.width.0 * 2.; + size -= border.width * 2.; } size } @@ -1365,7 +1365,7 @@ fn compute_toplevel_bounds( ) -> Size { let mut border = 0.; if !border_config.off { - border = border_config.width.0 * 2.; + border = border_config.width * 2.; } Size::from(( diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs index 5cb797e0..89fd058b 100644 --- a/src/layout/focus_ring.rs +++ b/src/layout/focus_ring.rs @@ -65,7 +65,7 @@ impl FocusRing { scale: f64, alpha: f32, ) { - let width = self.config.width.0; + let width = self.config.width; self.full_size = win_size + Size::from((width, width)).upscale(2.); let color = if is_urgent { @@ -261,7 +261,7 @@ impl FocusRing { } pub fn width(&self) -> f64 { - self.config.width.0 + self.config.width } pub fn is_off(&self) -> bool { diff --git a/src/layout/insert_hint_element.rs b/src/layout/insert_hint_element.rs index 92f9b480..818aeb7e 100644 --- a/src/layout/insert_hint_element.rs +++ b/src/layout/insert_hint_element.rs @@ -1,4 +1,4 @@ -use niri_config::{CornerRadius, FloatOrInt}; +use niri_config::CornerRadius; use smithay::utils::{Logical, Point, Rectangle, Size}; use super::focus_ring::{FocusRing, FocusRingRenderElement}; @@ -16,7 +16,7 @@ impl InsertHintElement { Self { inner: FocusRing::new(niri_config::FocusRing { off: config.off, - width: FloatOrInt(0.), + width: 0., active_color: config.color, inactive_color: config.color, urgent_color: config.color, @@ -30,7 +30,7 @@ impl InsertHintElement { pub fn update_config(&mut self, config: niri_config::InsertHint) { self.inner.update_config(niri_config::FocusRing { off: config.off, - width: FloatOrInt(0.), + width: 0., active_color: config.color, inactive_color: config.color, urgent_color: config.color, diff --git a/src/layout/mod.rs b/src/layout/mod.rs index c7d8a6fd..40f1deed 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -40,8 +40,8 @@ use std::time::Duration; use monitor::{InsertHint, InsertPosition, InsertWorkspace, MonitorAddWindowTarget}; use niri_config::utils::MergeWith as _; use niri_config::{ - CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts, - Workspace as WorkspaceConfig, WorkspaceReference, + CenterFocusedColumn, Config, CornerRadius, PresetSize, Struts, Workspace as WorkspaceConfig, + WorkspaceReference, }; use niri_ipc::{ColumnDisplay, PositionChange, SizeChange, WindowLayout}; use scrolling::{Column, ColumnWidth}; @@ -620,7 +620,7 @@ impl HitType { impl Options { fn from_config(config: &Config) -> Self { - let layout = &config.layout; + let layout = config.resolve_layout(); let preset_column_widths = if layout.preset_column_widths.is_empty() { Options::default().preset_column_widths @@ -633,16 +633,8 @@ impl Options { layout.preset_window_heights.clone() }; - // Missing default_column_width maps to Some(PresetSize::Proportion(0.5)), - // while present, but empty, maps to None. - let default_column_width = layout - .default_column_width - .as_ref() - .map(|w| w.0) - .unwrap_or(Some(PresetSize::Proportion(0.5))); - Self { - gaps: layout.gaps.0, + gaps: layout.gaps, struts: layout.struts, focus_ring: layout.focus_ring, border: layout.border, @@ -654,7 +646,7 @@ impl Options { empty_workspace_above_first: layout.empty_workspace_above_first, default_column_display: layout.default_column_display, preset_column_widths, - default_column_width, + default_column_width: layout.default_column_width, animations: config.animations.clone(), gestures: config.gestures, overview: config.overview, @@ -669,8 +661,8 @@ impl Options { let round = |logical: f64| round_logical_in_physical_max1(scale, logical); self.gaps = round(self.gaps); - self.focus_ring.width = FloatOrInt(round(self.focus_ring.width.0)); - self.border.width = FloatOrInt(round(self.border.width.0)); + self.focus_ring.width = round(self.focus_ring.width); + self.border.width = round(self.border.width); self } @@ -5252,7 +5244,7 @@ impl Layout { let rules = window.rules(); let border = self.options.border.merged_with(&rules.border); if !border.off { - fixed += border.width.0 * 2.; + fixed += border.width * 2.; } ColumnWidth::Fixed(fixed) diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 5f53ccee..52869024 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -488,7 +488,7 @@ impl ScrollingSpace { let size = match resolve_preset_size(size, &self.options, working_size.w, extra.w) { ResolvedSize::Tile(mut size) => { if !border.off { - size -= border.width.0 * 2.; + size -= border.width * 2.; } size } @@ -502,14 +502,14 @@ impl ScrollingSpace { let mut full_height = self.working_area.size.h - self.options.gaps * 2.; if !border.off { - full_height -= border.width.0 * 2.; + full_height -= border.width * 2.; } let height = if let Some(height) = height { let height = match resolve_preset_size(height, &self.options, working_size.h, extra.h) { ResolvedSize::Tile(mut size) => { if !border.off { - size -= border.width.0 * 2.; + size -= border.width * 2.; } size } @@ -5280,7 +5280,7 @@ fn compute_toplevel_bounds( ) -> Size { let mut border = 0.; if !border_config.off { - border = border_config.width.0 * 2.; + border = border_config.width * 2.; } Size::from(( diff --git a/src/layout/shadow.rs b/src/layout/shadow.rs index df018b7b..25082e3e 100644 --- a/src/layout/shadow.rs +++ b/src/layout/shadow.rs @@ -48,7 +48,7 @@ impl Shadow { // * We do not divide anything, only add, subtract and multiply by integers. // * At rendering time, tile positions are rounded to physical pixels. - let width = self.config.softness.0; + let width = self.config.softness; // Like in CSS box-shadow. let sigma = width / 2.; // Adjust width to draw all necessary pixels. @@ -57,7 +57,7 @@ impl Shadow { let offset = self.config.offset; let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0))); - let spread = self.config.spread.0; + let spread = self.config.spread; let spread = ceil(spread.abs()).copysign(spread); let offset = offset - Point::from((spread, spread)); diff --git a/src/layout/tab_indicator.rs b/src/layout/tab_indicator.rs index e35874e8..45405ccc 100644 --- a/src/layout/tab_indicator.rs +++ b/src/layout/tab_indicator.rs @@ -83,10 +83,10 @@ impl TabIndicator { let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.)); - let width = round_max1(self.config.width.0); - let gap = self.config.gap.0; + let width = round_max1(self.config.width); + let gap = self.config.gap; let gap = round_max1(gap.abs()).copysign(gap); - let gaps_between = round_max1(self.config.gaps_between_tabs.0); + let gaps_between = round_max1(self.config.gaps_between_tabs); let position = self.config.position; let side = match position { @@ -104,7 +104,7 @@ impl TabIndicator { let px_per_tab = (length + gaps_between) / count as f64 - gaps_between; let px_per_tab = px_per_tab * progress; - let gaps_between = round(self.config.gaps_between_tabs.0 * progress); + let gaps_between = round(self.config.gaps_between_tabs * progress); let length = count as f64 * (px_per_tab + gaps_between) - gaps_between; let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab); @@ -185,8 +185,8 @@ impl TabIndicator { self.shader_locs.resize_with(count, Default::default); let position = self.config.position; - let radius = self.config.corner_radius.0 as f32; - let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.; + let radius = self.config.corner_radius as f32; + let shared_rounded_corners = self.config.gaps_between_tabs == 0.; let mut tabs_left = tab_count; let rects = self.tab_rects(area, count, scale); @@ -317,8 +317,8 @@ impl TabIndicator { } let round = |logical: f64| round_logical_in_physical(scale, logical); - let width = round(self.config.width.0); - let gap = round(self.config.gap.0); + let width = round(self.config.width); + let gap = round(self.config.gap); // No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough // that it peeks from the other side of the window". diff --git a/src/layout/tests.rs b/src/layout/tests.rs index 4afa5364..1c071eb0 100644 --- a/src/layout/tests.rs +++ b/src/layout/tests.rs @@ -2067,7 +2067,7 @@ fn large_negative_height_change() { let mut options = Options::default(); options.border.off = false; - options.border.width = FloatOrInt(1.); + options.border.width = 1.; check_ops_with_options(options, ops); } @@ -2086,7 +2086,7 @@ fn large_max_size() { let mut options = Options::default(); options.border.off = false; - options.border.width = FloatOrInt(1.); + options.border.width = 1.; check_ops_with_options(options, ops); } @@ -2292,8 +2292,9 @@ fn removing_all_outputs_preserves_empty_named_workspaces() { #[test] fn config_change_updates_cached_sizes() { let mut config = Config::default(); - config.layout.border.off = false; - config.layout.border.width = FloatOrInt(2.); + let border = config.layout.border.get_or_insert_with(Default::default); + border.off = false; + border.width = Some(FloatOrInt(2.)); let mut layout = Layout::new(Clock::default(), &config); @@ -2305,7 +2306,11 @@ fn config_change_updates_cached_sizes() { } .apply(&mut layout); - config.layout.border.width = FloatOrInt(4.); + config + .layout + .border + .get_or_insert_with(Default::default) + .width = Some(FloatOrInt(4.)); layout.update_config(&config); layout.verify_invariants(); @@ -2314,7 +2319,7 @@ fn config_change_updates_cached_sizes() { #[test] fn preset_height_change_removes_preset() { let mut config = Config::default(); - config.layout.preset_window_heights = vec![PresetSize::Fixed(1), PresetSize::Fixed(2)]; + config.layout.preset_window_heights = Some(vec![PresetSize::Fixed(1), PresetSize::Fixed(2)]); let mut layout = Layout::new(Clock::default(), &config); @@ -2335,7 +2340,7 @@ fn preset_height_change_removes_preset() { } // Leave only one. - config.layout.preset_window_heights = vec![PresetSize::Fixed(1)]; + config.layout.preset_window_heights = Some(vec![PresetSize::Fixed(1)]); layout.update_config(&config); @@ -2424,7 +2429,7 @@ fn fixed_height_takes_max_non_auto_into_account() { let options = Options { border: niri_config::Border { off: false, - width: niri_config::FloatOrInt(4.), + width: 4., ..Default::default() }, gaps: 0., @@ -3173,7 +3178,7 @@ fn preset_column_width_fixed_correct_with_border() { preset_column_widths: vec![PresetSize::Fixed(500)], border: niri_config::Border { off: false, - width: FloatOrInt(5.), + width: 5., ..Default::default() }, ..Default::default() @@ -3408,7 +3413,7 @@ prop_compose! { ) -> niri_config::FocusRing { niri_config::FocusRing { off, - width: FloatOrInt(width), + width, ..Default::default() } } @@ -3421,7 +3426,7 @@ prop_compose! { ) -> niri_config::Border { niri_config::Border { off, - width: FloatOrInt(width), + width, ..Default::default() } } @@ -3434,7 +3439,7 @@ prop_compose! { ) -> niri_config::Shadow { niri_config::Shadow { on, - softness: FloatOrInt(width), + softness: width, ..Default::default() } } @@ -3454,8 +3459,8 @@ prop_compose! { off, hide_when_single_tab, place_within_column, - width: FloatOrInt(width), - gap: FloatOrInt(gap), + width, + gap, length: TabIndicatorLength { total_proportion: Some(length) }, position, ..Default::default() diff --git a/src/layout/tests/fullscreen.rs b/src/layout/tests/fullscreen.rs index 4d9d79ca..904595f5 100644 --- a/src/layout/tests/fullscreen.rs +++ b/src/layout/tests/fullscreen.rs @@ -190,7 +190,7 @@ fn unfullscreen_with_large_border() { let options = Options { border: niri_config::Border { off: false, - width: niri_config::FloatOrInt(10000.), + width: 10000., ..Default::default() }, ..Default::default() diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index d2340949..8c1c36d3 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -1859,8 +1859,8 @@ fn compute_workspace_shadow_config( let norm = view_size.h / 1080.; let mut config = niri_config::Shadow::from(config); - config.softness.0 *= norm; - config.spread.0 *= norm; + config.softness *= norm; + config.spread *= norm; config.offset.x.0 *= norm; config.offset.y.0 *= norm; diff --git a/src/niri.rs b/src/niri.rs index cb309830..8835cfb8 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1657,7 +1657,7 @@ impl State { let background_color = config .and_then(|c| c.background_color) - .unwrap_or(full_config.layout.background_color) + .unwrap_or_else(|| full_config.resolve_layout().background_color) .to_array_unpremul(); let background_color = Color32F::from(background_color); @@ -2906,7 +2906,7 @@ impl Niri { let background_color = c .and_then(|c| c.background_color) - .unwrap_or(config.layout.background_color) + .unwrap_or_else(|| config.resolve_layout().background_color) .to_array_unpremul(); let mut backdrop_color = c diff --git a/src/tests/animations.rs b/src/tests/animations.rs index 508fd6d0..4871f2c0 100644 --- a/src/tests/animations.rs +++ b/src/tests/animations.rs @@ -74,7 +74,7 @@ fn set_up() -> Fixture { }); let mut config = Config::default(); - config.layout.gaps = FloatOrInt(0.0); + config.layout.gaps = Some(FloatOrInt(0.0)); config.animations.window_resize.anim.kind = LINEAR; config.animations.window_movement.0.kind = LINEAR; -- cgit