diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-27 10:31:14 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-27 10:46:46 +0300 |
| commit | 02e3f9f66ee71ce5252df3bb7b7c9743e95fdcbc (patch) | |
| tree | 788579ba0374a25e26782c80f332469b5c8724a8 | |
| parent | 19c576cfd88fa58c8f3e8123933e8a13ba6da3fa (diff) | |
| download | niri-02e3f9f66ee71ce5252df3bb7b7c9743e95fdcbc.tar.gz niri-02e3f9f66ee71ce5252df3bb7b7c9743e95fdcbc.tar.bz2 niri-02e3f9f66ee71ce5252df3bb7b7c9743e95fdcbc.zip | |
config: Extract appearance and layout
| -rw-r--r-- | niri-config/src/appearance.rs | 1143 | ||||
| -rw-r--r-- | niri-config/src/layer_rule.rs | 2 | ||||
| -rw-r--r-- | niri-config/src/layout.rs | 132 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 1267 | ||||
| -rw-r--r-- | niri-config/src/window_rule.rs | 7 |
5 files changed, 1287 insertions, 1264 deletions
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<f32> for Color { + type Output = Self; + + fn mul(mut self, rhs: f32) -> Self::Output { + self.a *= rhs; + self + } +} + +impl MulAssign<f32> 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<Color> 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<CornerRadius> for [f32; 4] { + fn from(value: CornerRadius) -> Self { + [ + value.top_left, + value.top_right, + value.bottom_right, + value.bottom_left, + ] + } +} + +impl From<f32> 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<Gradient>, + #[knuffel(child)] + pub inactive_gradient: Option<Gradient>, + #[knuffel(child)] + pub urgent_gradient: Option<Gradient>, +} + +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<Gradient>, + #[knuffel(child)] + pub inactive_gradient: Option<Gradient>, + #[knuffel(child)] + pub urgent_gradient: Option<Gradient>, +} + +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<Border> 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<FocusRing> 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<Color>, +} + +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<WorkspaceShadow> 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<Color>, + #[knuffel(child)] + pub inactive_color: Option<Color>, + #[knuffel(child)] + pub urgent_color: Option<Color>, + #[knuffel(child)] + pub active_gradient: Option<Gradient>, + #[knuffel(child)] + pub inactive_gradient: Option<Gradient>, + #[knuffel(child)] + 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)] + pub total_proportion: Option<f64>, +} + +#[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<Gradient>, +} + +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<FloatOrInt<0, 65535>>, + #[knuffel(child)] + pub active_color: Option<Color>, + #[knuffel(child)] + pub inactive_color: Option<Color>, + #[knuffel(child)] + pub urgent_color: Option<Color>, + #[knuffel(child)] + pub active_gradient: Option<Gradient>, + #[knuffel(child)] + pub inactive_gradient: Option<Gradient>, + #[knuffel(child)] + pub urgent_gradient: Option<Gradient>, +} + +#[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<ShadowOffset>, + #[knuffel(child, unwrap(argument))] + pub softness: Option<FloatOrInt<0, 1024>>, + #[knuffel(child, unwrap(argument))] + pub spread: Option<FloatOrInt<-1024, 1024>>, + #[knuffel(child, unwrap(argument))] + pub draw_behind_window: Option<bool>, + #[knuffel(child)] + pub color: Option<Color>, + #[knuffel(child)] + pub inactive_color: Option<Color>, +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct TabIndicatorRule { + #[knuffel(child)] + pub active_color: Option<Color>, + #[knuffel(child)] + pub inactive_color: Option<Color>, + #[knuffel(child)] + pub urgent_color: Option<Color>, + #[knuffel(child)] + pub active_gradient: Option<Gradient>, + #[knuffel(child)] + pub inactive_gradient: Option<Gradient>, + #[knuffel(child)] + pub urgent_gradient: Option<Gradient>, +} + +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<Self, Self::Err> { + 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<Self, Self::Err> { + 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<ColorRgba> 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<S> knuffel::Decode<S> for Color +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + // 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<S> knuffel::Decode<S> for CornerRadius +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + // 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<S>, + val: &knuffel::ast::Value<S>| { + // 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::<GradientInterpolation>().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Srgb, + ..Default::default() + } + ); + assert_eq!( + "srgb-linear".parse::<GradientInterpolation>().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::SrgbLinear, + ..Default::default() + } + ); + assert_eq!( + "oklab".parse::<GradientInterpolation>().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklab, + ..Default::default() + } + ); + assert_eq!( + "oklch".parse::<GradientInterpolation>().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + ..Default::default() + } + ); + assert_eq!( + "oklch shorter hue" + .parse::<GradientInterpolation>() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Shorter, + } + ); + assert_eq!( + "oklch longer hue".parse::<GradientInterpolation>().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Longer, + } + ); + assert_eq!( + "oklch decreasing hue" + .parse::<GradientInterpolation>() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Decreasing, + } + ); + assert_eq!( + "oklch increasing hue" + .parse::<GradientInterpolation>() + |
