Skip to main content

orinium_browser/engine/css/
values.rs

1//! CSSの値を表す構造体と列挙型
2
3pub type CssIdent = smol_str::SmolStr;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum Unit {
7    Px,
8    Cm,
9    Mm,
10    In,
11    Pt,
12    Pc,
13
14    Em,
15    Rem,
16
17    Percent,
18
19    Vw,
20    Vh,
21    Vmin,
22    Vmax,
23
24    Deg,
25    Rad,
26    Grad,
27    Turn,
28
29    S,
30    Ms,
31
32    Fr,
33    Unknown,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub enum CssValue {
38    Keyword(CssIdent), // e.g. auto, none
39    Length(f32, Unit), // e.g. 10px
40    Number(f32),       // e.g. 1.5
41    String(String),    // e.g. "http"
42    Color(String),     // e.g. #fff, #1f1f11
43    /// e.g. `rgb(255,0,0)`.
44    ///
45    /// Arguments are comma-separated; within each argument, whitespace
46    /// separates individual components. The result is a two-level structure:
47    /// the outer `Vec` holds comma-separated arguments, and each inner `Vec`
48    /// holds the whitespace-separated components of that argument.
49    Function(String, Vec<Vec<CssValue>>),
50    List(Vec<CssValue>), // e.g. 100px auto
51    /// Marks a comma separator within a `CssValue::List`, preserving comma-group
52    /// structure for multi-layer properties like `background`, `font-family`,
53    /// `transition`, etc. Consumers that don't need comma grouping can skip
54    /// separators; consumers that do can split on `Separator` to get per-group
55    /// slices.
56    Separator,
57}
58
59impl CssValue {
60    /// Colorの文字列からRGBAタプルを返す
61    pub fn to_rgba_tuple(&self) -> Option<(u8, u8, u8, u8)> {
62        match self {
63            CssValue::Color(s) => parse_color(&format!("#{}", s)),
64            _ => None,
65        }
66    }
67}
68
69impl Unit {
70    pub(crate) fn as_str(self) -> &'static str {
71        match self {
72            Unit::Px => "px",
73            Unit::Cm => "cm",
74            Unit::Mm => "mm",
75            Unit::In => "in",
76            Unit::Pt => "pt",
77            Unit::Pc => "pc",
78
79            Unit::Em => "em",
80            Unit::Rem => "rem",
81
82            Unit::Percent => "%",
83
84            Unit::Vw => "vw",
85            Unit::Vh => "vh",
86            Unit::Vmin => "vmin",
87            Unit::Vmax => "vmax",
88
89            Unit::Deg => "deg",
90            Unit::Rad => "rad",
91            Unit::Grad => "grad",
92            Unit::Turn => "turn",
93
94            Unit::S => "s",
95            Unit::Ms => "ms",
96
97            Unit::Fr => "fr",
98            Unit::Unknown => "unknown",
99        }
100    }
101}
102
103/// Renders values back to their CSS source text, e.g. `10px`, `rgb(1, 2, 3)`
104/// or `100px auto`. Used by the DevTools style panels.
105impl std::fmt::Display for CssValue {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            CssValue::Keyword(keyword) => f.write_str(keyword),
109            CssValue::Length(value, unit) => write!(f, "{value}{}", unit.as_str()),
110            CssValue::Number(value) => write!(f, "{value}"),
111            CssValue::String(value) => write!(f, "\"{value}\""),
112            CssValue::Color(value) => write!(f, "#{value}"),
113            CssValue::Function(name, arguments) => {
114                let arguments = arguments
115                    .iter()
116                    .map(|argument| {
117                        argument
118                            .iter()
119                            .map(CssValue::to_string)
120                            .collect::<Vec<_>>()
121                            .join(" ")
122                    })
123                    .collect::<Vec<_>>()
124                    .join(", ");
125                write!(f, "{name}({arguments})")
126            }
127            CssValue::List(values) => {
128                let values = values
129                    .iter()
130                    .map(CssValue::to_string)
131                    .collect::<Vec<_>>()
132                    .join(" ");
133                f.write_str(&values)
134            }
135            CssValue::Separator => f.write_str(", "),
136        }
137    }
138}
139
140/// 簡易カラー文字列パーサ
141fn parse_color(s: &str) -> Option<(u8, u8, u8, u8)> {
142    let s = s.trim();
143    if let Some(hex) = s.strip_prefix('#') {
144        match hex.len() {
145            3 => {
146                // #RGB
147                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
148                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
149                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
150                Some((r, g, b, 255))
151            }
152            4 => {
153                // #RGBA
154                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
155                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
156                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
157                let a = u8::from_str_radix(&hex[3..4].repeat(2), 16).ok()?;
158                Some((r, g, b, a))
159            }
160            6 => {
161                // #RRGGBB
162                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
163                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
164                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
165                Some((r, g, b, 255))
166            }
167            8 => {
168                // #RRGGBBAA
169                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
170                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
171                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
172                let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
173                Some((r, g, b, a))
174            }
175            _ => None,
176        }
177    } else {
178        None
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn hex_invalid_returns_none() {
188        // Wrong length
189        assert_eq!(CssValue::Color("".into()).to_rgba_tuple(), None);
190        assert_eq!(CssValue::Color("a".into()).to_rgba_tuple(), None);
191        assert_eq!(CssValue::Color("aa".into()).to_rgba_tuple(), None);
192        assert_eq!(CssValue::Color("aaaaa".into()).to_rgba_tuple(), None);
193        assert_eq!(CssValue::Color("aaaaaaa".into()).to_rgba_tuple(), None);
194        assert_eq!(CssValue::Color("aaaaaaaaa".into()).to_rgba_tuple(), None);
195        // Non-hex characters (parse_color uses from_str_radix with base 16)
196        assert_eq!(CssValue::Color("zzz".into()).to_rgba_tuple(), None);
197        assert_eq!(CssValue::Color("gggggg".into()).to_rgba_tuple(), None);
198    }
199
200    #[test]
201    fn to_rgba_tuple_non_color_returns_none() {
202        assert_eq!(CssValue::Keyword("red".into()).to_rgba_tuple(), None);
203        assert_eq!(CssValue::Length(10.0, Unit::Px).to_rgba_tuple(), None);
204        assert_eq!(CssValue::Number(1.0).to_rgba_tuple(), None);
205        assert_eq!(CssValue::String("#fff".into()).to_rgba_tuple(), None);
206        assert_eq!(
207            CssValue::Function("rgb".into(), vec![]).to_rgba_tuple(),
208            None
209        );
210    }
211
212    #[test]
213    fn display_renders_css_source_text() {
214        assert_eq!(CssValue::Length(10.0, Unit::Px).to_string(), "10px");
215        assert_eq!(CssValue::Length(1.5, Unit::Percent).to_string(), "1.5%");
216        assert_eq!(CssValue::Number(0.75).to_string(), "0.75");
217        assert_eq!(CssValue::Keyword("auto".into()).to_string(), "auto");
218        assert_eq!(CssValue::Color("fff".into()).to_string(), "#fff");
219        assert_eq!(CssValue::String("a b".into()).to_string(), "\"a b\"");
220        assert_eq!(
221            CssValue::Function(
222                "rgb".into(),
223                vec![
224                    vec![CssValue::Number(255.0)],
225                    vec![CssValue::Number(0.0)],
226                    vec![CssValue::Number(0.0)],
227                ]
228            )
229            .to_string(),
230            "rgb(255, 0, 0)"
231        );
232        assert_eq!(
233            CssValue::List(vec![
234                CssValue::Length(100.0, Unit::Px),
235                CssValue::Keyword("auto".into())
236            ])
237            .to_string(),
238            "100px auto"
239        );
240    }
241}