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