Skip to main content

hjkl_theme/
color.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ThemeError;
4
5/// Resolved RGBA color (all channels 0–255).
6#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize)]
7pub struct Color {
8    pub r: u8,
9    pub g: u8,
10    pub b: u8,
11    pub a: u8,
12}
13
14impl Color {
15    /// Construct from RGB; alpha defaults to 255.
16    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
17        Self { r, g, b, a: 255 }
18    }
19
20    /// Construct from RGBA.
21    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
22        Self { r, g, b, a }
23    }
24
25    /// Parse `#rgb`, `#rrggbb`, or `#rrggbbaa`.
26    pub fn from_hex_str(s: &str) -> Result<Self, ThemeError> {
27        let s = s
28            .strip_prefix('#')
29            .ok_or_else(|| ThemeError::BadHex(s.to_owned()))?;
30        fn hex2(b: &[u8]) -> Option<u8> {
31            let hi = (b[0] as char).to_digit(16)? as u8;
32            let lo = (b[1] as char).to_digit(16)? as u8;
33            Some(hi << 4 | lo)
34        }
35        fn expand(nibble: u8) -> u8 {
36            nibble << 4 | nibble
37        }
38        match s.len() {
39            3 => {
40                let b = s.as_bytes();
41                let r = (b[0] as char).to_digit(16).map(|n| expand(n as u8));
42                let g = (b[1] as char).to_digit(16).map(|n| expand(n as u8));
43                let b_ = (b[2] as char).to_digit(16).map(|n| expand(n as u8));
44                match (r, g, b_) {
45                    (Some(r), Some(g), Some(b)) => Ok(Color::rgb(r, g, b)),
46                    _ => Err(ThemeError::BadHex(format!("#{s}"))),
47                }
48            }
49            6 => {
50                let b = s.as_bytes();
51                match (hex2(&b[0..2]), hex2(&b[2..4]), hex2(&b[4..6])) {
52                    (Some(r), Some(g), Some(b)) => Ok(Color::rgb(r, g, b)),
53                    _ => Err(ThemeError::BadHex(format!("#{s}"))),
54                }
55            }
56            8 => {
57                let b = s.as_bytes();
58                match (
59                    hex2(&b[0..2]),
60                    hex2(&b[2..4]),
61                    hex2(&b[4..6]),
62                    hex2(&b[6..8]),
63                ) {
64                    (Some(r), Some(g), Some(b), Some(a)) => Ok(Color::rgba(r, g, b, a)),
65                    _ => Err(ThemeError::BadHex(format!("#{s}"))),
66                }
67            }
68            _ => Err(ThemeError::BadHex(format!("#{s}"))),
69        }
70    }
71}
72
73/// Raw color value in TOML: either a literal hex string or a `$palette_name` ref.
74#[derive(Clone, Debug)]
75pub(crate) enum ColorRef {
76    Palette(String),
77    Literal(Color),
78}
79
80impl<'de> Deserialize<'de> for ColorRef {
81    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
82        let s = String::deserialize(d)?;
83        if let Some(name) = s.strip_prefix('$') {
84            Ok(ColorRef::Palette(name.to_owned()))
85        } else {
86            Color::from_hex_str(&s)
87                .map(ColorRef::Literal)
88                .map_err(serde::de::Error::custom)
89        }
90    }
91}
92
93/// Newtype for a hex-literal color in the palette table (no `$` refs allowed there).
94#[derive(Clone, Debug)]
95pub(crate) struct LiteralColor(pub Color);
96
97impl<'de> Deserialize<'de> for LiteralColor {
98    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
99        let s = String::deserialize(d)?;
100        Color::from_hex_str(&s)
101            .map(LiteralColor)
102            .map_err(serde::de::Error::custom)
103    }
104}
105
106impl ColorRef {
107    /// Resolve against the palette. Returns `ThemeError::UnresolvedPalette` on missing name.
108    pub(crate) fn resolve(
109        self,
110        palette: &std::collections::HashMap<String, Color>,
111    ) -> Result<Color, ThemeError> {
112        match self {
113            ColorRef::Literal(c) => Ok(c),
114            ColorRef::Palette(name) => palette
115                .get(&name)
116                .copied()
117                .ok_or(ThemeError::UnresolvedPalette(name)),
118        }
119    }
120}
121
122/// Raw palette deserialization: values are hex strings only (no `$` refs in palette itself).
123#[derive(Clone, Debug, Deserialize)]
124pub(crate) struct RawPalette(pub std::collections::HashMap<String, LiteralColor>);