Skip to main content

azul_css/props/basic/
color.rs

1//! CSS property types for color.
2
3use alloc::string::{String, ToString};
4use core::{
5    fmt,
6    num::{ParseFloatError, ParseIntError},
7};
8
9use crate::{
10    impl_option,
11    props::basic::{
12        direction::{
13            parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
14        },
15        length::{PercentageParseError, PercentageValue},
16    },
17};
18
19/// u8-based color, range 0 to 255 (similar to webrenders ColorU)
20#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
21#[repr(C)]
22pub struct ColorU {
23    pub r: u8,
24    pub g: u8,
25    pub b: u8,
26    pub a: u8,
27}
28
29impl_option!(
30    ColorU,
31    OptionColorU,
32    [Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash]
33);
34
35impl Default for ColorU {
36    fn default() -> Self {
37        ColorU::BLACK
38    }
39}
40
41impl fmt::Display for ColorU {
42    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43        write!(
44            f,
45            "rgba({}, {}, {}, {})",
46            self.r,
47            self.g,
48            self.b,
49            self.a as f32 / 255.0
50        )
51    }
52}
53
54impl ColorU {
55    pub const ALPHA_TRANSPARENT: u8 = 0;
56    pub const ALPHA_OPAQUE: u8 = 255;
57    pub const RED: ColorU = ColorU {
58        r: 255,
59        g: 0,
60        b: 0,
61        a: Self::ALPHA_OPAQUE,
62    };
63    pub const GREEN: ColorU = ColorU {
64        r: 0,
65        g: 255,
66        b: 0,
67        a: Self::ALPHA_OPAQUE,
68    };
69    pub const BLUE: ColorU = ColorU {
70        r: 0,
71        g: 0,
72        b: 255,
73        a: Self::ALPHA_OPAQUE,
74    };
75    pub const WHITE: ColorU = ColorU {
76        r: 255,
77        g: 255,
78        b: 255,
79        a: Self::ALPHA_OPAQUE,
80    };
81    pub const BLACK: ColorU = ColorU {
82        r: 0,
83        g: 0,
84        b: 0,
85        a: Self::ALPHA_OPAQUE,
86    };
87    pub const TRANSPARENT: ColorU = ColorU {
88        r: 0,
89        g: 0,
90        b: 0,
91        a: Self::ALPHA_TRANSPARENT,
92    };
93
94    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
95        Self { r, g, b, a }
96    }
97    pub const fn new_rgb(r: u8, g: u8, b: u8) -> Self {
98        Self { r, g, b, a: 255 }
99    }
100
101    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
102        Self {
103            r: libm::roundf(self.r as f32 + (other.r as f32 - self.r as f32) * t) as u8,
104            g: libm::roundf(self.g as f32 + (other.g as f32 - self.g as f32) * t) as u8,
105            b: libm::roundf(self.b as f32 + (other.b as f32 - self.b as f32) * t) as u8,
106            a: libm::roundf(self.a as f32 + (other.a as f32 - self.a as f32) * t) as u8,
107        }
108    }
109
110    pub const fn has_alpha(&self) -> bool {
111        self.a != Self::ALPHA_OPAQUE
112    }
113
114    pub fn to_hash(&self) -> String {
115        format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
116    }
117}
118
119/// f32-based color, range 0.0 to 1.0 (similar to webrenders ColorF)
120#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
121pub struct ColorF {
122    pub r: f32,
123    pub g: f32,
124    pub b: f32,
125    pub a: f32,
126}
127
128impl Default for ColorF {
129    fn default() -> Self {
130        ColorF::BLACK
131    }
132}
133
134impl fmt::Display for ColorF {
135    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136        write!(
137            f,
138            "rgba({}, {}, {}, {})",
139            self.r * 255.0,
140            self.g * 255.0,
141            self.b * 255.0,
142            self.a
143        )
144    }
145}
146
147impl ColorF {
148    pub const ALPHA_TRANSPARENT: f32 = 0.0;
149    pub const ALPHA_OPAQUE: f32 = 1.0;
150    pub const WHITE: ColorF = ColorF {
151        r: 1.0,
152        g: 1.0,
153        b: 1.0,
154        a: Self::ALPHA_OPAQUE,
155    };
156    pub const BLACK: ColorF = ColorF {
157        r: 0.0,
158        g: 0.0,
159        b: 0.0,
160        a: Self::ALPHA_OPAQUE,
161    };
162    pub const TRANSPARENT: ColorF = ColorF {
163        r: 0.0,
164        g: 0.0,
165        b: 0.0,
166        a: Self::ALPHA_TRANSPARENT,
167    };
168}
169
170impl From<ColorU> for ColorF {
171    fn from(input: ColorU) -> ColorF {
172        ColorF {
173            r: (input.r as f32) / 255.0,
174            g: (input.g as f32) / 255.0,
175            b: (input.b as f32) / 255.0,
176            a: (input.a as f32) / 255.0,
177        }
178    }
179}
180
181impl From<ColorF> for ColorU {
182    fn from(input: ColorF) -> ColorU {
183        ColorU {
184            r: (input.r.min(1.0) * 255.0) as u8,
185            g: (input.g.min(1.0) * 255.0) as u8,
186            b: (input.b.min(1.0) * 255.0) as u8,
187            a: (input.a.min(1.0) * 255.0) as u8,
188        }
189    }
190}
191
192// --- PARSER ---
193
194#[derive(Debug, Copy, Clone, PartialEq)]
195pub enum CssColorComponent {
196    Red,
197    Green,
198    Blue,
199    Hue,
200    Saturation,
201    Lightness,
202    Alpha,
203}
204
205#[derive(Clone, PartialEq)]
206pub enum CssColorParseError<'a> {
207    InvalidColor(&'a str),
208    InvalidFunctionName(&'a str),
209    InvalidColorComponent(u8),
210    IntValueParseErr(ParseIntError),
211    FloatValueParseErr(ParseFloatError),
212    FloatValueOutOfRange(f32),
213    MissingColorComponent(CssColorComponent),
214    ExtraArguments(&'a str),
215    UnclosedColor(&'a str),
216    EmptyInput,
217    DirectionParseError(CssDirectionParseError<'a>),
218    UnsupportedDirection(&'a str),
219    InvalidPercentage(PercentageParseError),
220}
221
222impl_debug_as_display!(CssColorParseError<'a>);
223impl_display! {CssColorParseError<'a>, {
224    InvalidColor(i) => format!("Invalid CSS color: \"{}\"", i),
225    InvalidFunctionName(i) => format!("Invalid function name, expected one of: \"rgb\", \"rgba\", \"hsl\", \"hsla\" got: \"{}\"", i),
226    InvalidColorComponent(i) => format!("Invalid color component when parsing CSS color: \"{}\"", i),
227    IntValueParseErr(e) => format!("CSS color component: Value not in range between 00 - FF: \"{}\"", e),
228    FloatValueParseErr(e) => format!("CSS color component: Value cannot be parsed as floating point number: \"{}\"", e),
229    FloatValueOutOfRange(v) => format!("CSS color component: Value not in range between 0.0 - 1.0: \"{}\"", v),
230    MissingColorComponent(c) => format!("CSS color is missing {:?} component", c),
231    ExtraArguments(a) => format!("Extra argument to CSS color: \"{}\"", a),
232    EmptyInput => format!("Empty color string."),
233    UnclosedColor(i) => format!("Unclosed color: \"{}\"", i),
234    DirectionParseError(e) => format!("Could not parse direction argument for CSS color: \"{}\"", e),
235    UnsupportedDirection(d) => format!("Unsupported direction type for CSS color: \"{}\"", d),
236    InvalidPercentage(p) => format!("Invalid percentage when parsing CSS color: \"{}\"", p),
237}}
238
239impl<'a> From<ParseIntError> for CssColorParseError<'a> {
240    fn from(e: ParseIntError) -> Self {
241        CssColorParseError::IntValueParseErr(e)
242    }
243}
244impl<'a> From<ParseFloatError> for CssColorParseError<'a> {
245    fn from(e: ParseFloatError) -> Self {
246        CssColorParseError::FloatValueParseErr(e)
247    }
248}
249impl_from!(
250    CssDirectionParseError<'a>,
251    CssColorParseError::DirectionParseError
252);
253
254#[derive(Debug, Clone, PartialEq)]
255pub enum CssColorParseErrorOwned {
256    InvalidColor(String),
257    InvalidFunctionName(String),
258    InvalidColorComponent(u8),
259    IntValueParseErr(ParseIntError),
260    FloatValueParseErr(ParseFloatError),
261    FloatValueOutOfRange(f32),
262    MissingColorComponent(CssColorComponent),
263    ExtraArguments(String),
264    UnclosedColor(String),
265    EmptyInput,
266    DirectionParseError(CssDirectionParseErrorOwned),
267    UnsupportedDirection(String),
268    InvalidPercentage(PercentageParseError),
269}
270
271impl<'a> CssColorParseError<'a> {
272    pub fn to_contained(&self) -> CssColorParseErrorOwned {
273        match self {
274            CssColorParseError::InvalidColor(s) => {
275                CssColorParseErrorOwned::InvalidColor(s.to_string())
276            }
277            CssColorParseError::InvalidFunctionName(s) => {
278                CssColorParseErrorOwned::InvalidFunctionName(s.to_string())
279            }
280            CssColorParseError::InvalidColorComponent(n) => {
281                CssColorParseErrorOwned::InvalidColorComponent(*n)
282            }
283            CssColorParseError::IntValueParseErr(e) => {
284                CssColorParseErrorOwned::IntValueParseErr(e.clone())
285            }
286            CssColorParseError::FloatValueParseErr(e) => {
287                CssColorParseErrorOwned::FloatValueParseErr(e.clone())
288            }
289            CssColorParseError::FloatValueOutOfRange(n) => {
290                CssColorParseErrorOwned::FloatValueOutOfRange(*n)
291            }
292            CssColorParseError::MissingColorComponent(c) => {
293                CssColorParseErrorOwned::MissingColorComponent(*c)
294            }
295            CssColorParseError::ExtraArguments(s) => {
296                CssColorParseErrorOwned::ExtraArguments(s.to_string())
297            }
298            CssColorParseError::UnclosedColor(s) => {
299                CssColorParseErrorOwned::UnclosedColor(s.to_string())
300            }
301            CssColorParseError::EmptyInput => CssColorParseErrorOwned::EmptyInput,
302            CssColorParseError::DirectionParseError(e) => {
303                CssColorParseErrorOwned::DirectionParseError(e.to_contained())
304            }
305            CssColorParseError::UnsupportedDirection(s) => {
306                CssColorParseErrorOwned::UnsupportedDirection(s.to_string())
307            }
308            CssColorParseError::InvalidPercentage(e) => {
309                CssColorParseErrorOwned::InvalidPercentage(e.clone())
310            }
311        }
312    }
313}
314
315impl CssColorParseErrorOwned {
316    pub fn to_shared<'a>(&'a self) -> CssColorParseError<'a> {
317        match self {
318            CssColorParseErrorOwned::InvalidColor(s) => CssColorParseError::InvalidColor(s),
319            CssColorParseErrorOwned::InvalidFunctionName(s) => {
320                CssColorParseError::InvalidFunctionName(s)
321            }
322            CssColorParseErrorOwned::InvalidColorComponent(n) => {
323                CssColorParseError::InvalidColorComponent(*n)
324            }
325            CssColorParseErrorOwned::IntValueParseErr(e) => {
326                CssColorParseError::IntValueParseErr(e.clone())
327            }
328            CssColorParseErrorOwned::FloatValueParseErr(e) => {
329                CssColorParseError::FloatValueParseErr(e.clone())
330            }
331            CssColorParseErrorOwned::FloatValueOutOfRange(n) => {
332                CssColorParseError::FloatValueOutOfRange(*n)
333            }
334            CssColorParseErrorOwned::MissingColorComponent(c) => {
335                CssColorParseError::MissingColorComponent(*c)
336            }
337            CssColorParseErrorOwned::ExtraArguments(s) => CssColorParseError::ExtraArguments(s),
338            CssColorParseErrorOwned::UnclosedColor(s) => CssColorParseError::UnclosedColor(s),
339            CssColorParseErrorOwned::EmptyInput => CssColorParseError::EmptyInput,
340            CssColorParseErrorOwned::DirectionParseError(e) => {
341                CssColorParseError::DirectionParseError(e.to_shared())
342            }
343            CssColorParseErrorOwned::UnsupportedDirection(s) => {
344                CssColorParseError::UnsupportedDirection(s)
345            }
346            CssColorParseErrorOwned::InvalidPercentage(e) => {
347                CssColorParseError::InvalidPercentage(e.clone())
348            }
349        }
350    }
351}
352
353#[cfg(feature = "parser")]
354pub fn parse_css_color<'a>(input: &'a str) -> Result<ColorU, CssColorParseError<'a>> {
355    let input = input.trim();
356    if input.starts_with('#') {
357        parse_color_no_hash(&input[1..])
358    } else {
359        use crate::props::basic::parse::{parse_parentheses, ParenthesisParseError};
360        match parse_parentheses(input, &["rgba", "rgb", "hsla", "hsl"]) {
361            Ok((stopword, inner_value)) => match stopword {
362                "rgba" => parse_color_rgb(inner_value, true),
363                "rgb" => parse_color_rgb(inner_value, false),
364                "hsla" => parse_color_hsl(inner_value, true),
365                "hsl" => parse_color_hsl(inner_value, false),
366                _ => unreachable!(),
367            },
368            Err(e) => match e {
369                ParenthesisParseError::UnclosedBraces => {
370                    Err(CssColorParseError::UnclosedColor(input))
371                }
372                ParenthesisParseError::EmptyInput => Err(CssColorParseError::EmptyInput),
373                ParenthesisParseError::StopWordNotFound(stopword) => {
374                    Err(CssColorParseError::InvalidFunctionName(stopword))
375                }
376                ParenthesisParseError::NoClosingBraceFound => {
377                    Err(CssColorParseError::UnclosedColor(input))
378                }
379                ParenthesisParseError::NoOpeningBraceFound => parse_color_builtin(input),
380            },
381        }
382    }
383}
384
385#[cfg(feature = "parser")]
386fn parse_color_no_hash<'a>(input: &'a str) -> Result<ColorU, CssColorParseError<'a>> {
387    #[inline]
388    fn from_hex<'a>(c: u8) -> Result<u8, CssColorParseError<'a>> {
389        match c {
390            b'0'..=b'9' => Ok(c - b'0'),
391            b'a'..=b'f' => Ok(c - b'a' + 10),
392            b'A'..=b'F' => Ok(c - b'A' + 10),
393            _ => Err(CssColorParseError::InvalidColorComponent(c)),
394        }
395    }
396
397    match input.len() {
398        3 => {
399            let mut bytes = input.bytes();
400            let r = bytes.next().unwrap();
401            let g = bytes.next().unwrap();
402            let b = bytes.next().unwrap();
403            Ok(ColorU::new_rgb(
404                from_hex(r)? * 17,
405                from_hex(g)? * 17,
406                from_hex(b)? * 17,
407            ))
408        }
409        4 => {
410            let mut bytes = input.bytes();
411            let r = bytes.next().unwrap();
412            let g = bytes.next().unwrap();
413            let b = bytes.next().unwrap();
414            let a = bytes.next().unwrap();
415            Ok(ColorU::new(
416                from_hex(r)? * 17,
417                from_hex(g)? * 17,
418                from_hex(b)? * 17,
419                from_hex(a)? * 17,
420            ))
421        }
422        6 => {
423            let val = u32::from_str_radix(input, 16)?;
424            Ok(ColorU::new_rgb(
425                ((val >> 16) & 0xFF) as u8,
426                ((val >> 8) & 0xFF) as u8,
427                (val & 0xFF) as u8,
428            ))
429        }
430        8 => {
431            let val = u32::from_str_radix(input, 16)?;
432            Ok(ColorU::new(
433                ((val >> 24) & 0xFF) as u8,
434                ((val >> 16) & 0xFF) as u8,
435                ((val >> 8) & 0xFF) as u8,
436                (val & 0xFF) as u8,
437            ))
438        }
439        _ => Err(CssColorParseError::InvalidColor(input)),
440    }
441}
442
443#[cfg(feature = "parser")]
444fn parse_color_rgb<'a>(
445    input: &'a str,
446    parse_alpha: bool,
447) -> Result<ColorU, CssColorParseError<'a>> {
448    let mut components = input.split(',').map(|c| c.trim());
449    let rgb_color = parse_color_rgb_components(&mut components)?;
450    let a = if parse_alpha {
451        parse_alpha_component(&mut components)?
452    } else {
453        255
454    };
455    if let Some(arg) = components.next() {
456        return Err(CssColorParseError::ExtraArguments(arg));
457    }
458    Ok(ColorU { a, ..rgb_color })
459}
460
461#[cfg(feature = "parser")]
462fn parse_color_rgb_components<'a>(
463    components: &mut dyn Iterator<Item = &'a str>,
464) -> Result<ColorU, CssColorParseError<'a>> {
465    #[inline]
466    fn component_from_str<'a>(
467        components: &mut dyn Iterator<Item = &'a str>,
468        which: CssColorComponent,
469    ) -> Result<u8, CssColorParseError<'a>> {
470        let c = components
471            .next()
472            .ok_or(CssColorParseError::MissingColorComponent(which))?;
473        if c.is_empty() {
474            return Err(CssColorParseError::MissingColorComponent(which));
475        }
476        Ok(c.parse::<u8>()?)
477    }
478    Ok(ColorU {
479        r: component_from_str(components, CssColorComponent::Red)?,
480        g: component_from_str(components, CssColorComponent::Green)?,
481        b: component_from_str(components, CssColorComponent::Blue)?,
482        a: 255,
483    })
484}
485
486#[cfg(feature = "parser")]
487fn parse_color_hsl<'a>(
488    input: &'a str,
489    parse_alpha: bool,
490) -> Result<ColorU, CssColorParseError<'a>> {
491    let mut components = input.split(',').map(|c| c.trim());
492    let rgb_color = parse_color_hsl_components(&mut components)?;
493    let a = if parse_alpha {
494        parse_alpha_component(&mut components)?
495    } else {
496        255
497    };
498    if let Some(arg) = components.next() {
499        return Err(CssColorParseError::ExtraArguments(arg));
500    }
501    Ok(ColorU { a, ..rgb_color })
502}
503
504#[cfg(feature = "parser")]
505fn parse_color_hsl_components<'a>(
506    components: &mut dyn Iterator<Item = &'a str>,
507) -> Result<ColorU, CssColorParseError<'a>> {
508    #[inline]
509    fn angle_from_str<'a>(
510        components: &mut dyn Iterator<Item = &'a str>,
511        which: CssColorComponent,
512    ) -> Result<f32, CssColorParseError<'a>> {
513        let c = components
514            .next()
515            .ok_or(CssColorParseError::MissingColorComponent(which))?;
516        if c.is_empty() {
517            return Err(CssColorParseError::MissingColorComponent(which));
518        }
519        let dir = parse_direction(c)?;
520        match dir {
521            Direction::Angle(deg) => Ok(deg.to_degrees()),
522            Direction::FromTo(_) => Err(CssColorParseError::UnsupportedDirection(c)),
523        }
524    }
525
526    #[inline]
527    fn percent_from_str<'a>(
528        components: &mut dyn Iterator<Item = &'a str>,
529        which: CssColorComponent,
530    ) -> Result<f32, CssColorParseError<'a>> {
531        use crate::props::basic::parse_percentage_value;
532
533        let c = components
534            .next()
535            .ok_or(CssColorParseError::MissingColorComponent(which))?;
536        if c.is_empty() {
537            return Err(CssColorParseError::MissingColorComponent(which));
538        }
539
540        // Modern CSS allows both percentage and unitless values for HSL
541        Ok(parse_percentage_value(c)
542            .map_err(CssColorParseError::InvalidPercentage)?
543            .normalized()
544            * 100.0)
545    }
546
547    #[inline]
548    fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
549        let s = s / 100.0;
550        let l = l / 100.0;
551        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
552        let h_prime = h / 60.0;
553        let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
554        let (r1, g1, b1) = if h_prime >= 0.0 && h_prime < 1.0 {
555            (c, x, 0.0)
556        } else if h_prime >= 1.0 && h_prime < 2.0 {
557            (x, c, 0.0)
558        } else if h_prime >= 2.0 && h_prime < 3.0 {
559            (0.0, c, x)
560        } else if h_prime >= 3.0 && h_prime < 4.0 {
561            (0.0, x, c)
562        } else if h_prime >= 4.0 && h_prime < 5.0 {
563            (x, 0.0, c)
564        } else {
565            (c, 0.0, x)
566        };
567        let m = l - c / 2.0;
568        (
569            ((r1 + m) * 255.0) as u8,
570            ((g1 + m) * 255.0) as u8,
571            ((b1 + m) * 255.0) as u8,
572        )
573    }
574
575    let (h, s, l) = (
576        angle_from_str(components, CssColorComponent::Hue)?,
577        percent_from_str(components, CssColorComponent::Saturation)?,
578        percent_from_str(components, CssColorComponent::Lightness)?,
579    );
580
581    let (r, g, b) = hsl_to_rgb(h, s, l);
582    Ok(ColorU { r, g, b, a: 255 })
583}
584
585#[cfg(feature = "parser")]
586fn parse_alpha_component<'a>(
587    components: &mut dyn Iterator<Item = &'a str>,
588) -> Result<u8, CssColorParseError<'a>> {
589    let a_str = components
590        .next()
591        .ok_or(CssColorParseError::MissingColorComponent(
592            CssColorComponent::Alpha,
593        ))?;
594    if a_str.is_empty() {
595        return Err(CssColorParseError::MissingColorComponent(
596            CssColorComponent::Alpha,
597        ));
598    }
599    let a = a_str.parse::<f32>()?;
600    if a < 0.0 || a > 1.0 {
601        return Err(CssColorParseError::FloatValueOutOfRange(a));
602    }
603    Ok((a * 255.0).round() as u8)
604}
605
606#[cfg(feature = "parser")]
607fn parse_color_builtin<'a>(input: &'a str) -> Result<ColorU, CssColorParseError<'a>> {
608    let (r, g, b, a) = match input.to_lowercase().as_str() {
609        "aliceblue" => (240, 248, 255, 255),
610        "antiquewhite" => (250, 235, 215, 255),
611        "aqua" => (0, 255, 255, 255),
612        "aquamarine" => (127, 255, 212, 255),
613        "azure" => (240, 255, 255, 255),
614        "beige" => (245, 245, 220, 255),
615        "bisque" => (255, 228, 196, 255),
616        "black" => (0, 0, 0, 255),
617        "blanchedalmond" => (255, 235, 205, 255),
618        "blue" => (0, 0, 255, 255),
619        "blueviolet" => (138, 43, 226, 255),
620        "brown" => (165, 42, 42, 255),
621        "burlywood" => (222, 184, 135, 255),
622        "cadetblue" => (95, 158, 160, 255),
623        "chartreuse" => (127, 255, 0, 255),
624        "chocolate" => (210, 105, 30, 255),
625        "coral" => (255, 127, 80, 255),
626        "cornflowerblue" => (100, 149, 237, 255),
627        "cornsilk" => (255, 248, 220, 255),
628        "crimson" => (220, 20, 60, 255),
629        "cyan" => (0, 255, 255, 255),
630        "darkblue" => (0, 0, 139, 255),
631        "darkcyan" => (0, 139, 139, 255),
632        "darkgoldenrod" => (184, 134, 11, 255),
633        "darkgray" | "darkgrey" => (169, 169, 169, 255),
634        "darkgreen" => (0, 100, 0, 255),
635        "darkkhaki" => (189, 183, 107, 255),
636        "darkmagenta" => (139, 0, 139, 255),
637        "darkolivegreen" => (85, 107, 47, 255),
638        "darkorange" => (255, 140, 0, 255),
639        "darkorchid" => (153, 50, 204, 255),
640        "darkred" => (139, 0, 0, 255),
641        "darksalmon" => (233, 150, 122, 255),
642        "darkseagreen" => (143, 188, 143, 255),
643        "darkslateblue" => (72, 61, 139, 255),
644        "darkslategray" | "darkslategrey" => (47, 79, 79, 255),
645        "darkturquoise" => (0, 206, 209, 255),
646        "darkviolet" => (148, 0, 211, 255),
647        "deeppink" => (255, 20, 147, 255),
648        "deepskyblue" => (0, 191, 255, 255),
649        "dimgray" | "dimgrey" => (105, 105, 105, 255),
650        "dodgerblue" => (30, 144, 255, 255),
651        "firebrick" => (178, 34, 34, 255),
652        "floralwhite" => (255, 250, 240, 255),
653        "forestgreen" => (34, 139, 34, 255),
654        "fuchsia" => (255, 0, 255, 255),
655        "gainsboro" => (220, 220, 220, 255),
656        "ghostwhite" => (248, 248, 255, 255),
657        "gold" => (255, 215, 0, 255),
658        "goldenrod" => (218, 165, 32, 255),
659        "gray" | "grey" => (128, 128, 128, 255),
660        "green" => (0, 128, 0, 255),
661        "greenyellow" => (173, 255, 47, 255),
662        "honeydew" => (240, 255, 240, 255),
663        "hotpink" => (255, 105, 180, 255),
664        "indianred" => (205, 92, 92, 255),
665        "indigo" => (75, 0, 130, 255),
666        "ivory" => (255, 255, 240, 255),
667        "khaki" => (240, 230, 140, 255),
668        "lavender" => (230, 230, 250, 255),
669        "lavenderblush" => (255, 240, 245, 255),
670        "lawngreen" => (124, 252, 0, 255),
671        "lemonchiffon" => (255, 250, 205, 255),
672        "lightblue" => (173, 216, 230, 255),
673        "lightcoral" => (240, 128, 128, 255),
674        "lightcyan" => (224, 255, 255, 255),
675        "lightgoldenrodyellow" => (250, 250, 210, 255),
676        "lightgray" | "lightgrey" => (211, 211, 211, 255),
677        "lightgreen" => (144, 238, 144, 255),
678        "lightpink" => (255, 182, 193, 255),
679        "lightsalmon" => (255, 160, 122, 255),
680        "lightseagreen" => (32, 178, 170, 255),
681        "lightskyblue" => (135, 206, 250, 255),
682        "lightslategray" | "lightslategrey" => (119, 136, 153, 255),
683        "lightsteelblue" => (176, 196, 222, 255),
684        "lightyellow" => (255, 255, 224, 255),
685        "lime" => (0, 255, 0, 255),
686        "limegreen" => (50, 205, 50, 255),
687        "linen" => (250, 240, 230, 255),
688        "magenta" => (255, 0, 255, 255),
689        "maroon" => (128, 0, 0, 255),
690        "mediumaquamarine" => (102, 205, 170, 255),
691        "mediumblue" => (0, 0, 205, 255),
692        "mediumorchid" => (186, 85, 211, 255),
693        "mediumpurple" => (147, 112, 219, 255),
694        "mediumseagreen" => (60, 179, 113, 255),
695        "mediumslateblue" => (123, 104, 238, 255),
696        "mediumspringgreen" => (0, 250, 154, 255),
697        "mediumturquoise" => (72, 209, 204, 255),
698        "mediumvioletred" => (199, 21, 133, 255),
699        "midnightblue" => (25, 25, 112, 255),
700        "mintcream" => (245, 255, 250, 255),
701        "mistyrose" => (255, 228, 225, 255),
702        "moccasin" => (255, 228, 181, 255),
703        "navajowhite" => (255, 222, 173, 255),
704        "navy" => (0, 0, 128, 255),
705        "oldlace" => (253, 245, 230, 255),
706        "olive" => (128, 128, 0, 255),
707        "olivedrab" => (107, 142, 35, 255),
708        "orange" => (255, 165, 0, 255),
709        "orangered" => (255, 69, 0, 255),
710        "orchid" => (218, 112, 214, 255),
711        "palegoldenrod" => (238, 232, 170, 255),
712        "palegreen" => (152, 251, 152, 255),
713        "paleturquoise" => (175, 238, 238, 255),
714        "palevioletred" => (219, 112, 147, 255),
715        "papayawhip" => (255, 239, 213, 255),
716        "peachpuff" => (255, 218, 185, 255),
717        "peru" => (205, 133, 63, 255),
718        "pink" => (255, 192, 203, 255),
719        "plum" => (221, 160, 221, 255),
720        "powderblue" => (176, 224, 230, 255),
721        "purple" => (128, 0, 128, 255),
722        "rebeccapurple" => (102, 51, 153, 255),
723        "red" => (255, 0, 0, 255),
724        "rosybrown" => (188, 143, 143, 255),
725        "royalblue" => (65, 105, 225, 255),
726        "saddlebrown" => (139, 69, 19, 255),
727        "salmon" => (250, 128, 114, 255),
728        "sandybrown" => (244, 164, 96, 255),
729        "seagreen" => (46, 139, 87, 255),
730        "seashell" => (255, 245, 238, 255),
731        "sienna" => (160, 82, 45, 255),
732        "silver" => (192, 192, 192, 255),
733        "skyblue" => (135, 206, 235, 255),
734        "slateblue" => (106, 90, 205, 255),
735        "slategray" | "slategrey" => (112, 128, 144, 255),
736        "snow" => (255, 250, 250, 255),
737        "springgreen" => (0, 255, 127, 255),
738        "steelblue" => (70, 130, 180, 255),
739        "tan" => (210, 180, 140, 255),
740        "teal" => (0, 128, 128, 255),
741        "thistle" => (216, 191, 216, 255),
742        "tomato" => (255, 99, 71, 255),
743        "transparent" => (0, 0, 0, 0),
744        "turquoise" => (64, 224, 208, 255),
745        "violet" => (238, 130, 238, 255),
746        "wheat" => (245, 222, 179, 255),
747        "white" => (255, 255, 255, 255),
748        "whitesmoke" => (245, 245, 245, 255),
749        "yellow" => (255, 255, 0, 255),
750        "yellowgreen" => (154, 205, 50, 255),
751        _ => return Err(CssColorParseError::InvalidColor(input)),
752    };
753    Ok(ColorU { r, g, b, a })
754}
755
756#[cfg(all(test, feature = "parser"))]
757mod tests {
758    use super::*;
759
760    #[test]
761    fn test_parse_color_keywords() {
762        assert_eq!(parse_css_color("red").unwrap(), ColorU::RED);
763        assert_eq!(parse_css_color("blue").unwrap(), ColorU::BLUE);
764        assert_eq!(parse_css_color("transparent").unwrap(), ColorU::TRANSPARENT);
765        assert_eq!(
766            parse_css_color("rebeccapurple").unwrap(),
767            ColorU::new_rgb(102, 51, 153)
768        );
769    }
770
771    #[test]
772    fn test_parse_color_hex() {
773        // 3-digit
774        assert_eq!(parse_css_color("#f00").unwrap(), ColorU::RED);
775        // 4-digit
776        assert_eq!(
777            parse_css_color("#f008").unwrap(),
778            ColorU::new(255, 0, 0, 136)
779        );
780        // 6-digit
781        assert_eq!(parse_css_color("#00ff00").unwrap(), ColorU::GREEN);
782        // 8-digit
783        assert_eq!(
784            parse_css_color("#0000ff80").unwrap(),
785            ColorU::new(0, 0, 255, 128)
786        );
787        // Uppercase
788        assert_eq!(
789            parse_css_color("#FFC0CB").unwrap(),
790            ColorU::new_rgb(255, 192, 203)
791        ); // Pink
792    }
793
794    #[test]
795    fn test_parse_color_rgb() {
796        assert_eq!(parse_css_color("rgb(255, 0, 0)").unwrap(), ColorU::RED);
797        assert_eq!(
798            parse_css_color("rgba(0, 255, 0, 0.5)").unwrap(),
799            ColorU::new(0, 255, 0, 128)
800        );
801        assert_eq!(
802            parse_css_color("rgba(10, 20, 30, 1)").unwrap(),
803            ColorU::new_rgb(10, 20, 30)
804        );
805        assert_eq!(parse_css_color("rgb( 0 , 0 , 0 )").unwrap(), ColorU::BLACK);
806    }
807
808    #[test]
809    fn test_parse_color_hsl() {
810        assert_eq!(parse_css_color("hsl(0, 100%, 50%)").unwrap(), ColorU::RED);
811        assert_eq!(
812            parse_css_color("hsl(120, 100%, 50%)").unwrap(),
813            ColorU::GREEN
814        );
815        assert_eq!(
816            parse_css_color("hsla(240, 100%, 50%, 0.5)").unwrap(),
817            ColorU::new(0, 0, 255, 128)
818        );
819        assert_eq!(parse_css_color("hsl(0, 0%, 0%)").unwrap(), ColorU::BLACK);
820    }
821
822    #[test]
823    fn test_parse_color_errors() {
824        assert!(parse_css_color("redd").is_err());
825        assert!(parse_css_color("#12345").is_err()); // Invalid length
826        assert!(parse_css_color("#ggg").is_err()); // Invalid hex digit
827        assert!(parse_css_color("rgb(255, 0)").is_err()); // Missing component
828        assert!(parse_css_color("rgba(255, 0, 0, 2)").is_err()); // Alpha out of range
829        assert!(parse_css_color("rgb(256, 0, 0)").is_err()); // Value out of range
830                                                             // Modern CSS allows both hsl(0, 100%, 50%) and hsl(0 100 50)
831        assert!(parse_css_color("hsl(0, 100, 50%)").is_ok()); // Valid in modern CSS
832        assert!(parse_css_color("rgb(255 0 0)").is_err()); // Missing commas (this implementation
833                                                           // requires commas)
834    }
835}