config: Split Layout from LayoutPart

This commit is contained in:
Ivan Molodetskikh
2025-09-20 12:57:41 +03:00
parent 2781d3a743
commit 1fa9dd32ed
21 changed files with 579 additions and 434 deletions

View File

@@ -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<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
@@ -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<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
@@ -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<BorderRule> 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<BorderRule> 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<Color>,
}
@@ -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<ShadowRule> 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<WorkspaceShadow> 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,26 +426,99 @@ impl From<WorkspaceShadow> for Shadow {
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TabIndicator {
pub off: bool,
pub hide_when_single_tab: bool,
pub place_within_column: bool,
pub gap: f64,
pub width: f64,
pub length: TabIndicatorLength,
pub position: TabIndicatorPosition,
pub gaps_between_tabs: f64,
pub corner_radius: f64,
pub active_color: Option<Color>,
pub inactive_color: Option<Color>,
pub urgent_color: Option<Color>,
pub active_gradient: Option<Gradient>,
pub inactive_gradient: Option<Gradient>,
pub urgent_gradient: Option<Gradient>,
}
impl Default for TabIndicator {
fn default() -> Self {
Self {
off: false,
hide_when_single_tab: false,
place_within_column: false,
gap: 5.,
width: 4.,
length: TabIndicatorLength {
total_proportion: Some(0.5),
},
position: TabIndicatorPosition::Left,
gaps_between_tabs: 0.,
corner_radius: 0.,
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
}
}
}
impl MergeWith<TabIndicatorPart> 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 hide_when_single_tab: bool,
pub on: 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 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>,
pub hide_when_single_tab: Option<Flag>,
#[knuffel(child)]
pub place_within_column: Option<Flag>,
#[knuffel(child, unwrap(argument))]
pub gap: Option<FloatOrInt<-65535, 65535>>,
#[knuffel(child, unwrap(argument))]
pub width: Option<FloatOrInt<0, 65535>>,
#[knuffel(child)]
pub length: Option<TabIndicatorLength>,
#[knuffel(child, unwrap(argument))]
pub position: Option<TabIndicatorPosition>,
#[knuffel(child, unwrap(argument))]
pub gaps_between_tabs: Option<FloatOrInt<0, 65535>>,
#[knuffel(child, unwrap(argument))]
pub corner_radius: Option<FloatOrInt<0, 65535>>,
#[knuffel(child)]
pub active_color: Option<Color>,
#[knuffel(child)]
@@ -483,30 +533,6 @@ pub struct TabIndicator {
pub urgent_gradient: Option<Gradient>,
}
impl Default for TabIndicator {
fn default() -> Self {
Self {
off: false,
hide_when_single_tab: false,
place_within_column: false,
gap: FloatOrInt(5.),
width: FloatOrInt(4.),
length: TabIndicatorLength {
total_proportion: Some(0.5),
},
position: TabIndicatorPosition::Left,
gaps_between_tabs: FloatOrInt(0.),
corner_radius: FloatOrInt(0.),
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
}
}
}
#[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<Gradient>,
}
@@ -541,6 +564,29 @@ impl Default for InsertHint {
}
}
impl MergeWith<InsertHintPart> 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<Color>,
#[knuffel(child)]
pub gradient: Option<Gradient>,
}
#[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!(
(

View File

@@ -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<PresetSize>,
#[knuffel(child)]
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(children), default)]
pub default_column_width: Option<PresetSize>,
pub preset_window_heights: Vec<PresetSize>,
#[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<LayoutPart> 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<BorderRule>,
#[knuffel(child)]
pub border: Option<BorderRule>,
#[knuffel(child)]
pub shadow: Option<ShadowRule>,
#[knuffel(child)]
pub tab_indicator: Option<TabIndicatorPart>,
#[knuffel(child)]
pub insert_hint: Option<InsertHintPart>,
#[knuffel(child, unwrap(children))]
pub preset_column_widths: Option<Vec<PresetSize>>,
#[knuffel(child)]
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(children))]
pub preset_window_heights: Option<Vec<PresetSize>>,
#[knuffel(child, unwrap(argument))]
pub center_focused_column: Option<CenterFocusedColumn>,
#[knuffel(child)]
pub always_center_single_column: Option<Flag>,
#[knuffel(child)]
pub empty_workspace_above_first: Option<Flag>,
#[knuffel(child, unwrap(argument, str))]
pub default_column_display: Option<ColumnDisplay>,
#[knuffel(child, unwrap(argument))]
pub gaps: Option<FloatOrInt<0, 65535>>,
#[knuffel(child)]
pub struts: Option<Struts>,
#[knuffel(child)]
pub background_color: Option<Color>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub enum PresetSize {
Proportion(#[knuffel(argument)] f64),

View File

@@ -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<SpawnShAtStartup>,
#[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,
},
),
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,
},
),
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,
},
),
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,
},
),
insert_hint: Some(
InsertHintPart {
off: false,
on: false,
color: Some(
Color {
r: 1.0,
g: 0.78431374,
b: 0.49803922,
a: 1.0,
},
),
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,
},
},
),
},
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,
},
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(
),
preset_column_widths: Some(
[
Proportion(
0.25,
),
Proportion(
0.5,
),
},
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,
},
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,
),
],
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,
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,
),
right: FloatOrInt(
2.0,
),
top: FloatOrInt(
3.0,
),
bottom: FloatOrInt(
0.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,
+ ),
+ ),
+ ),
+ ),
"#
"#,
);
}
}

View File

@@ -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),+ $(,)*) => {
$(

View File

@@ -14,6 +14,15 @@ pub struct Percent(pub f64);
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct FloatOrInt<const MIN: i32, const MAX: i32>(pub f64);
/// Flag, with an optional explicit value.
///
/// Intended to be used as an `Option<MaybeBool>` 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<const MIN: i32, const MAX: i32> MergeWith<FloatOrInt<MIN, MAX>> for f64 {
}
}
impl MergeWith<Flag> for bool {
fn merge_with(&mut self, part: &Flag) {
*self = part.0;
}
}
impl<S: knuffel::traits::ErrorSpan, const MIN: i32, const MAX: i32> knuffel::DecodeScalar<S>
for FloatOrInt<MIN, MAX>
{

View File

@@ -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(),

View File

@@ -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),

View File

@@ -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()
},

View File

@@ -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);

View File

@@ -1227,7 +1227,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
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<i32, Logical> {
let mut border = 0.;
if !border_config.off {
border = border_config.width.0 * 2.;
border = border_config.width * 2.;
}
Size::from((

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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<W: LayoutElement> Layout<W> {
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)

View File

@@ -488,7 +488,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
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<W: LayoutElement> ScrollingSpace<W> {
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<i32, Logical> {
let mut border = 0.;
if !border_config.off {
border = border_config.width.0 * 2.;
border = border_config.width * 2.;
}
Size::from((

View File

@@ -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));

View File

@@ -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".

View File

@@ -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()

View File

@@ -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()

View File

@@ -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;

View File

@@ -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

View File

@@ -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;