From 02e3f9f66ee71ce5252df3bb7b7c9743e95fdcbc Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Wed, 27 Aug 2025 10:31:14 +0300 Subject: config: Extract appearance and layout --- niri-config/src/appearance.rs | 1143 ++++++++++++++++++++++++++++++++++++ niri-config/src/layer_rule.rs | 2 +- niri-config/src/layout.rs | 132 +++++ niri-config/src/lib.rs | 1267 +--------------------------------------- niri-config/src/window_rule.rs | 7 +- 5 files changed, 1287 insertions(+), 1264 deletions(-) create mode 100644 niri-config/src/appearance.rs create mode 100644 niri-config/src/layout.rs diff --git a/niri-config/src/appearance.rs b/niri-config/src/appearance.rs new file mode 100644 index 00000000..a50e6a55 --- /dev/null +++ b/niri-config/src/appearance.rs @@ -0,0 +1,1143 @@ +use std::ops::{Mul, MulAssign}; +use std::str::FromStr; + +use knuffel::errors::DecodeError; +use miette::{miette, IntoDiagnostic as _}; +use smithay::backend::renderer::Color32F; + +use crate::FloatOrInt; + +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]); +pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15, 0.15, 1.]); + +/// RGB color in [0, 1] with unpremultiplied alpha. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl Color { + pub const fn new_unpremul(r: f32, g: f32, b: f32, a: f32) -> Self { + Self { r, g, b, a } + } + + pub fn from_rgba8_unpremul(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) + } + + pub fn from_array_premul([r, g, b, a]: [f32; 4]) -> Self { + let a = a.clamp(0., 1.); + + if a == 0. { + Self::new_unpremul(0., 0., 0., 0.) + } else { + Self { + r: (r / a).clamp(0., 1.), + g: (g / a).clamp(0., 1.), + b: (b / a).clamp(0., 1.), + a, + } + } + } + + pub const fn from_array_unpremul([r, g, b, a]: [f32; 4]) -> Self { + Self { r, g, b, a } + } + + pub fn from_color32f(color: Color32F) -> Self { + Self::from_array_premul(color.components()) + } + + pub fn to_array_unpremul(self) -> [f32; 4] { + [self.r, self.g, self.b, self.a] + } + + pub fn to_array_premul(self) -> [f32; 4] { + let [r, g, b, a] = [self.r, self.g, self.b, self.a]; + [r * a, g * a, b * a, a] + } +} + +impl Mul for Color { + type Output = Self; + + fn mul(mut self, rhs: f32) -> Self::Output { + self.a *= rhs; + self + } +} + +impl MulAssign for Color { + fn mul_assign(&mut self, rhs: f32) { + self.a *= rhs; + } +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct Gradient { + #[knuffel(property, str)] + pub from: Color, + #[knuffel(property, str)] + pub to: Color, + #[knuffel(property, default = 180)] + pub angle: i16, + #[knuffel(property, default)] + pub relative_to: GradientRelativeTo, + #[knuffel(property(name = "in"), str, default)] + pub in_: GradientInterpolation, +} + +impl From for Gradient { + fn from(value: Color) -> Self { + Self { + from: value, + to: value, + angle: 0, + relative_to: GradientRelativeTo::Window, + in_: GradientInterpolation::default(), + } + } +} + +#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum GradientRelativeTo { + #[default] + Window, + WorkspaceView, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct GradientInterpolation { + pub color_space: GradientColorSpace, + pub hue_interpolation: HueInterpolation, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum GradientColorSpace { + #[default] + Srgb, + SrgbLinear, + Oklab, + Oklch, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum HueInterpolation { + #[default] + Shorter, + Longer, + Increasing, + Decreasing, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct CornerRadius { + pub top_left: f32, + pub top_right: f32, + pub bottom_right: f32, + pub bottom_left: f32, +} + +impl From for [f32; 4] { + fn from(value: CornerRadius) -> Self { + [ + value.top_left, + value.top_right, + value.bottom_right, + value.bottom_left, + ] + } +} + +impl From for CornerRadius { + fn from(value: f32) -> Self { + Self { + top_left: value, + top_right: value, + bottom_right: value, + bottom_left: value, + } + } +} + +impl CornerRadius { + pub fn fit_to(self, width: f32, height: f32) -> Self { + // Like in CSS: https://drafts.csswg.org/css-backgrounds/#corner-overlap + let reduction = f32::min( + f32::min( + width / (self.top_left + self.top_right), + width / (self.bottom_left + self.bottom_right), + ), + f32::min( + height / (self.top_left + self.bottom_left), + height / (self.top_right + self.bottom_right), + ), + ); + let reduction = f32::min(1., reduction); + + Self { + top_left: self.top_left * reduction, + top_right: self.top_right * reduction, + bottom_right: self.bottom_right * reduction, + bottom_left: self.bottom_left * reduction, + } + } + + pub fn expanded_by(mut self, width: f32) -> Self { + // Radius = 0 is preserved, so that square corners remain square. + if self.top_left > 0. { + self.top_left += width; + } + if self.top_right > 0. { + self.top_right += width; + } + if self.bottom_right > 0. { + self.bottom_right += width; + } + if self.bottom_left > 0. { + self.bottom_left += width; + } + + if width < 0. { + self.top_left = self.top_left.max(0.); + self.top_right = self.top_right.max(0.); + self.bottom_left = self.bottom_left.max(0.); + self.bottom_right = self.bottom_right.max(0.); + } + + self + } + + pub fn scaled_by(self, scale: f32) -> Self { + Self { + top_left: self.top_left * scale, + top_right: self.top_right * scale, + bottom_right: self.bottom_right * scale, + bottom_left: self.bottom_left * scale, + } + } +} + +#[derive(knuffel::Decode, 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 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, +} + +impl Default for FocusRing { + fn default() -> Self { + Self { + off: false, + width: FloatOrInt(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), + active_gradient: None, + inactive_gradient: None, + urgent_gradient: None, + } + } +} + +#[derive(knuffel::Decode, 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 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, +} + +impl Default for Border { + fn default() -> Self { + Self { + off: true, + width: FloatOrInt(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), + active_gradient: None, + inactive_gradient: None, + urgent_gradient: None, + } + } +} + +impl From for FocusRing { + fn from(value: Border) -> Self { + Self { + off: value.off, + width: value.width, + active_color: value.active_color, + inactive_color: value.inactive_color, + urgent_color: value.urgent_color, + active_gradient: value.active_gradient, + inactive_gradient: value.inactive_gradient, + urgent_gradient: value.urgent_gradient, + } + } +} + +impl From for Border { + fn from(value: FocusRing) -> Self { + Self { + off: value.off, + width: value.width, + active_color: value.active_color, + inactive_color: value.inactive_color, + urgent_color: value.urgent_color, + active_gradient: value.active_gradient, + inactive_gradient: value.inactive_gradient, + urgent_gradient: value.urgent_gradient, + } + } +} + +#[derive(knuffel::Decode, 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 draw_behind_window: bool, + #[knuffel(child, default = Self::default().color)] + pub color: Color, + #[knuffel(child)] + pub inactive_color: Option, +} + +impl Default for Shadow { + fn default() -> Self { + Self { + on: false, + offset: ShadowOffset { + x: FloatOrInt(0.), + y: FloatOrInt(5.), + }, + softness: FloatOrInt(30.), + spread: FloatOrInt(5.), + draw_behind_window: false, + color: Color::from_rgba8_unpremul(0, 0, 0, 0x70), + inactive_color: None, + } + } +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct ShadowOffset { + #[knuffel(property, default)] + pub x: FloatOrInt<-65535, 65535>, + #[knuffel(property, default)] + pub y: FloatOrInt<-65535, 65535>, +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct WorkspaceShadow { + #[knuffel(child)] + pub off: 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, default = Self::default().color)] + pub color: Color, +} + +impl Default for WorkspaceShadow { + fn default() -> Self { + Self { + off: false, + offset: ShadowOffset { + x: FloatOrInt(0.), + y: FloatOrInt(10.), + }, + softness: FloatOrInt(40.), + spread: FloatOrInt(10.), + color: Color::from_rgba8_unpremul(0, 0, 0, 0x50), + } + } +} + +impl From for Shadow { + fn from(value: WorkspaceShadow) -> Self { + Self { + on: !value.off, + offset: value.offset, + softness: value.softness, + spread: value.spread, + draw_behind_window: false, + color: value.color, + inactive_color: None, + } + } +} + +#[derive(knuffel::Decode, 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 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 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, +} + +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)] + pub total_proportion: Option, +} + +#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] +pub enum TabIndicatorPosition { + Left, + Right, + Top, + Bottom, +} + +#[derive(knuffel::Decode, 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, +} + +impl Default for InsertHint { + fn default() -> Self { + Self { + off: false, + color: Color::from_rgba8_unpremul(127, 200, 255, 128), + gradient: None, + } + } +} + +#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlockOutFrom { + Screencast, + ScreenCapture, +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct BorderRule { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child, unwrap(argument))] + pub width: 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, Default, Clone, Copy, PartialEq)] +pub struct ShadowRule { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child)] + pub offset: Option, + #[knuffel(child, unwrap(argument))] + pub softness: Option>, + #[knuffel(child, unwrap(argument))] + pub spread: Option>, + #[knuffel(child, unwrap(argument))] + pub draw_behind_window: Option, + #[knuffel(child)] + pub color: Option, + #[knuffel(child)] + pub inactive_color: Option, +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct TabIndicatorRule { + #[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, +} + +impl BorderRule { + pub fn merge_with(&mut self, other: &Self) { + if other.off { + self.off = true; + self.on = false; + } + + if other.on { + self.off = false; + self.on = true; + } + + if let Some(x) = other.width { + self.width = Some(x); + } + if let Some(x) = other.active_color { + self.active_color = Some(x); + } + if let Some(x) = other.inactive_color { + self.inactive_color = Some(x); + } + if let Some(x) = other.urgent_color { + self.urgent_color = Some(x); + } + if let Some(x) = other.active_gradient { + self.active_gradient = Some(x); + } + if let Some(x) = other.inactive_gradient { + self.inactive_gradient = Some(x); + } + if let Some(x) = other.urgent_gradient { + self.urgent_gradient = Some(x); + } + } + + pub fn resolve_against(&self, mut config: Border) -> Border { + config.off |= self.off; + if self.on { + config.off = false; + } + + if let Some(x) = self.width { + config.width = x; + } + if let Some(x) = self.active_color { + config.active_color = x; + config.active_gradient = None; + } + if let Some(x) = self.inactive_color { + config.inactive_color = x; + config.inactive_gradient = None; + } + if let Some(x) = self.urgent_color { + config.urgent_color = x; + config.urgent_gradient = None; + } + if let Some(x) = self.active_gradient { + config.active_gradient = Some(x); + } + if let Some(x) = self.inactive_gradient { + config.inactive_gradient = Some(x); + } + if let Some(x) = self.urgent_gradient { + config.urgent_gradient = Some(x); + } + + config + } +} + +impl ShadowRule { + pub fn merge_with(&mut self, other: &Self) { + if other.off { + self.off = true; + self.on = false; + } + + if other.on { + self.off = false; + self.on = true; + } + + if let Some(x) = other.offset { + self.offset = Some(x); + } + if let Some(x) = other.softness { + self.softness = Some(x); + } + if let Some(x) = other.spread { + self.spread = Some(x); + } + if let Some(x) = other.draw_behind_window { + self.draw_behind_window = Some(x); + } + if let Some(x) = other.color { + self.color = Some(x); + } + if let Some(x) = other.inactive_color { + self.inactive_color = Some(x); + } + } + + pub fn resolve_against(&self, mut config: Shadow) -> Shadow { + config.on |= self.on; + if self.off { + config.on = false; + } + + if let Some(x) = self.offset { + config.offset = x; + } + if let Some(x) = self.softness { + config.softness = x; + } + if let Some(x) = self.spread { + config.spread = x; + } + if let Some(x) = self.draw_behind_window { + config.draw_behind_window = x; + } + if let Some(x) = self.color { + config.color = x; + } + if let Some(x) = self.inactive_color { + config.inactive_color = Some(x); + } + + config + } +} + +impl TabIndicatorRule { + pub fn merge_with(&mut self, other: &Self) { + if let Some(x) = other.active_color { + self.active_color = Some(x); + } + if let Some(x) = other.inactive_color { + self.inactive_color = Some(x); + } + if let Some(x) = other.urgent_color { + self.urgent_color = Some(x); + } + if let Some(x) = other.active_gradient { + self.active_gradient = Some(x); + } + if let Some(x) = other.inactive_gradient { + self.inactive_gradient = Some(x); + } + if let Some(x) = other.urgent_gradient { + self.urgent_gradient = Some(x); + } + } +} + +impl FromStr for GradientInterpolation { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + let mut iter = s.split_whitespace(); + let in_part1 = iter.next(); + let in_part2 = iter.next(); + let in_part3 = iter.next(); + + let Some(in_part1) = in_part1 else { + return Err(miette!("missing color space")); + }; + + let color = match in_part1 { + "srgb" => GradientColorSpace::Srgb, + "srgb-linear" => GradientColorSpace::SrgbLinear, + "oklab" => GradientColorSpace::Oklab, + "oklch" => GradientColorSpace::Oklch, + x => { + return Err(miette!( + "invalid color space {x}; can be srgb, srgb-linear, oklab or oklch" + )) + } + }; + + let interpolation = if let Some(in_part2) = in_part2 { + if color != GradientColorSpace::Oklch { + return Err(miette!("only oklch color space can have hue interpolation")); + } + + if in_part3 != Some("hue") { + return Err(miette!( + "interpolation must end with \"hue\", like \"oklch shorter hue\"" + )); + } else if iter.next().is_some() { + return Err(miette!("unexpected text after hue interpolation")); + } else { + match in_part2 { + "shorter" => HueInterpolation::Shorter, + "longer" => HueInterpolation::Longer, + "increasing" => HueInterpolation::Increasing, + "decreasing" => HueInterpolation::Decreasing, + x => { + return Err(miette!( + "invalid hue interpolation {x}; \ + can be shorter, longer, increasing, decreasing" + )) + } + } + } + } else { + HueInterpolation::default() + }; + + Ok(Self { + color_space: color, + hue_interpolation: interpolation, + }) + } +} + +impl FromStr for Color { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + let color = csscolorparser::parse(s) + .into_diagnostic()? + .clamp() + .to_array(); + Ok(Self::from_array_unpremul(color)) + } +} + +#[derive(knuffel::Decode)] +struct ColorRgba { + #[knuffel(argument)] + r: u8, + #[knuffel(argument)] + g: u8, + #[knuffel(argument)] + b: u8, + #[knuffel(argument)] + a: u8, +} + +impl From for Color { + fn from(value: ColorRgba) -> Self { + let ColorRgba { r, g, b, a } = value; + Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) + } +} + +// Manual impl to allow both one-argument string and 4-argument RGBA forms. +impl knuffel::Decode for Color +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + // Check for unexpected type name. + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + + // Get the first argument. + let mut iter_args = node.arguments.iter(); + let val = iter_args + .next() + .ok_or_else(|| DecodeError::missing(node, "additional argument is required"))?; + + // Check for unexpected type name. + if let Some(typ) = &val.type_name { + ctx.emit_error(DecodeError::TypeName { + span: typ.span().clone(), + found: Some((**typ).clone()), + expected: knuffel::errors::ExpectedType::no_type(), + rust_type: "str", + }); + } + + // Check the argument type. + let rv = match *val.literal { + // If it's a string, use FromStr. + knuffel::ast::Literal::String(ref s) => { + Color::from_str(s).map_err(|e| DecodeError::conversion(&val.literal, e)) + } + // Otherwise, fall back to the 4-argument RGBA form. + _ => return ColorRgba::decode_node(node, ctx).map(Color::from), + }?; + + // Check for unexpected following arguments. + if let Some(val) = iter_args.next() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "unexpected argument", + )); + } + + // Check for unexpected properties and children. + for name in node.properties.keys() { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name.escape_default()), + )); + } + for child in node.children.as_ref().map(|lst| &lst[..]).unwrap_or(&[]) { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + Ok(rv) + } +} + +impl knuffel::Decode for CornerRadius +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + // Check for unexpected type name. + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + + let decode_radius = |ctx: &mut knuffel::decode::Context, + val: &knuffel::ast::Value| { + // Check for unexpected type name. + if let Some(typ) = &val.type_name { + ctx.emit_error(DecodeError::TypeName { + span: typ.span().clone(), + found: Some((**typ).clone()), + expected: knuffel::errors::ExpectedType::no_type(), + rust_type: "str", + }); + } + + // Decode both integers and floats. + let radius = match *val.literal { + knuffel::ast::Literal::Int(ref x) => f32::from(match x.try_into() { + Ok(x) => x, + Err(err) => { + ctx.emit_error(DecodeError::conversion(&val.literal, err)); + 0i16 + } + }), + knuffel::ast::Literal::Decimal(ref x) => match x.try_into() { + Ok(x) => x, + Err(err) => { + ctx.emit_error(DecodeError::conversion(&val.literal, err)); + 0. + } + }, + _ => { + ctx.emit_error(DecodeError::scalar_kind( + knuffel::decode::Kind::Int, + &val.literal, + )); + 0. + } + }; + + if radius < 0. { + ctx.emit_error(DecodeError::conversion(&val.literal, "radius must be >= 0")); + } + + radius + }; + + // Get the first argument. + let mut iter_args = node.arguments.iter(); + let val = iter_args + .next() + .ok_or_else(|| DecodeError::missing(node, "additional argument is required"))?; + + let top_left = decode_radius(ctx, val); + + let mut rv = CornerRadius { + top_left, + top_right: top_left, + bottom_right: top_left, + bottom_left: top_left, + }; + + if let Some(val) = iter_args.next() { + rv.top_right = decode_radius(ctx, val); + + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing(node, "either 1 or 4 arguments are required") + })?; + rv.bottom_right = decode_radius(ctx, val); + + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing(node, "either 1 or 4 arguments are required") + })?; + rv.bottom_left = decode_radius(ctx, val); + + // Check for unexpected following arguments. + if let Some(val) = iter_args.next() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "unexpected argument", + )); + } + } + + // Check for unexpected properties and children. + for name in node.properties.keys() { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name.escape_default()), + )); + } + for child in node.children.as_ref().map(|lst| &lst[..]).unwrap_or(&[]) { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + Ok(rv) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + + use super::*; + + #[test] + fn parse_gradient_interpolation() { + assert_eq!( + "srgb".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Srgb, + ..Default::default() + } + ); + assert_eq!( + "srgb-linear".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::SrgbLinear, + ..Default::default() + } + ); + assert_eq!( + "oklab".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklab, + ..Default::default() + } + ); + assert_eq!( + "oklch".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + ..Default::default() + } + ); + assert_eq!( + "oklch shorter hue" + .parse::() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Shorter, + } + ); + assert_eq!( + "oklch longer hue".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Longer, + } + ); + assert_eq!( + "oklch decreasing hue" + .parse::() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Decreasing, + } + ); + assert_eq!( + "oklch increasing hue" + .parse::() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Increasing, + } + ); + + assert!("".parse::().is_err()); + assert!("srgb shorter hue".parse::().is_err()); + assert!("oklch shorter".parse::().is_err()); + assert!("oklch shorter h".parse::().is_err()); + assert!("oklch a hue".parse::().is_err()); + assert!("oklch shorter hue a" + .parse::() + .is_err()); + } + + #[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, + }; + + for rule in rules.iter().copied() { + let rule = BorderRule { + off: rule == "off" || rule == "off,on", + on: rule == "on" || rule == "off,on", + ..Default::default() + }; + + resolved.merge_with(&rule); + } + + let config = Border { + off: config == "off", + ..Default::default() + }; + + if resolved.resolve_against(config).off { + "off" + } else { + "on" + } + .to_owned() + } + + assert_snapshot!(is_on("off", &[]), @"off"); + assert_snapshot!(is_on("off", &["off"]), @"off"); + assert_snapshot!(is_on("off", &["on"]), @"on"); + assert_snapshot!(is_on("off", &["off,on"]), @"on"); + + assert_snapshot!(is_on("on", &[]), @"on"); + assert_snapshot!(is_on("on", &["off"]), @"off"); + assert_snapshot!(is_on("on", &["on"]), @"on"); + assert_snapshot!(is_on("on", &["off,on"]), @"on"); + + assert_snapshot!(is_on("off", &["off", "off"]), @"off"); + assert_snapshot!(is_on("off", &["off", "on"]), @"on"); + assert_snapshot!(is_on("off", &["on", "off"]), @"off"); + assert_snapshot!(is_on("off", &["on", "on"]), @"on"); + + assert_snapshot!(is_on("on", &["off", "off"]), @"off"); + assert_snapshot!(is_on("on", &["off", "on"]), @"on"); + assert_snapshot!(is_on("on", &["on", "off"]), @"off"); + assert_snapshot!(is_on("on", &["on", "on"]), @"on"); + } +} diff --git a/niri-config/src/layer_rule.rs b/niri-config/src/layer_rule.rs index 08dad40b..c11edc13 100644 --- a/niri-config/src/layer_rule.rs +++ b/niri-config/src/layer_rule.rs @@ -1,5 +1,5 @@ +use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule}; use crate::utils::RegexEq; -use crate::{BlockOutFrom, CornerRadius, ShadowRule}; #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] pub struct LayerRule { diff --git a/niri-config/src/layout.rs b/niri-config/src/layout.rs new file mode 100644 index 00000000..6f9c9135 --- /dev/null +++ b/niri-config/src/layout.rs @@ -0,0 +1,132 @@ +use knuffel::errors::DecodeError; +use niri_ipc::{ColumnDisplay, SizeChange}; + +use crate::appearance::{ + Border, FocusRing, InsertHint, Shadow, TabIndicator, DEFAULT_BACKGROUND_COLOR, +}; +use crate::{expect_only_children, Color, FloatOrInt}; + +#[derive(knuffel::Decode, 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 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 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(), + 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(), + background_color: DEFAULT_BACKGROUND_COLOR, + } + } +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub enum PresetSize { + Proportion(#[knuffel(argument)] f64), + Fixed(#[knuffel(argument)] i32), +} + +impl From for SizeChange { + fn from(value: PresetSize) -> Self { + match value { + PresetSize::Proportion(prop) => SizeChange::SetProportion(prop * 100.), + PresetSize::Fixed(fixed) => SizeChange::SetFixed(fixed), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct DefaultPresetSize(pub Option); + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct Struts { + #[knuffel(child, unwrap(argument), default)] + pub left: FloatOrInt<-65535, 65535>, + #[knuffel(child, unwrap(argument), default)] + pub right: FloatOrInt<-65535, 65535>, + #[knuffel(child, unwrap(argument), default)] + pub top: FloatOrInt<-65535, 65535>, + #[knuffel(child, unwrap(argument), default)] + pub bottom: FloatOrInt<-65535, 65535>, +} + +#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum CenterFocusedColumn { + /// Focusing a column will not center the column. + #[default] + Never, + /// The focused column will always be centered. + Always, + /// Focusing a column will center it if it doesn't fit on the screen together with the + /// previously focused column. + OnOverflow, +} + +impl knuffel::Decode for DefaultPresetSize +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + expect_only_children(node, ctx); + + let mut children = node.children(); + + if let Some(child) = children.next() { + if let Some(unwanted_child) = children.next() { + ctx.emit_error(DecodeError::unexpected( + unwanted_child, + "node", + "expected no more than one child", + )); + } + PresetSize::decode_node(child, ctx).map(Some).map(Self) + } else { + Ok(Self(None)) + } + } +} diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 35789be2..0de753f7 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -5,7 +5,6 @@ use std::collections::HashSet; use std::ffi::OsStr; use std::fs::{self, File}; use std::io::Write; -use std::ops::{Mul, MulAssign}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; @@ -16,17 +15,15 @@ use miette::{miette, Context, IntoDiagnostic}; use niri_ipc::{ ColumnDisplay, LayoutSwitchTarget, PositionChange, SizeChange, WorkspaceReferenceArg, }; -use smithay::backend::renderer::Color32F; use smithay::input::keyboard::keysyms::KEY_NoSymbol; use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE}; use smithay::input::keyboard::Keysym; -pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]); -pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15, 0.15, 1.]); - pub mod animations; +pub mod appearance; pub mod input; pub mod layer_rule; +pub mod layout; pub mod output; pub mod utils; pub mod window_rule; @@ -34,8 +31,10 @@ pub mod window_rule; pub use crate::animations::{ Animation, AnimationCurve, AnimationKind, Animations, EasingParams, SpringParams, }; +pub use crate::appearance::*; pub use crate::input::{Input, ModKey, ScrollMethod, TrackLayout, WarpMouseToFocusMode, Xkb}; pub use crate::layer_rule::LayerRule; +pub use crate::layout::*; pub use crate::output::{Output, OutputName, Outputs, Position, Vrr}; pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule}; @@ -93,18 +92,6 @@ pub struct Config { pub workspaces: Vec, } -#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq, Clone, Copy)] -pub enum CenterFocusedColumn { - /// Focusing a column will not center the column. - #[default] - Never, - /// The focused column will always be centered. - Always, - /// Focusing a column will center it if it doesn't fit on the screen together with the - /// previously focused column. - OnOverflow, -} - #[derive(Debug, Clone, Copy, PartialEq)] pub struct Percent(pub f64); @@ -112,62 +99,6 @@ pub struct Percent(pub f64); #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct FloatOrInt(pub f64); -#[derive(knuffel::Decode, 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 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 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(), - 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(), - background_color: DEFAULT_BACKGROUND_COLOR, - } - } -} - #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct SpawnAtStartup { #[knuffel(arguments)] @@ -180,407 +111,6 @@ pub struct SpawnShAtStartup { pub command: String, } -#[derive(knuffel::Decode, 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 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, -} - -impl Default for FocusRing { - fn default() -> Self { - Self { - off: false, - width: FloatOrInt(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), - active_gradient: None, - inactive_gradient: None, - urgent_gradient: None, - } - } -} - -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] -pub struct Gradient { - #[knuffel(property, str)] - pub from: Color, - #[knuffel(property, str)] - pub to: Color, - #[knuffel(property, default = 180)] - pub angle: i16, - #[knuffel(property, default)] - pub relative_to: GradientRelativeTo, - #[knuffel(property(name = "in"), str, default)] - pub in_: GradientInterpolation, -} - -impl From for Gradient { - fn from(value: Color) -> Self { - Self { - from: value, - to: value, - angle: 0, - relative_to: GradientRelativeTo::Window, - in_: GradientInterpolation::default(), - } - } -} - -#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum GradientRelativeTo { - #[default] - Window, - WorkspaceView, -} - -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct GradientInterpolation { - pub color_space: GradientColorSpace, - pub hue_interpolation: HueInterpolation, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum GradientColorSpace { - #[default] - Srgb, - SrgbLinear, - Oklab, - Oklch, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum HueInterpolation { - #[default] - Shorter, - Longer, - Increasing, - Decreasing, -} - -#[derive(knuffel::Decode, 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 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, -} - -impl Default for Border { - fn default() -> Self { - Self { - off: true, - width: FloatOrInt(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), - active_gradient: None, - inactive_gradient: None, - urgent_gradient: None, - } - } -} - -impl From for FocusRing { - fn from(value: Border) -> Self { - Self { - off: value.off, - width: value.width, - active_color: value.active_color, - inactive_color: value.inactive_color, - urgent_color: value.urgent_color, - active_gradient: value.active_gradient, - inactive_gradient: value.inactive_gradient, - urgent_gradient: value.urgent_gradient, - } - } -} - -impl From for Border { - fn from(value: FocusRing) -> Self { - Self { - off: value.off, - width: value.width, - active_color: value.active_color, - inactive_color: value.inactive_color, - urgent_color: value.urgent_color, - active_gradient: value.active_gradient, - inactive_gradient: value.inactive_gradient, - urgent_gradient: value.urgent_gradient, - } - } -} - -#[derive(knuffel::Decode, 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 draw_behind_window: bool, - #[knuffel(child, default = Self::default().color)] - pub color: Color, - #[knuffel(child)] - pub inactive_color: Option, -} - -impl Default for Shadow { - fn default() -> Self { - Self { - on: false, - offset: ShadowOffset { - x: FloatOrInt(0.), - y: FloatOrInt(5.), - }, - softness: FloatOrInt(30.), - spread: FloatOrInt(5.), - draw_behind_window: false, - color: Color::from_rgba8_unpremul(0, 0, 0, 0x70), - inactive_color: None, - } - } -} - -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] -pub struct ShadowOffset { - #[knuffel(property, default)] - pub x: FloatOrInt<-65535, 65535>, - #[knuffel(property, default)] - pub y: FloatOrInt<-65535, 65535>, -} - -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] -pub struct WorkspaceShadow { - #[knuffel(child)] - pub off: 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, default = Self::default().color)] - pub color: Color, -} - -impl Default for WorkspaceShadow { - fn default() -> Self { - Self { - off: false, - offset: ShadowOffset { - x: FloatOrInt(0.), - y: FloatOrInt(10.), - }, - softness: FloatOrInt(40.), - spread: FloatOrInt(10.), - color: Color::from_rgba8_unpremul(0, 0, 0, 0x50), - } - } -} - -impl From for Shadow { - fn from(value: WorkspaceShadow) -> Self { - Self { - on: !value.off, - offset: value.offset, - softness: value.softness, - spread: value.spread, - draw_behind_window: false, - color: value.color, - inactive_color: None, - } - } -} - -#[derive(knuffel::Decode, 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 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 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, -} - -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)] - pub total_proportion: Option, -} - -#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] -pub enum TabIndicatorPosition { - Left, - Right, - Top, - Bottom, -} - -#[derive(knuffel::Decode, 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, -} - -impl Default for InsertHint { - fn default() -> Self { - Self { - off: false, - color: Color::from_rgba8_unpremul(127, 200, 255, 128), - gradient: None, - } - } -} - -/// RGB color in [0, 1] with unpremultiplied alpha. -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub struct Color { - pub r: f32, - pub g: f32, - pub b: f32, - pub a: f32, -} - -impl Color { - pub const fn new_unpremul(r: f32, g: f32, b: f32, a: f32) -> Self { - Self { r, g, b, a } - } - - pub fn from_rgba8_unpremul(r: u8, g: u8, b: u8, a: u8) -> Self { - Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) - } - - pub fn from_array_premul([r, g, b, a]: [f32; 4]) -> Self { - let a = a.clamp(0., 1.); - - if a == 0. { - Self::new_unpremul(0., 0., 0., 0.) - } else { - Self { - r: (r / a).clamp(0., 1.), - g: (g / a).clamp(0., 1.), - b: (b / a).clamp(0., 1.), - a, - } - } - } - - pub const fn from_array_unpremul([r, g, b, a]: [f32; 4]) -> Self { - Self { r, g, b, a } - } - - pub fn from_color32f(color: Color32F) -> Self { - Self::from_array_premul(color.components()) - } - - pub fn to_array_unpremul(self) -> [f32; 4] { - [self.r, self.g, self.b, self.a] - } - - pub fn to_array_premul(self) -> [f32; 4] { - let [r, g, b, a] = [self.r, self.g, self.b, self.a]; - [r * a, g * a, b * a, a] - } -} - -impl Mul for Color { - type Output = Self; - - fn mul(mut self, rhs: f32) -> Self::Output { - self.a *= rhs; - self - } -} - -impl MulAssign for Color { - fn mul_assign(&mut self, rhs: f32) { - self.a *= rhs; - } -} - #[derive(knuffel::Decode, Debug, PartialEq)] pub struct Cursor { #[knuffel(child, unwrap(argument), default = String::from("default"))] @@ -604,36 +134,6 @@ impl Default for Cursor { } } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] -pub enum PresetSize { - Proportion(#[knuffel(argument)] f64), - Fixed(#[knuffel(argument)] i32), -} - -impl From for SizeChange { - fn from(value: PresetSize) -> Self { - match value { - PresetSize::Proportion(prop) => SizeChange::SetProportion(prop * 100.), - PresetSize::Fixed(fixed) => SizeChange::SetFixed(fixed), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct DefaultPresetSize(pub Option); - -#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] -pub struct Struts { - #[knuffel(child, unwrap(argument), default)] - pub left: FloatOrInt<-65535, 65535>, - #[knuffel(child, unwrap(argument), default)] - pub right: FloatOrInt<-65535, 65535>, - #[knuffel(child, unwrap(argument), default)] - pub top: FloatOrInt<-65535, 65535>, - #[knuffel(child, unwrap(argument), default)] - pub bottom: FloatOrInt<-65535, 65535>, -} - #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct HotkeyOverlay { #[knuffel(child)] @@ -717,15 +217,15 @@ pub struct Overview { #[knuffel(child, default = Self::default().backdrop_color)] pub backdrop_color: Color, #[knuffel(child, default)] - pub workspace_shadow: WorkspaceShadow, + pub workspace_shadow: appearance::WorkspaceShadow, } impl Default for Overview { fn default() -> Self { Self { zoom: FloatOrInt(0.5), - backdrop_color: DEFAULT_BACKDROP_COLOR, - workspace_shadow: WorkspaceShadow::default(), + backdrop_color: appearance::DEFAULT_BACKDROP_COLOR, + workspace_shadow: appearance::WorkspaceShadow::default(), } } } @@ -769,100 +269,6 @@ pub struct Workspace { #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkspaceName(pub String); -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub struct CornerRadius { - pub top_left: f32, - pub top_right: f32, - pub bottom_right: f32, - pub bottom_left: f32, -} - -impl From for [f32; 4] { - fn from(value: CornerRadius) -> Self { - [ - value.top_left, - value.top_right, - value.bottom_right, - value.bottom_left, - ] - } -} - -impl From for CornerRadius { - fn from(value: f32) -> Self { - Self { - top_left: value, - top_right: value, - bottom_right: value, - bottom_left: value, - } - } -} - -#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)] -pub enum BlockOutFrom { - Screencast, - ScreenCapture, -} - -#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] -pub struct BorderRule { - #[knuffel(child)] - pub off: bool, - #[knuffel(child)] - pub on: bool, - #[knuffel(child, unwrap(argument))] - pub width: 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, Default, Clone, Copy, PartialEq)] -pub struct ShadowRule { - #[knuffel(child)] - pub off: bool, - #[knuffel(child)] - pub on: bool, - #[knuffel(child)] - pub offset: Option, - #[knuffel(child, unwrap(argument))] - pub softness: Option>, - #[knuffel(child, unwrap(argument))] - pub spread: Option>, - #[knuffel(child, unwrap(argument))] - pub draw_behind_window: Option, - #[knuffel(child)] - pub color: Option, - #[knuffel(child)] - pub inactive_color: Option, -} - -#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] -pub struct TabIndicatorRule { - #[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(Debug, Default, PartialEq)] pub struct Binds(pub Vec); @@ -1776,383 +1182,6 @@ impl Default for Config { } } -impl BorderRule { - pub fn merge_with(&mut self, other: &Self) { - if other.off { - self.off = true; - self.on = false; - } - - if other.on { - self.off = false; - self.on = true; - } - - if let Some(x) = other.width { - self.width = Some(x); - } - if let Some(x) = other.active_color { - self.active_color = Some(x); - } - if let Some(x) = other.inactive_color { - self.inactive_color = Some(x); - } - if let Some(x) = other.urgent_color { - self.urgent_color = Some(x); - } - if let Some(x) = other.active_gradient { - self.active_gradient = Some(x); - } - if let Some(x) = other.inactive_gradient { - self.inactive_gradient = Some(x); - } - if let Some(x) = other.urgent_gradient { - self.urgent_gradient = Some(x); - } - } - - pub fn resolve_against(&self, mut config: Border) -> Border { - config.off |= self.off; - if self.on { - config.off = false; - } - - if let Some(x) = self.width { - config.width = x; - } - if let Some(x) = self.active_color { - config.active_color = x; - config.active_gradient = None; - } - if let Some(x) = self.inactive_color { - config.inactive_color = x; - config.inactive_gradient = None; - } - if let Some(x) = self.urgent_color { - config.urgent_color = x; - config.urgent_gradient = None; - } - if let Some(x) = self.active_gradient { - config.active_gradient = Some(x); - } - if let Some(x) = self.inactive_gradient { - config.i