Skip to main content

azul_css/props/basic/
color.rs

1//! CSS color types and parser.
2//!
3//! Core types: [`ColorU`] (u8 RGBA), [`ColorF`] (f32 RGBA), [`ColorOrSystem`]
4//! (concrete color or runtime system-theme reference). The parser supports hex,
5//! `rgb()`/`rgba()`, `hsl()`/`hsla()`, CSS named colors, and `system:*` syntax.
6
7use alloc::string::{String, ToString};
8use core::fmt;
9use crate::corety::AzString;
10use crate::props::basic::error::{ParseFloatError, ParseIntError};
11
12use crate::{
13    impl_option,
14    props::basic::{
15        direction::{
16            parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
17        },
18        length::{PercentageParseError, PercentageValue},
19    },
20};
21
22/// Round-saturating `f32` → `u8` for colour channels. Rust's `as u8` already
23/// saturates a float (NaN→0, negatives→0, >255→255, otherwise truncates toward
24/// zero), so this is behaviour-preserving; it just names the intent and isolates
25/// the one unavoidable float→int cast (there is no infallible `f32`→`u8` in std).
26#[inline]
27#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
28const fn channel_to_u8(v: f32) -> u8 {
29    v as u8
30}
31
32/// u8-based color, range 0 to 255 (similar to webrenders `ColorU`)
33#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
34#[repr(C)]
35pub struct ColorU {
36    pub r: u8,
37    pub g: u8,
38    pub b: u8,
39    pub a: u8,
40}
41
42impl_option!(
43    ColorU,
44    OptionColorU,
45    [Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash]
46);
47
48impl Default for ColorU {
49    fn default() -> Self {
50        Self::BLACK
51    }
52}
53
54impl fmt::Display for ColorU {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(
57            f,
58            "rgba({}, {}, {}, {})",
59            self.r,
60            self.g,
61            self.b,
62            f32::from(self.a) / 255.0
63        )
64    }
65}
66
67// Colour math keeps explicit `a*b + c` rather than `mul_add`: the latter is a
68// software `fmaf` (slower) without target `+fma` and changes results bit-for-bit.
69#[allow(clippy::suboptimal_flops)]
70impl ColorU {
71    pub const ALPHA_TRANSPARENT: u8 = 0;
72    pub const ALPHA_OPAQUE: u8 = 255;
73    pub const RED: Self = Self {
74        r: 255,
75        g: 0,
76        b: 0,
77        a: Self::ALPHA_OPAQUE,
78    };
79    pub const GREEN: Self = Self {
80        r: 0,
81        g: 255,
82        b: 0,
83        a: Self::ALPHA_OPAQUE,
84    };
85    pub const BLUE: Self = Self {
86        r: 0,
87        g: 0,
88        b: 255,
89        a: Self::ALPHA_OPAQUE,
90    };
91    pub const WHITE: Self = Self {
92        r: 255,
93        g: 255,
94        b: 255,
95        a: Self::ALPHA_OPAQUE,
96    };
97    pub const BLACK: Self = Self {
98        r: 0,
99        g: 0,
100        b: 0,
101        a: Self::ALPHA_OPAQUE,
102    };
103    pub const TRANSPARENT: Self = Self {
104        r: 0,
105        g: 0,
106        b: 0,
107        a: Self::ALPHA_TRANSPARENT,
108    };
109
110    // Additional common colors
111    pub const YELLOW: Self = Self { r: 255, g: 255, b: 0, a: Self::ALPHA_OPAQUE };
112    pub const CYAN: Self = Self { r: 0, g: 255, b: 255, a: Self::ALPHA_OPAQUE };
113    pub const MAGENTA: Self = Self { r: 255, g: 0, b: 255, a: Self::ALPHA_OPAQUE };
114    pub const ORANGE: Self = Self { r: 255, g: 165, b: 0, a: Self::ALPHA_OPAQUE };
115    pub const PINK: Self = Self { r: 255, g: 192, b: 203, a: Self::ALPHA_OPAQUE };
116    pub const PURPLE: Self = Self { r: 128, g: 0, b: 128, a: Self::ALPHA_OPAQUE };
117    pub const BROWN: Self = Self { r: 139, g: 69, b: 19, a: Self::ALPHA_OPAQUE };
118    pub const GRAY: Self = Self { r: 128, g: 128, b: 128, a: Self::ALPHA_OPAQUE };
119    pub const LIGHT_GRAY: Self = Self { r: 211, g: 211, b: 211, a: Self::ALPHA_OPAQUE };
120    pub const DARK_GRAY: Self = Self { r: 64, g: 64, b: 64, a: Self::ALPHA_OPAQUE };
121    pub const NAVY: Self = Self { r: 0, g: 0, b: 128, a: Self::ALPHA_OPAQUE };
122    pub const TEAL: Self = Self { r: 0, g: 128, b: 128, a: Self::ALPHA_OPAQUE };
123    pub const OLIVE: Self = Self { r: 128, g: 128, b: 0, a: Self::ALPHA_OPAQUE };
124    pub const MAROON: Self = Self { r: 128, g: 0, b: 0, a: Self::ALPHA_OPAQUE };
125    pub const LIME: Self = Self { r: 0, g: 255, b: 0, a: Self::ALPHA_OPAQUE };
126    pub const AQUA: Self = Self { r: 0, g: 255, b: 255, a: Self::ALPHA_OPAQUE };
127    pub const SILVER: Self = Self { r: 192, g: 192, b: 192, a: Self::ALPHA_OPAQUE };
128    pub const FUCHSIA: Self = Self { r: 255, g: 0, b: 255, a: Self::ALPHA_OPAQUE };
129    pub const INDIGO: Self = Self { r: 75, g: 0, b: 130, a: Self::ALPHA_OPAQUE };
130    pub const GOLD: Self = Self { r: 255, g: 215, b: 0, a: Self::ALPHA_OPAQUE };
131    pub const CORAL: Self = Self { r: 255, g: 127, b: 80, a: Self::ALPHA_OPAQUE };
132    pub const SALMON: Self = Self { r: 250, g: 128, b: 114, a: Self::ALPHA_OPAQUE };
133    pub const TURQUOISE: Self = Self { r: 64, g: 224, b: 208, a: Self::ALPHA_OPAQUE };
134    pub const VIOLET: Self = Self { r: 238, g: 130, b: 238, a: Self::ALPHA_OPAQUE };
135    pub const CRIMSON: Self = Self { r: 220, g: 20, b: 60, a: Self::ALPHA_OPAQUE };
136    pub const CHOCOLATE: Self = Self { r: 210, g: 105, b: 30, a: Self::ALPHA_OPAQUE };
137    pub const SKY_BLUE: Self = Self { r: 135, g: 206, b: 235, a: Self::ALPHA_OPAQUE };
138    pub const FOREST_GREEN: Self = Self { r: 34, g: 139, b: 34, a: Self::ALPHA_OPAQUE };
139    pub const SEA_GREEN: Self = Self { r: 46, g: 139, b: 87, a: Self::ALPHA_OPAQUE };
140    pub const SLATE_GRAY: Self = Self { r: 112, g: 128, b: 144, a: Self::ALPHA_OPAQUE };
141    pub const MIDNIGHT_BLUE: Self = Self { r: 25, g: 25, b: 112, a: Self::ALPHA_OPAQUE };
142    pub const DARK_RED: Self = Self { r: 139, g: 0, b: 0, a: Self::ALPHA_OPAQUE };
143    pub const DARK_GREEN: Self = Self { r: 0, g: 100, b: 0, a: Self::ALPHA_OPAQUE };
144    pub const DARK_BLUE: Self = Self { r: 0, g: 0, b: 139, a: Self::ALPHA_OPAQUE };
145    pub const LIGHT_BLUE: Self = Self { r: 173, g: 216, b: 230, a: Self::ALPHA_OPAQUE };
146    pub const LIGHT_GREEN: Self = Self { r: 144, g: 238, b: 144, a: Self::ALPHA_OPAQUE };
147    pub const LIGHT_YELLOW: Self = Self { r: 255, g: 255, b: 224, a: Self::ALPHA_OPAQUE };
148    pub const LIGHT_PINK: Self = Self { r: 255, g: 182, b: 193, a: Self::ALPHA_OPAQUE };
149
150    // Constructor functions for C API (become AzColorU_red(), AzColorU_cyan(), etc.)
151    #[must_use] pub const fn red() -> Self { Self::RED }
152    #[must_use] pub const fn green() -> Self { Self::GREEN }
153    #[must_use] pub const fn blue() -> Self { Self::BLUE }
154    #[must_use] pub const fn white() -> Self { Self::WHITE }
155    #[must_use] pub const fn black() -> Self { Self::BLACK }
156    #[must_use] pub const fn transparent() -> Self { Self::TRANSPARENT }
157    #[must_use] pub const fn yellow() -> Self { Self::YELLOW }
158    #[must_use] pub const fn cyan() -> Self { Self::CYAN }
159    #[must_use] pub const fn magenta() -> Self { Self::MAGENTA }
160    #[must_use] pub const fn orange() -> Self { Self::ORANGE }
161    #[must_use] pub const fn pink() -> Self { Self::PINK }
162    #[must_use] pub const fn purple() -> Self { Self::PURPLE }
163    #[must_use] pub const fn brown() -> Self { Self::BROWN }
164    #[must_use] pub const fn gray() -> Self { Self::GRAY }
165    #[must_use] pub const fn light_gray() -> Self { Self::LIGHT_GRAY }
166    #[must_use] pub const fn dark_gray() -> Self { Self::DARK_GRAY }
167    #[must_use] pub const fn navy() -> Self { Self::NAVY }
168    #[must_use] pub const fn teal() -> Self { Self::TEAL }
169    #[must_use] pub const fn olive() -> Self { Self::OLIVE }
170    #[must_use] pub const fn maroon() -> Self { Self::MAROON }
171    #[must_use] pub const fn lime() -> Self { Self::LIME }
172    #[must_use] pub const fn aqua() -> Self { Self::AQUA }
173    #[must_use] pub const fn silver() -> Self { Self::SILVER }
174    #[must_use] pub const fn fuchsia() -> Self { Self::FUCHSIA }
175    #[must_use] pub const fn indigo() -> Self { Self::INDIGO }
176    #[must_use] pub const fn gold() -> Self { Self::GOLD }
177    #[must_use] pub const fn coral() -> Self { Self::CORAL }
178    #[must_use] pub const fn salmon() -> Self { Self::SALMON }
179    #[must_use] pub const fn turquoise() -> Self { Self::TURQUOISE }
180    #[must_use] pub const fn violet() -> Self { Self::VIOLET }
181    #[must_use] pub const fn crimson() -> Self { Self::CRIMSON }
182    #[must_use] pub const fn chocolate() -> Self { Self::CHOCOLATE }
183    #[must_use] pub const fn sky_blue() -> Self { Self::SKY_BLUE }
184    #[must_use] pub const fn forest_green() -> Self { Self::FOREST_GREEN }
185    #[must_use] pub const fn sea_green() -> Self { Self::SEA_GREEN }
186    #[must_use] pub const fn slate_gray() -> Self { Self::SLATE_GRAY }
187    #[must_use] pub const fn midnight_blue() -> Self { Self::MIDNIGHT_BLUE }
188    #[must_use] pub const fn dark_red() -> Self { Self::DARK_RED }
189    #[must_use] pub const fn dark_green() -> Self { Self::DARK_GREEN }
190    #[must_use] pub const fn dark_blue() -> Self { Self::DARK_BLUE }
191    #[must_use] pub const fn light_blue() -> Self { Self::LIGHT_BLUE }
192    #[must_use] pub const fn light_green() -> Self { Self::LIGHT_GREEN }
193    #[must_use] pub const fn light_yellow() -> Self { Self::LIGHT_YELLOW }
194    #[must_use] pub const fn light_pink() -> Self { Self::LIGHT_PINK }
195
196    /// Creates a new color with RGBA values.
197    #[must_use] pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
198        Self { r, g, b, a }
199    }
200    /// Creates a new color with RGB values (alpha = 255).
201    #[must_use] pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
202        Self { r, g, b, a: 255 }
203    }
204    /// Alias for `rgba` - kept for internal compatibility, not exposed in FFI.
205    #[inline]
206    #[must_use] pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
207        Self::rgba(r, g, b, a)
208    }
209    /// Alias for `rgb` - kept for internal compatibility, not exposed in FFI.
210    #[inline]
211    #[must_use] pub const fn new_rgb(r: u8, g: u8, b: u8) -> Self {
212        Self::rgb(r, g, b)
213    }
214
215    /// Linearly interpolate all four RGBA channels between `self` and `other`.
216    /// `t = 0.0` returns `self`, `t = 1.0` returns `other`.
217    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
218        Self {
219            r: channel_to_u8(libm::roundf(f32::from(self.r) + (f32::from(other.r) - f32::from(self.r)) * t)),
220            g: channel_to_u8(libm::roundf(f32::from(self.g) + (f32::from(other.g) - f32::from(self.g)) * t)),
221            b: channel_to_u8(libm::roundf(f32::from(self.b) + (f32::from(other.b) - f32::from(self.b)) * t)),
222            a: channel_to_u8(libm::roundf(f32::from(self.a) + (f32::from(other.a) - f32::from(self.a)) * t)),
223        }
224    }
225    
226    /// Lighten a color by a percentage (0.0 to 1.0).
227    /// Returns a new color blended towards white, preserving the original alpha.
228    #[must_use] pub fn lighten(&self, amount: f32) -> Self {
229        let mut c = self.interpolate(&Self::WHITE, amount.clamp(0.0, 1.0));
230        c.a = self.a;
231        c
232    }
233
234    /// Darken a color by a percentage (0.0 to 1.0).
235    /// Returns a new color blended towards black, preserving the original alpha.
236    #[must_use] pub fn darken(&self, amount: f32) -> Self {
237        let mut c = self.interpolate(&Self::BLACK, amount.clamp(0.0, 1.0));
238        c.a = self.a;
239        c
240    }
241    
242    /// Mix two colors together with a given ratio (0.0 = self, 1.0 = other).
243    #[must_use] pub fn mix(&self, other: &Self, ratio: f32) -> Self {
244        self.interpolate(other, ratio.clamp(0.0, 1.0))
245    }
246    
247    /// Create a hover variant (slightly lighter for dark colors, darker for light colors).
248    /// This is useful for button hover states.
249    #[must_use] pub fn hover_variant(&self) -> Self {
250        let luminance = self.relative_luminance();
251        if luminance > 0.5 {
252            self.darken(0.08)
253        } else {
254            self.lighten(0.12)
255        }
256    }
257
258    /// Create an active/pressed variant (darker than hover).
259    /// This is useful for button active states.
260    #[must_use] pub fn active_variant(&self) -> Self {
261        let luminance = self.relative_luminance();
262        if luminance > 0.5 {
263            self.darken(0.15)
264        } else {
265            self.lighten(0.05)
266        }
267    }
268    
269    /// Calculate approximate luminance (0.0 = black, 1.0 = white).
270    ///
271    /// **Note:** This applies BT.709 coefficients directly to gamma-encoded sRGB
272    /// values without linearizing first, so it is only an approximation.
273    /// For accurate results (e.g. WCAG contrast checks), use [`relative_luminance()`].
274    #[must_use] pub fn luminance(&self) -> f32 {
275        let r = f32::from(self.r) / 255.0;
276        let g = f32::from(self.g) / 255.0;
277        let b = f32::from(self.b) / 255.0;
278        0.2126 * r + 0.7152 * g + 0.0722 * b
279    }
280
281    /// Returns white or black text color for best contrast on this background.
282    #[must_use] pub fn contrast_text(&self) -> Self {
283        self.best_contrast_text()
284    }
285    
286    // ============================================================
287    // WCAG Accessibility and Contrast Helpers
288    // Based on W3C WCAG 2.1 guidelines and Chromium research
289    // ============================================================
290    
291    /// Converts a single sRGB channel to linear RGB.
292    /// Used for accurate luminance and contrast calculations.
293    fn srgb_to_linear(c: f32) -> f32 {
294        if c <= 0.03928 {
295            c / 12.92
296        } else {
297            libm::powf((c + 0.055) / 1.055, 2.4)
298        }
299    }
300    
301    /// Calculate relative luminance per WCAG 2.1 specification.
302    /// Returns a value between 0.0 (darkest) and 1.0 (lightest).
303    /// Uses the sRGB to linear conversion for accurate results.
304    #[must_use] pub fn relative_luminance(&self) -> f32 {
305        let r = Self::srgb_to_linear(f32::from(self.r) / 255.0);
306        let g = Self::srgb_to_linear(f32::from(self.g) / 255.0);
307        let b = Self::srgb_to_linear(f32::from(self.b) / 255.0);
308        0.2126 * r + 0.7152 * g + 0.0722 * b
309    }
310    
311    /// Calculate the contrast ratio between this color and another.
312    /// Returns a value between 1.0 (no contrast) and 21.0 (max contrast).
313    /// 
314    /// WCAG 2.1 requirements:
315    /// - AA normal text: >= 4.5:1
316    /// - AA large text: >= 3.0:1
317    /// - AAA normal text: >= 7.0:1
318    /// - AAA large text: >= 4.5:1
319    #[must_use] pub fn contrast_ratio(&self, other: &Self) -> f32 {
320        let l1 = self.relative_luminance();
321        let l2 = other.relative_luminance();
322        let lighter = if l1 > l2 { l1 } else { l2 };
323        let darker = if l1 > l2 { l2 } else { l1 };
324        (lighter + 0.05) / (darker + 0.05)
325    }
326    
327    /// Check if the contrast ratio meets WCAG AA requirements for normal text (>= 4.5:1).
328    #[must_use] pub fn meets_wcag_aa(&self, other: &Self) -> bool {
329        self.contrast_ratio(other) >= 4.5
330    }
331    
332    /// Check if the contrast ratio meets WCAG AA requirements for large text (>= 3.0:1).
333    /// Large text is defined as 18pt+ or 14pt+ bold.
334    #[must_use] pub fn meets_wcag_aa_large(&self, other: &Self) -> bool {
335        self.contrast_ratio(other) >= 3.0
336    }
337    
338    /// Check if the contrast ratio meets WCAG AAA requirements for normal text (>= 7.0:1).
339    #[must_use] pub fn meets_wcag_aaa(&self, other: &Self) -> bool {
340        self.contrast_ratio(other) >= 7.0
341    }
342    
343    /// Check if the contrast ratio meets WCAG AAA requirements for large text (>= 4.5:1).
344    #[must_use] pub fn meets_wcag_aaa_large(&self, other: &Self) -> bool {
345        self.contrast_ratio(other) >= 4.5
346    }
347    
348    /// Returns true if this color is considered "light" (relative luminance > 0.5).
349    /// Useful for determining if dark or light text should be used.
350    #[must_use] pub fn is_light(&self) -> bool {
351        self.relative_luminance() > 0.5
352    }
353
354    /// Returns true if this color is considered "dark" (relative luminance <= 0.5).
355    #[must_use] pub fn is_dark(&self) -> bool {
356        self.relative_luminance() <= 0.5
357    }
358    
359    /// Suggest the best text color (black or white) for this background,
360    /// ensuring WCAG AA compliance for normal text.
361    /// 
362    /// If neither black nor white meets AA requirements (unlikely), 
363    /// returns the one with higher contrast.
364    #[must_use] pub fn best_contrast_text(&self) -> Self {
365        let white_contrast = self.contrast_ratio(&Self::WHITE);
366        let black_contrast = self.contrast_ratio(&Self::BLACK);
367        
368        if white_contrast >= black_contrast {
369            Self::WHITE
370        } else {
371            Self::BLACK
372        }
373    }
374    
375    /// Adjust the color to ensure it meets the minimum contrast ratio against a background.
376    /// Lightens or darkens the color as needed.
377    /// 
378    /// Returns the original color if it already meets the requirement,
379    /// otherwise returns an adjusted color that meets the minimum contrast.
380    #[must_use] pub fn ensure_contrast(&self, background: &Self, min_ratio: f32) -> Self {
381        let current_ratio = self.contrast_ratio(background);
382        if current_ratio >= min_ratio {
383            return *self;
384        }
385        
386        // Determine if we should lighten or darken
387        let bg_luminance = background.relative_luminance();
388        let should_lighten = bg_luminance < 0.5;
389        
390        // Binary search for the right amount
391        let mut low = 0.0f32;
392        let mut high = 1.0f32;
393        let mut result = *self;
394        
395        for _ in 0..16 {
396            let mid = f32::midpoint(low, high);
397            let candidate = if should_lighten {
398                self.lighten(mid)
399            } else {
400                self.darken(mid)
401            };
402            
403            if candidate.contrast_ratio(background) >= min_ratio {
404                result = candidate;
405                high = mid;
406            } else {
407                low = mid;
408            }
409        }
410        
411        result
412    }
413    
414    /// Calculate the APCA (Accessible Perceptual Contrast Algorithm) contrast.
415    /// This is a newer algorithm that may replace WCAG contrast in future standards.
416    /// Returns a value between -108 (white on black) and 106 (black on white).
417    ///
418    /// **Note:** This is an approximation — it reuses the WCAG piecewise sRGB
419    /// linearization and BT.709 luminance coefficients rather than the APCA-specific
420    /// TRC exponents and coefficients from the full 0.0.98G specification.
421    ///
422    /// The sign indicates polarity (negative = light text on dark bg).
423    /// For most purposes, use the absolute value.
424    #[must_use] pub fn apca_contrast(&self, background: &Self) -> f32 {
425        // APCA 0.0.98G constants
426        const NORMBLKTXT: f32 = 0.56;
427        const NORMWHT: f32 = 0.57;
428        const REVTXT: f32 = 0.62;
429        const REVWHT: f32 = 0.65;
430        const BLKTHRS: f32 = 0.022;
431        const SCALEBLKT: f32 = 1.414;
432        const SCALEWHT: f32 = 1.14;
433
434        // Convert to Y (luminance) using sRGB TRC
435        let text_y = self.relative_luminance();
436        let bg_y = background.relative_luminance();
437        
438        // Soft clamp
439        let text_y = if text_y < 0.0 { 0.0 } else { text_y };
440        let bg_y = if bg_y < 0.0 { 0.0 } else { bg_y };
441        
442        
443        // Clamp black levels
444        let txt_clamp = if text_y < BLKTHRS { 
445            text_y + libm::powf(BLKTHRS - text_y, SCALEBLKT)
446        } else { 
447            text_y 
448        };
449        let bg_clamp = if bg_y < BLKTHRS { 
450            bg_y + libm::powf(BLKTHRS - bg_y, SCALEBLKT)
451        } else { 
452            bg_y 
453        };
454        
455        // Calculate contrast
456        if bg_clamp > txt_clamp {
457            // Dark text on light bg
458            let s = (libm::powf(bg_clamp, NORMWHT) - libm::powf(txt_clamp, NORMBLKTXT)) * SCALEWHT;
459            if s < 0.1 { 0.0 } else { s * 100.0 }
460        } else {
461            // Light text on dark bg
462            let s = (libm::powf(bg_clamp, REVWHT) - libm::powf(txt_clamp, REVTXT)) * SCALEWHT;
463            if s > -0.1 { 0.0 } else { s * 100.0 }
464        }
465    }
466    
467    /// Check if the APCA contrast meets the recommended minimum for body text (|Lc| >= 60).
468    #[must_use] pub fn meets_apca_body(&self, background: &Self) -> bool {
469        libm::fabsf(self.apca_contrast(background)) >= 60.0
470    }
471    
472    /// Check if the APCA contrast meets the minimum for large text (|Lc| >= 45).
473    #[must_use] pub fn meets_apca_large(&self, background: &Self) -> bool {
474        libm::fabsf(self.apca_contrast(background)) >= 45.0
475    }
476    
477    /// Set the alpha channel while keeping RGB values.
478    #[must_use] pub const fn with_alpha(&self, a: u8) -> Self {
479        Self { r: self.r, g: self.g, b: self.b, a }
480    }
481    
482    /// Set the alpha as a float (0.0 to 1.0).
483    #[must_use] pub fn with_alpha_f32(&self, a: f32) -> Self {
484        self.with_alpha(channel_to_u8(a.clamp(0.0, 1.0) * 255.0))
485    }
486    
487    /// Invert the color (keeping alpha).
488    #[must_use] pub const fn invert(&self) -> Self {
489        Self {
490            r: 255 - self.r,
491            g: 255 - self.g,
492            b: 255 - self.b,
493            a: self.a,
494        }
495    }
496    
497    /// Convert to grayscale using luminance weights.
498    #[must_use] pub fn to_grayscale(&self) -> Self {
499        let gray = channel_to_u8(0.299 * f32::from(self.r) + 0.587 * f32::from(self.g) + 0.114 * f32::from(self.b));
500        Self { r: gray, g: gray, b: gray, a: self.a }
501    }
502
503    /// Returns `true` if the alpha channel is not fully opaque (i.e. `a != 255`).
504    #[must_use] pub const fn has_alpha(&self) -> bool {
505        self.a != Self::ALPHA_OPAQUE
506    }
507
508    /// Format the color as an 8-digit lowercase hex string (e.g. `#ff0000ff`).
509    #[must_use] pub fn to_hash(&self) -> String {
510        format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
511    }
512
513    // ============================================================
514    // Elementary OS color palette (with shade parameter 100-900)
515    // ============================================================
516
517    /// Strawberry color palette (shade: 100, 300, 500, 700, 900)
518    #[must_use] pub const fn strawberry(shade: usize) -> Self {
519        match shade {
520            0..=200 => Self::rgb(0xff, 0x8c, 0x82),   // 100: #ff8c82
521            201..=400 => Self::rgb(0xed, 0x53, 0x53), // 300: #ed5353
522            401..=600 => Self::rgb(0xc6, 0x26, 0x2e), // 500: #c6262e
523            601..=800 => Self::rgb(0xa1, 0x07, 0x05), // 700: #a10705
524            _ => Self::rgb(0x7a, 0x00, 0x00),         // 900: #7a0000
525        }
526    }
527
528    /// Orange color palette (shade: 100, 300, 500, 700, 900)
529    #[must_use] pub const fn palette_orange(shade: usize) -> Self {
530        match shade {
531            0..=200 => Self::rgb(0xff, 0xc2, 0x7d),   // 100: #ffc27d
532            201..=400 => Self::rgb(0xff, 0xa1, 0x54), // 300: #ffa154
533            401..=600 => Self::rgb(0xf3, 0x73, 0x29), // 500: #f37329
534            601..=800 => Self::rgb(0xcc, 0x3b, 0x02), // 700: #cc3b02
535            _ => Self::rgb(0xa6, 0x21, 0x00),         // 900: #a62100
536        }
537    }
538
539    /// Banana color palette (shade: 100, 300, 500, 700, 900)
540    #[must_use] pub const fn banana(shade: usize) -> Self {
541        match shade {
542            0..=200 => Self::rgb(0xff, 0xf3, 0x94),   // 100: #fff394
543            201..=400 => Self::rgb(0xff, 0xe1, 0x6b), // 300: #ffe16b
544            401..=600 => Self::rgb(0xf9, 0xc4, 0x40), // 500: #f9c440
545            601..=800 => Self::rgb(0xd4, 0x8e, 0x15), // 700: #d48e15
546            _ => Self::rgb(0xad, 0x5f, 0x00),         // 900: #ad5f00
547        }
548    }
549
550    /// Lime color palette (shade: 100, 300, 500, 700, 900)
551    #[must_use] pub const fn palette_lime(shade: usize) -> Self {
552        match shade {
553            0..=200 => Self::rgb(0xd1, 0xff, 0x82),   // 100: #d1ff82
554            201..=400 => Self::rgb(0x9b, 0xdb, 0x4d), // 300: #9bdb4d
555            401..=600 => Self::rgb(0x68, 0xb7, 0x23), // 500: #68b723
556            601..=800 => Self::rgb(0x3a, 0x91, 0x04), // 700: #3a9104
557            _ => Self::rgb(0x20, 0x6b, 0x00),         // 900: #206b00
558        }
559    }
560
561    /// Mint color palette (shade: 100, 300, 500, 700, 900)
562    #[must_use] pub const fn mint(shade: usize) -> Self {
563        match shade {
564            0..=200 => Self::rgb(0x89, 0xff, 0xdd),   // 100: #89ffdd
565            201..=400 => Self::rgb(0x43, 0xd6, 0xb5), // 300: #43d6b5
566            401..=600 => Self::rgb(0x28, 0xbc, 0xa3), // 500: #28bca3
567            601..=800 => Self::rgb(0x0e, 0x9a, 0x83), // 700: #0e9a83
568            _ => Self::rgb(0x00, 0x73, 0x67),         // 900: #007367
569        }
570    }
571
572    /// Blueberry color palette (shade: 100, 300, 500, 700, 900)
573    #[must_use] pub const fn blueberry(shade: usize) -> Self {
574        match shade {
575            0..=200 => Self::rgb(0x8c, 0xd5, 0xff),   // 100: #8cd5ff
576            201..=400 => Self::rgb(0x64, 0xba, 0xff), // 300: #64baff
577            401..=600 => Self::rgb(0x36, 0x89, 0xe6), // 500: #3689e6
578            601..=800 => Self::rgb(0x0d, 0x52, 0xbf), // 700: #0d52bf
579            _ => Self::rgb(0x00, 0x2e, 0x99),         // 900: #002e99
580        }
581    }
582
583    /// Grape color palette (shade: 100, 300, 500, 700, 900)
584    #[must_use] pub const fn grape(shade: usize) -> Self {
585        match shade {
586            0..=200 => Self::rgb(0xe4, 0xc6, 0xfa),   // 100: #e4c6fa
587            201..=400 => Self::rgb(0xcd, 0x9e, 0xf7), // 300: #cd9ef7
588            401..=600 => Self::rgb(0xa5, 0x6d, 0xe2), // 500: #a56de2
589            601..=800 => Self::rgb(0x72, 0x39, 0xb3), // 700: #7239b3
590            _ => Self::rgb(0x45, 0x29, 0x81),         // 900: #452981
591        }
592    }
593
594    /// Bubblegum color palette (shade: 100, 300, 500, 700, 900)
595    #[must_use] pub const fn bubblegum(shade: usize) -> Self {
596        match shade {
597            0..=200 => Self::rgb(0xfe, 0x9a, 0xb8),   // 100: #fe9ab8
598            201..=400 => Self::rgb(0xf4, 0x67, 0x9d), // 300: #f4679d
599            401..=600 => Self::rgb(0xde, 0x3e, 0x80), // 500: #de3e80
600            601..=800 => Self::rgb(0xbc, 0x24, 0x5d), // 700: #bc245d
601            _ => Self::rgb(0x91, 0x0e, 0x38),         // 900: #910e38
602        }
603    }
604
605    /// Cocoa color palette (shade: 100, 300, 500, 700, 900)
606    #[must_use] pub const fn cocoa(shade: usize) -> Self {
607        match shade {
608            0..=200 => Self::rgb(0xa3, 0x90, 0x7c),   // 100: #a3907c
609            201..=400 => Self::rgb(0x8a, 0x71, 0x5e), // 300: #8a715e
610            401..=600 => Self::rgb(0x71, 0x53, 0x44), // 500: #715344
611            601..=800 => Self::rgb(0x57, 0x39, 0x2d), // 700: #57392d
612            _ => Self::rgb(0x3d, 0x21, 0x1b),         // 900: #3d211b
613        }
614    }
615
616    /// Silver color palette (shade: 100, 300, 500, 700, 900)
617    #[must_use] pub const fn palette_silver(shade: usize) -> Self {
618        match shade {
619            0..=200 => Self::rgb(0xfa, 0xfa, 0xfa),   // 100: #fafafa
620            201..=400 => Self::rgb(0xd4, 0xd4, 0xd4), // 300: #d4d4d4
621            401..=600 => Self::rgb(0xab, 0xac, 0xae), // 500: #abacae
622            601..=800 => Self::rgb(0x7e, 0x80, 0x87), // 700: #7e8087
623            _ => Self::rgb(0x55, 0x57, 0x61),         // 900: #555761
624        }
625    }
626
627    /// Slate color palette (shade: 100, 300, 500, 700, 900)
628    #[must_use] pub const fn slate(shade: usize) -> Self {
629        match shade {
630            0..=200 => Self::rgb(0x95, 0xa3, 0xab),   // 100: #95a3ab
631            201..=400 => Self::rgb(0x66, 0x78, 0x85), // 300: #667885
632            401..=600 => Self::rgb(0x48, 0x5a, 0x6c), // 500: #485a6c
633            601..=800 => Self::rgb(0x27, 0x34, 0x45), // 700: #273445
634            _ => Self::rgb(0x0e, 0x14, 0x1f),         // 900: #0e141f
635        }
636    }
637
638    /// Dark color palette (shade: 100, 300, 500, 700, 900)
639    #[must_use] pub const fn dark(shade: usize) -> Self {
640        match shade {
641            0..=200 => Self::rgb(0x66, 0x66, 0x66),   // 100: #666
642            201..=400 => Self::rgb(0x4d, 0x4d, 0x4d), // 300: #4d4d4d
643            401..=600 => Self::rgb(0x33, 0x33, 0x33), // 500: #333
644            601..=800 => Self::rgb(0x1a, 0x1a, 0x1a), // 700: #1a1a1a
645            _ => Self::rgb(0x00, 0x00, 0x00),         // 900: #000
646        }
647    }
648
649    // ============================================================
650    // Apple System Colors (light and dark variants)
651    // ============================================================
652
653    /// Apple Red (light mode)
654    #[must_use] pub const fn apple_red() -> Self { Self::rgb(255, 59, 48) }
655    /// Apple Red (dark mode)
656    #[must_use] pub const fn apple_red_dark() -> Self { Self::rgb(255, 69, 58) }
657    /// Apple Orange (light mode)
658    #[must_use] pub const fn apple_orange() -> Self { Self::rgb(255, 149, 0) }
659    /// Apple Orange (dark mode)
660    #[must_use] pub const fn apple_orange_dark() -> Self { Self::rgb(255, 159, 10) }
661    /// Apple Yellow (light mode)
662    #[must_use] pub const fn apple_yellow() -> Self { Self::rgb(255, 204, 0) }
663    /// Apple Yellow (dark mode)
664    #[must_use] pub const fn apple_yellow_dark() -> Self { Self::rgb(255, 214, 10) }
665    /// Apple Green (light mode)
666    #[must_use] pub const fn apple_green() -> Self { Self::rgb(40, 205, 65) }
667    /// Apple Green (dark mode)
668    #[must_use] pub const fn apple_green_dark() -> Self { Self::rgb(40, 215, 75) }
669    /// Apple Mint (light mode)
670    #[must_use] pub const fn apple_mint() -> Self { Self::rgb(0, 199, 190) }
671    /// Apple Mint (dark mode)
672    #[must_use] pub const fn apple_mint_dark() -> Self { Self::rgb(102, 212, 207) }
673    /// Apple Teal (light mode)
674    #[must_use] pub const fn apple_teal() -> Self { Self::rgb(89, 173, 196) }
675    /// Apple Teal (dark mode)
676    #[must_use] pub const fn apple_teal_dark() -> Self { Self::rgb(106, 196, 220) }
677    /// Apple Cyan (light mode)
678    #[must_use] pub const fn apple_cyan() -> Self { Self::rgb(85, 190, 240) }
679    /// Apple Cyan (dark mode)
680    #[must_use] pub const fn apple_cyan_dark() -> Self { Self::rgb(90, 200, 245) }
681    /// Apple Blue (light mode)
682    #[must_use] pub const fn apple_blue() -> Self { Self::rgb(0, 122, 255) }
683    /// Apple Blue (dark mode)
684    #[must_use] pub const fn apple_blue_dark() -> Self { Self::rgb(10, 132, 255) }
685    /// Apple Indigo (light mode)
686    #[must_use] pub const fn apple_indigo() -> Self { Self::rgb(88, 86, 214) }
687    /// Apple Indigo (dark mode)
688    #[must_use] pub const fn apple_indigo_dark() -> Self { Self::rgb(94, 92, 230) }
689    /// Apple Purple (light mode)
690    #[must_use] pub const fn apple_purple() -> Self { Self::rgb(175, 82, 222) }
691    /// Apple Purple (dark mode)
692    #[must_use] pub const fn apple_purple_dark() -> Self { Self::rgb(191, 90, 242) }
693    /// Apple Pink (light mode)
694    #[must_use] pub const fn apple_pink() -> Self { Self::rgb(255, 45, 85) }
695    /// Apple Pink (dark mode)
696    #[must_use] pub const fn apple_pink_dark() -> Self { Self::rgb(255, 55, 95) }
697    /// Apple Brown (light mode)
698    #[must_use] pub const fn apple_brown() -> Self { Self::rgb(162, 132, 94) }
699    /// Apple Brown (dark mode)
700    #[must_use] pub const fn apple_brown_dark() -> Self { Self::rgb(172, 142, 104) }
701    /// Apple Gray (light mode)
702    #[must_use] pub const fn apple_gray() -> Self { Self::rgb(142, 142, 147) }
703    /// Apple Gray (dark mode)
704    #[must_use] pub const fn apple_gray_dark() -> Self { Self::rgb(152, 152, 157) }
705
706    // ============================================================
707    // Bootstrap-style semantic button colors
708    // These provide consistent button styling across platforms
709    // ============================================================
710
711    /// Primary button color (blue) - used for main actions
712    #[must_use] pub const fn bootstrap_primary() -> Self { Self::rgb(13, 110, 253) }
713    #[must_use] pub const fn bootstrap_primary_hover() -> Self { Self::rgb(11, 94, 215) }
714    #[must_use] pub const fn bootstrap_primary_active() -> Self { Self::rgb(10, 88, 202) }
715    
716    /// Secondary button color (gray) - used for secondary actions
717    #[must_use] pub const fn bootstrap_secondary() -> Self { Self::rgb(108, 117, 125) }
718    #[must_use] pub const fn bootstrap_secondary_hover() -> Self { Self::rgb(92, 99, 106) }
719    #[must_use] pub const fn bootstrap_secondary_active() -> Self { Self::rgb(86, 94, 100) }
720    
721    /// Success button color (green) - used for confirmations
722    #[must_use] pub const fn bootstrap_success() -> Self { Self::rgb(25, 135, 84) }
723    #[must_use] pub const fn bootstrap_success_hover() -> Self { Self::rgb(21, 115, 71) }
724    #[must_use] pub const fn bootstrap_success_active() -> Self { Self::rgb(20, 108, 67) }
725    
726    /// Danger button color (red) - used for destructive actions
727    #[must_use] pub const fn bootstrap_danger() -> Self { Self::rgb(220, 53, 69) }
728    #[must_use] pub const fn bootstrap_danger_hover() -> Self { Self::rgb(187, 45, 59) }
729    #[must_use] pub const fn bootstrap_danger_active() -> Self { Self::rgb(176, 42, 55) }
730    
731    /// Warning button color (yellow) - used for warnings, uses BLACK text
732    #[must_use] pub const fn bootstrap_warning() -> Self { Self::rgb(255, 193, 7) }
733    #[must_use] pub const fn bootstrap_warning_hover() -> Self { Self::rgb(255, 202, 44) }
734    #[must_use] pub const fn bootstrap_warning_active() -> Self { Self::rgb(255, 205, 57) }
735    
736    /// Info button color (teal/cyan) - used for informational actions
737    #[must_use] pub const fn bootstrap_info() -> Self { Self::rgb(13, 202, 240) }
738    #[must_use] pub const fn bootstrap_info_hover() -> Self { Self::rgb(49, 210, 242) }
739    #[must_use] pub const fn bootstrap_info_active() -> Self { Self::rgb(61, 213, 243) }
740    
741    /// Light button color - used for light-themed buttons
742    #[must_use] pub const fn bootstrap_light() -> Self { Self::rgb(248, 249, 250) }
743    #[must_use] pub const fn bootstrap_light_hover() -> Self { Self::rgb(233, 236, 239) }
744    #[must_use] pub const fn bootstrap_light_active() -> Self { Self::rgb(218, 222, 226) }
745    
746    /// Dark button color - used for dark-themed buttons
747    #[must_use] pub const fn bootstrap_dark() -> Self { Self::rgb(33, 37, 41) }
748    #[must_use] pub const fn bootstrap_dark_hover() -> Self { Self::rgb(66, 70, 73) }
749    #[must_use] pub const fn bootstrap_dark_active() -> Self { Self::rgb(78, 81, 84) }
750    
751    /// Link button text color
752    #[must_use] pub const fn bootstrap_link() -> Self { Self::rgb(13, 110, 253) }
753    #[must_use] pub const fn bootstrap_link_hover() -> Self { Self::rgb(10, 88, 202) }
754}
755
756/// f32-based color, range 0.0 to 1.0 (similar to webrenders `ColorF`)
757#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
758pub struct ColorF {
759    pub r: f32,
760    pub g: f32,
761    pub b: f32,
762    pub a: f32,
763}
764
765impl Default for ColorF {
766    fn default() -> Self {
767        Self::BLACK
768    }
769}
770
771impl fmt::Display for ColorF {
772    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773        write!(
774            f,
775            "rgba({}, {}, {}, {})",
776            self.r * 255.0,
777            self.g * 255.0,
778            self.b * 255.0,
779            self.a
780        )
781    }
782}
783
784impl ColorF {
785    pub const ALPHA_TRANSPARENT: f32 = 0.0;
786    pub const ALPHA_OPAQUE: f32 = 1.0;
787    pub const WHITE: Self = Self {
788        r: 1.0,
789        g: 1.0,
790        b: 1.0,
791        a: Self::ALPHA_OPAQUE,
792    };
793    pub const BLACK: Self = Self {
794        r: 0.0,
795        g: 0.0,
796        b: 0.0,
797        a: Self::ALPHA_OPAQUE,
798    };
799    pub const TRANSPARENT: Self = Self {
800        r: 0.0,
801        g: 0.0,
802        b: 0.0,
803        a: Self::ALPHA_TRANSPARENT,
804    };
805}
806
807impl From<ColorU> for ColorF {
808    fn from(input: ColorU) -> Self {
809        Self {
810            r: f32::from(input.r) / 255.0,
811            g: f32::from(input.g) / 255.0,
812            b: f32::from(input.b) / 255.0,
813            a: f32::from(input.a) / 255.0,
814        }
815    }
816}
817
818impl From<ColorF> for ColorU {
819    fn from(input: ColorF) -> Self {
820        Self {
821            r: channel_to_u8(input.r.min(1.0) * 255.0),
822            g: channel_to_u8(input.g.min(1.0) * 255.0),
823            b: channel_to_u8(input.b.min(1.0) * 255.0),
824            a: channel_to_u8(input.a.min(1.0) * 255.0),
825        }
826    }
827}
828
829/// A color reference that can be either a concrete color or a system color.
830/// System colors are lazily evaluated at runtime based on the user's system theme.
831/// 
832/// CSS syntax: `system:accent`, `system:text`, `system:background`, etc.
833#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
834#[repr(C, u8)]
835pub enum ColorOrSystem {
836    /// A concrete RGBA color value.
837    Color(ColorU),
838    /// A reference to a system color, resolved at runtime.
839    System(SystemColorRef),
840}
841
842impl Default for ColorOrSystem {
843    fn default() -> Self {
844        Self::Color(ColorU::BLACK)
845    }
846}
847
848impl From<ColorU> for ColorOrSystem {
849    fn from(color: ColorU) -> Self {
850        Self::Color(color)
851    }
852}
853
854impl ColorOrSystem {
855    /// Create a new `ColorOrSystem` from a concrete color.
856    #[must_use] pub const fn color(c: ColorU) -> Self {
857        Self::Color(c)
858    }
859    
860    /// Create a new `ColorOrSystem` from a system color reference.
861    #[must_use] pub const fn system(s: SystemColorRef) -> Self {
862        Self::System(s)
863    }
864    
865    /// Resolve the color against a `SystemColors` struct.
866    /// Returns the system color if available, or falls back to the provided default.
867    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
868        match self {
869            Self::Color(c) => *c,
870            Self::System(ref_type) => ref_type.resolve(system_colors, fallback),
871        }
872    }
873    
874    /// Returns the concrete color if available, or a default fallback for system colors.
875    /// Use this when `SystemColors` is not available (e.g., during rendering setup).
876    #[must_use] pub const fn to_color_u_with_fallback(&self, fallback: ColorU) -> ColorU {
877        match self {
878            Self::Color(c) => *c,
879            Self::System(_) => fallback,
880        }
881    }
882    
883    /// Returns the concrete color if available, or a gray fallback for system colors.
884    #[must_use] pub const fn to_color_u_default(&self) -> ColorU {
885        self.to_color_u_with_fallback(ColorU { r: 128, g: 128, b: 128, a: 255 })
886    }
887}
888
889/// Reference to a specific system color.
890/// These are resolved at runtime based on the user's system preferences.
891#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
892#[repr(C)]
893pub enum SystemColorRef {
894    /// System text color (e.g., black on light theme, white on dark)
895    Text,
896    /// System background color
897    Background,
898    /// System accent color (user-selected highlight color)
899    Accent,
900    /// Text color when on accent background
901    AccentText,
902    /// Button face background color
903    ButtonFace,
904    /// Button text color
905    ButtonText,
906    /// Window/panel background color
907    WindowBackground,
908    /// Selection/highlight background color
909    SelectionBackground,
910    /// Text color when selected
911    SelectionText,
912}
913
914impl SystemColorRef {
915    /// Resolve this system color reference against actual system colors.
916    #[must_use] pub fn resolve(&self, colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
917        match self {
918            Self::Text => colors.text.as_option().copied().unwrap_or(fallback),
919            Self::Background => colors.background.as_option().copied().unwrap_or(fallback),
920            Self::Accent => colors.accent.as_option().copied().unwrap_or(fallback),
921            Self::AccentText => colors.accent_text.as_option().copied().unwrap_or(fallback),
922            Self::ButtonFace => colors.button_face.as_option().copied().unwrap_or(fallback),
923            Self::ButtonText => colors.button_text.as_option().copied().unwrap_or(fallback),
924            Self::WindowBackground => colors.window_background.as_option().copied().unwrap_or(fallback),
925            Self::SelectionBackground => colors.selection_background.as_option().copied().unwrap_or(fallback),
926            Self::SelectionText => colors.selection_text.as_option().copied().unwrap_or(fallback),
927        }
928    }
929    
930    /// Get the CSS syntax for this system color reference.
931    #[must_use] pub const fn as_css_str(&self) -> &'static str {
932        match self {
933            Self::Text => "system:text",
934            Self::Background => "system:background",
935            Self::Accent => "system:accent",
936            Self::AccentText => "system:accent-text",
937            Self::ButtonFace => "system:button-face",
938            Self::ButtonText => "system:button-text",
939            Self::WindowBackground => "system:window-background",
940            Self::SelectionBackground => "system:selection-background",
941            Self::SelectionText => "system:selection-text",
942        }
943    }
944}
945
946// --- PARSER ---
947
948#[derive(Debug, Copy, Clone, PartialEq, Eq)]
949#[repr(C)]
950pub enum CssColorComponent {
951    Red,
952    Green,
953    Blue,
954    Hue,
955    Saturation,
956    Lightness,
957    Alpha,
958}
959
960#[derive(Clone, PartialEq)]
961pub enum CssColorParseError<'a> {
962    InvalidColor(&'a str),
963    InvalidFunctionName(&'a str),
964    InvalidColorComponent(u8),
965    IntValueParseErr(ParseIntError),
966    FloatValueParseErr(ParseFloatError),
967    FloatValueOutOfRange(f32),
968    MissingColorComponent(CssColorComponent),
969    ExtraArguments(&'a str),
970    UnclosedColor(&'a str),
971    EmptyInput,
972    DirectionParseError(CssDirectionParseError<'a>),
973    UnsupportedDirection(&'a str),
974    InvalidPercentage(PercentageParseError),
975}
976
977impl_debug_as_display!(CssColorParseError<'a>);
978impl_display! {CssColorParseError<'a>, {
979    InvalidColor(i) => format!("Invalid CSS color: \"{}\"", i),
980    InvalidFunctionName(i) => format!("Invalid function name, expected one of: \"rgb\", \"rgba\", \"hsl\", \"hsla\" got: \"{}\"", i),
981    InvalidColorComponent(i) => format!("Invalid color component when parsing CSS color: \"{}\"", i),
982    IntValueParseErr(e) => format!("CSS color component: Value not in range between 00 - FF: \"{}\"", e),
983    FloatValueParseErr(e) => format!("CSS color component: Value cannot be parsed as floating point number: \"{}\"", e),
984    FloatValueOutOfRange(v) => format!("CSS color component: Value not in range between 0.0 - 1.0: \"{}\"", v),
985    MissingColorComponent(c) => format!("CSS color is missing {:?} component", c),
986    ExtraArguments(a) => format!("Extra argument to CSS color: \"{}\"", a),
987    EmptyInput => format!("Empty color string."),
988    UnclosedColor(i) => format!("Unclosed color: \"{}\"", i),
989    DirectionParseError(e) => format!("Could not parse direction argument for CSS color: \"{}\"", e),
990    UnsupportedDirection(d) => format!("Unsupported direction type for CSS color: \"{}\"", d),
991    InvalidPercentage(p) => format!("Invalid percentage when parsing CSS color: \"{}\"", p),
992}}
993
994impl From<ParseIntError> for CssColorParseError<'_> {
995    fn from(e: ParseIntError) -> Self {
996        CssColorParseError::IntValueParseErr(e)
997    }
998}
999impl From<ParseFloatError> for CssColorParseError<'_> {
1000    fn from(e: ParseFloatError) -> Self {
1001        CssColorParseError::FloatValueParseErr(e)
1002    }
1003}
1004impl From<core::num::ParseIntError> for CssColorParseError<'_> {
1005    fn from(e: core::num::ParseIntError) -> Self {
1006        CssColorParseError::IntValueParseErr(ParseIntError::from(e))
1007    }
1008}
1009impl From<core::num::ParseFloatError> for CssColorParseError<'_> {
1010    fn from(e: core::num::ParseFloatError) -> Self {
1011        CssColorParseError::FloatValueParseErr(ParseFloatError::from(e))
1012    }
1013}
1014impl_from!(
1015    CssDirectionParseError<'a>,
1016    CssColorParseError::DirectionParseError
1017);
1018
1019#[derive(Debug, Clone, PartialEq)]
1020#[repr(C, u8)]
1021pub enum CssColorParseErrorOwned {
1022    InvalidColor(AzString),
1023    InvalidFunctionName(AzString),
1024    InvalidColorComponent(u8),
1025    IntValueParseErr(ParseIntError),
1026    FloatValueParseErr(ParseFloatError),
1027    FloatValueOutOfRange(f32),
1028    MissingColorComponent(CssColorComponent),
1029    ExtraArguments(AzString),
1030    UnclosedColor(AzString),
1031    EmptyInput,
1032    DirectionParseError(CssDirectionParseErrorOwned),
1033    UnsupportedDirection(AzString),
1034    InvalidPercentage(PercentageParseError),
1035}
1036
1037impl CssColorParseError<'_> {
1038    #[must_use] pub fn to_contained(&self) -> CssColorParseErrorOwned {
1039        match self {
1040            CssColorParseError::InvalidColor(s) => {
1041                CssColorParseErrorOwned::InvalidColor((*s).to_string().into())
1042            }
1043            CssColorParseError::InvalidFunctionName(s) => {
1044                CssColorParseErrorOwned::InvalidFunctionName((*s).to_string().into())
1045            }
1046            CssColorParseError::InvalidColorComponent(n) => {
1047                CssColorParseErrorOwned::InvalidColorComponent(*n)
1048            }
1049            CssColorParseError::IntValueParseErr(e) => {
1050                CssColorParseErrorOwned::IntValueParseErr(*e)
1051            }
1052            CssColorParseError::FloatValueParseErr(e) => {
1053                CssColorParseErrorOwned::FloatValueParseErr(*e)
1054            }
1055            CssColorParseError::FloatValueOutOfRange(n) => {
1056                CssColorParseErrorOwned::FloatValueOutOfRange(*n)
1057            }
1058            CssColorParseError::MissingColorComponent(c) => {
1059                CssColorParseErrorOwned::MissingColorComponent(*c)
1060            }
1061            CssColorParseError::ExtraArguments(s) => {
1062                CssColorParseErrorOwned::ExtraArguments((*s).to_string().into())
1063            }
1064            CssColorParseError::UnclosedColor(s) => {
1065                CssColorParseErrorOwned::UnclosedColor((*s).to_string().into())
1066            }
1067            CssColorParseError::EmptyInput => CssColorParseErrorOwned::EmptyInput,
1068            CssColorParseError::DirectionParseError(e) => {
1069                CssColorParseErrorOwned::DirectionParseError(e.to_contained())
1070            }
1071            CssColorParseError::UnsupportedDirection(s) => {
1072                CssColorParseErrorOwned::UnsupportedDirection((*s).to_string().into())
1073            }
1074            CssColorParseError::InvalidPercentage(e) => {
1075                CssColorParseErrorOwned::InvalidPercentage(e.clone())
1076            }
1077        }
1078    }
1079}
1080
1081impl CssColorParseErrorOwned {
1082    #[must_use] pub fn to_shared(&self) -> CssColorParseError<'_> {
1083        match self {
1084            Self::InvalidColor(s) => CssColorParseError::InvalidColor(s),
1085            Self::InvalidFunctionName(s) => {
1086                CssColorParseError::InvalidFunctionName(s)
1087            }
1088            Self::InvalidColorComponent(n) => {
1089                CssColorParseError::InvalidColorComponent(*n)
1090            }
1091            Self::IntValueParseErr(e) => {
1092                CssColorParseError::IntValueParseErr(*e)
1093            }
1094            Self::FloatValueParseErr(e) => {
1095                CssColorParseError::FloatValueParseErr(*e)
1096            }
1097            Self::FloatValueOutOfRange(n) => {
1098                CssColorParseError::FloatValueOutOfRange(*n)
1099            }
1100            Self::MissingColorComponent(c) => {
1101                CssColorParseError::MissingColorComponent(*c)
1102            }
1103            Self::ExtraArguments(s) => CssColorParseError::ExtraArguments(s),
1104            Self::UnclosedColor(s) => CssColorParseError::UnclosedColor(s),
1105            Self::EmptyInput => CssColorParseError::EmptyInput,
1106            Self::DirectionParseError(e) => {
1107                CssColorParseError::DirectionParseError(e.to_shared())
1108            }
1109            Self::UnsupportedDirection(s) => {
1110                CssColorParseError::UnsupportedDirection(s)
1111            }
1112            Self::InvalidPercentage(e) => {
1113                CssColorParseError::InvalidPercentage(e.clone())
1114            }
1115        }
1116    }
1117}
1118
1119#[cfg(feature = "parser")]
1120/// # Errors
1121///
1122/// Returns an error if `input` is not a valid CSS `css-color` value.
1123pub fn parse_css_color(input: &str) -> Result<ColorU, CssColorParseError<'_>> {
1124    use crate::props::basic::parse::{parse_parentheses, ParenthesisParseError};
1125
1126    let input = input.trim();
1127    if let Some(rest) = input.strip_prefix('#') {
1128        return parse_color_no_hash(rest);
1129    }
1130
1131    match parse_parentheses(input, &["rgba", "rgb", "hsla", "hsl"]) {
1132        Ok((stopword, inner_value)) => match stopword {
1133            "rgba" => parse_color_rgb(inner_value, true),
1134            "rgb" => parse_color_rgb(inner_value, false),
1135            "hsla" => parse_color_hsl(inner_value, true),
1136            "hsl" => parse_color_hsl(inner_value, false),
1137            _ => unreachable!(),
1138        },
1139        Err(e) => match e {
1140            ParenthesisParseError::UnclosedBraces | ParenthesisParseError::NoClosingBraceFound => {
1141                Err(CssColorParseError::UnclosedColor(input))
1142            }
1143            ParenthesisParseError::EmptyInput => Err(CssColorParseError::EmptyInput),
1144            ParenthesisParseError::StopWordNotFound(stopword) => {
1145                Err(CssColorParseError::InvalidFunctionName(stopword))
1146            }
1147            ParenthesisParseError::NoOpeningBraceFound => parse_color_builtin(input),
1148        },
1149    }
1150}
1151
1152/// Parse a color that can be either a concrete color or a system color reference.
1153/// 
1154/// Supports all standard CSS color formats plus:
1155/// - `system:accent` - System accent/highlight color
1156/// - `system:text` - System text color
1157/// - `system:background` - System background color
1158/// - `system:selection-background` - Selection/highlight background
1159/// - `system:selection-text` - Text color when selected
1160/// - `system:button-face` - Button background color
1161/// - `system:button-text` - Button text color
1162/// - `system:window-background` - Window background color
1163/// - `system:accent-text` - Text color on accent background
1164#[cfg(feature = "parser")]
1165/// # Errors
1166///
1167/// Returns an error if `input` is not a valid CSS `color-or-system` value.
1168pub fn parse_color_or_system(input: &str) -> Result<ColorOrSystem, CssColorParseError<'_>> {
1169    let input = input.trim();
1170    
1171    // Check for system color syntax: "system:name"
1172    if let Some(system_name) = input.strip_prefix("system:") {
1173        let system_ref = match system_name.trim() {
1174            "text" => SystemColorRef::Text,
1175            "background" => SystemColorRef::Background,
1176            "accent" => SystemColorRef::Accent,
1177            "accent-text" => SystemColorRef::AccentText,
1178            "button-face" => SystemColorRef::ButtonFace,
1179            "button-text" => SystemColorRef::ButtonText,
1180            "window-background" => SystemColorRef::WindowBackground,
1181            "selection-background" => SystemColorRef::SelectionBackground,
1182            "selection-text" => SystemColorRef::SelectionText,
1183            _ => return Err(CssColorParseError::InvalidColor(input)),
1184        };
1185        return Ok(ColorOrSystem::System(system_ref));
1186    }
1187    
1188    // Otherwise parse as regular color
1189    parse_css_color(input).map(ColorOrSystem::Color)
1190}
1191
1192#[cfg(feature = "parser")]
1193fn parse_color_no_hash(input: &str) -> Result<ColorU, CssColorParseError<'_>> {
1194    #[inline]
1195    const fn from_hex<'a>(c: u8) -> Result<u8, CssColorParseError<'a>> {
1196        match c {
1197            b'0'..=b'9' => Ok(c - b'0'),
1198            b'a'..=b'f' => Ok(c - b'a' + 10),
1199            b'A'..=b'F' => Ok(c - b'A' + 10),
1200            _ => Err(CssColorParseError::InvalidColorComponent(c)),
1201        }
1202    }
1203
1204    match input.len() {
1205        3 => {
1206            let mut bytes = input.bytes();
1207            let r = bytes.next().unwrap();
1208            let g = bytes.next().unwrap();
1209            let b = bytes.next().unwrap();
1210            Ok(ColorU::new_rgb(
1211                from_hex(r)? * 17,
1212                from_hex(g)? * 17,
1213                from_hex(b)? * 17,
1214            ))
1215        }
1216        4 => {
1217            let mut bytes = input.bytes();
1218            let r = bytes.next().unwrap();
1219            let g = bytes.next().unwrap();
1220            let b = bytes.next().unwrap();
1221            let a = bytes.next().unwrap();
1222            Ok(ColorU::new(
1223                from_hex(r)? * 17,
1224                from_hex(g)? * 17,
1225                from_hex(b)? * 17,
1226                from_hex(a)? * 17,
1227            ))
1228        }
1229        6 => {
1230            // u32::from_str_radix silently accepts a leading '+' ("+f0000"), which is
1231            // not a valid <hex-color> (CSS Color 4 §5.1: only hex digits). The 3/4-digit
1232            // branches decode per-byte and already reject it; guard the radix branches.
1233            if !input.bytes().all(|b| b.is_ascii_hexdigit()) {
1234                return Err(CssColorParseError::InvalidColor(input));
1235            }
1236            let val = u32::from_str_radix(input, 16)?;
1237            Ok(ColorU::new_rgb(
1238                ((val >> 16) & 0xFF) as u8,
1239                ((val >> 8) & 0xFF) as u8,
1240                (val & 0xFF) as u8,
1241            ))
1242        }
1243        8 => {
1244            if !input.bytes().all(|b| b.is_ascii_hexdigit()) {
1245                return Err(CssColorParseError::InvalidColor(input));
1246            }
1247            let val = u32::from_str_radix(input, 16)?;
1248            Ok(ColorU::new(
1249                ((val >> 24) & 0xFF) as u8,
1250                ((val >> 16) & 0xFF) as u8,
1251                ((val >> 8) & 0xFF) as u8,
1252                (val & 0xFF) as u8,
1253            ))
1254        }
1255        _ => Err(CssColorParseError::InvalidColor(input)),
1256    }
1257}
1258
1259#[cfg(feature = "parser")]
1260fn parse_color_rgb(
1261    input: &str,
1262    parse_alpha: bool,
1263) -> Result<ColorU, CssColorParseError<'_>> {
1264    let mut components = input.split(',').map(str::trim);
1265    let rgb_color = parse_color_rgb_components(&mut components)?;
1266    let a = if parse_alpha {
1267        parse_alpha_component(&mut components)?
1268    } else {
1269        255
1270    };
1271    if let Some(arg) = components.next() {
1272        return Err(CssColorParseError::ExtraArguments(arg));
1273    }
1274    Ok(ColorU { a, ..rgb_color })
1275}
1276
1277#[cfg(feature = "parser")]
1278fn parse_color_rgb_components<'a>(
1279    components: &mut dyn Iterator<Item = &'a str>,
1280) -> Result<ColorU, CssColorParseError<'a>> {
1281    #[inline]
1282    fn component_from_str<'a>(
1283        components: &mut dyn Iterator<Item = &'a str>,
1284        which: CssColorComponent,
1285    ) -> Result<u8, CssColorParseError<'a>> {
1286        let c = components
1287            .next()
1288            .ok_or(CssColorParseError::MissingColorComponent(which))?;
1289        if c.is_empty() {
1290            return Err(CssColorParseError::MissingColorComponent(which));
1291        }
1292        Ok(c.parse::<u8>()?)
1293    }
1294    Ok(ColorU {
1295        r: component_from_str(components, CssColorComponent::Red)?,
1296        g: component_from_str(components, CssColorComponent::Green)?,
1297        b: component_from_str(components, CssColorComponent::Blue)?,
1298        a: 255,
1299    })
1300}
1301
1302#[cfg(feature = "parser")]
1303fn parse_color_hsl(
1304    input: &str,
1305    parse_alpha: bool,
1306) -> Result<ColorU, CssColorParseError<'_>> {
1307    let mut components = input.split(',').map(str::trim);
1308    let rgb_color = parse_color_hsl_components(&mut components)?;
1309    let a = if parse_alpha {
1310        parse_alpha_component(&mut components)?
1311    } else {
1312        255
1313    };
1314    if let Some(arg) = components.next() {
1315        return Err(CssColorParseError::ExtraArguments(arg));
1316    }
1317    Ok(ColorU { a, ..rgb_color })
1318}
1319
1320#[cfg(feature = "parser")]
1321#[allow(clippy::many_single_char_names)] // domain-standard h/s/l/r/g/b colour component names
1322fn parse_color_hsl_components<'a>(
1323    components: &mut dyn Iterator<Item = &'a str>,
1324) -> Result<ColorU, CssColorParseError<'a>> {
1325    #[inline]
1326    fn angle_from_str<'a>(
1327        components: &mut dyn Iterator<Item = &'a str>,
1328        which: CssColorComponent,
1329    ) -> Result<f32, CssColorParseError<'a>> {
1330        let c = components
1331            .next()
1332            .ok_or(CssColorParseError::MissingColorComponent(which))?;
1333        if c.is_empty() {
1334            return Err(CssColorParseError::MissingColorComponent(which));
1335        }
1336        let dir = parse_direction(c)?;
1337        match dir {
1338            Direction::Angle(deg) => Ok(deg.to_degrees()),
1339            Direction::FromTo(_) => Err(CssColorParseError::UnsupportedDirection(c)),
1340        }
1341    }
1342
1343    #[inline]
1344    fn percent_from_str<'a>(
1345        components: &mut dyn Iterator<Item = &'a str>,
1346        which: CssColorComponent,
1347    ) -> Result<f32, CssColorParseError<'a>> {
1348        use crate::props::basic::parse_percentage_value;
1349
1350        let c = components
1351            .next()
1352            .ok_or(CssColorParseError::MissingColorComponent(which))?;
1353        if c.is_empty() {
1354            return Err(CssColorParseError::MissingColorComponent(which));
1355        }
1356
1357        // Modern CSS allows both percentage and unitless values for HSL
1358        Ok(parse_percentage_value(c)
1359            .map_err(CssColorParseError::InvalidPercentage)?
1360            .normalized()
1361            * 100.0)
1362    }
1363
1364    #[inline]
1365    #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
1366    #[allow(clippy::many_single_char_names)] // domain-standard colour/coordinate component names
1367    fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
1368        let s = s / 100.0;
1369        let l = l / 100.0;
1370        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
1371        let h_prime = h / 60.0;
1372        let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
1373        let (r1, g1, b1) = if (0.0..1.0).contains(&h_prime) {
1374            (c, x, 0.0)
1375        } else if (1.0..2.0).contains(&h_prime) {
1376            (x, c, 0.0)
1377        } else if (2.0..3.0).contains(&h_prime) {
1378            (0.0, c, x)
1379        } else if (3.0..4.0).contains(&h_prime) {
1380            (0.0, x, c)
1381        } else if (4.0..5.0).contains(&h_prime) {
1382            (x, 0.0, c)
1383        } else {
1384            (c, 0.0, x)
1385        };
1386        let m = l - c / 2.0;
1387        (
1388            channel_to_u8((r1 + m) * 255.0),
1389            channel_to_u8((g1 + m) * 255.0),
1390            channel_to_u8((b1 + m) * 255.0),
1391        )
1392    }
1393
1394    let (h, s, l) = (
1395        angle_from_str(components, CssColorComponent::Hue)?,
1396        percent_from_str(components, CssColorComponent::Saturation)?,
1397        percent_from_str(components, CssColorComponent::Lightness)?,
1398    );
1399
1400    let (r, g, b) = hsl_to_rgb(h, s, l);
1401    Ok(ColorU { r, g, b, a: 255 })
1402}
1403
1404#[cfg(feature = "parser")]
1405fn parse_alpha_component<'a>(
1406    components: &mut dyn Iterator<Item = &'a str>,
1407) -> Result<u8, CssColorParseError<'a>> {
1408    let a_str = components
1409        .next()
1410        .ok_or(CssColorParseError::MissingColorComponent(
1411            CssColorComponent::Alpha,
1412        ))?;
1413    if a_str.is_empty() {
1414        return Err(CssColorParseError::MissingColorComponent(
1415            CssColorComponent::Alpha,
1416        ));
1417    }
1418    let a = a_str.parse::<f32>()?;
1419    if !(0.0..=1.0).contains(&a) {
1420        return Err(CssColorParseError::FloatValueOutOfRange(a));
1421    }
1422    Ok(channel_to_u8((a * 255.0).round()))
1423}
1424
1425#[cfg(feature = "parser")]
1426#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
1427fn parse_color_builtin(input: &str) -> Result<ColorU, CssColorParseError<'_>> {
1428    let (r, g, b, a) = match input.to_lowercase().as_str() {
1429        "aliceblue" => (240, 248, 255, 255),
1430        "antiquewhite" => (250, 235, 215, 255),
1431        "aqua" | "cyan" => (0, 255, 255, 255),
1432        "aquamarine" => (127, 255, 212, 255),
1433        "azure" => (240, 255, 255, 255),
1434        "beige" => (245, 245, 220, 255),
1435        "bisque" => (255, 228, 196, 255),
1436        "black" => (0, 0, 0, 255),
1437        "blanchedalmond" => (255, 235, 205, 255),
1438        "blue" => (0, 0, 255, 255),
1439        "blueviolet" => (138, 43, 226, 255),
1440        "brown" => (165, 42, 42, 255),
1441        "burlywood" => (222, 184, 135, 255),
1442        "cadetblue" => (95, 158, 160, 255),
1443        "chartreuse" => (127, 255, 0, 255),
1444        "chocolate" => (210, 105, 30, 255),
1445        "coral" => (255, 127, 80, 255),
1446        "cornflowerblue" => (100, 149, 237, 255),
1447        "cornsilk" => (255, 248, 220, 255),
1448        "crimson" => (220, 20, 60, 255),
1449        "darkblue" => (0, 0, 139, 255),
1450        "darkcyan" => (0, 139, 139, 255),
1451        "darkgoldenrod" => (184, 134, 11, 255),
1452        "darkgray" | "darkgrey" => (169, 169, 169, 255),
1453        "darkgreen" => (0, 100, 0, 255),
1454        "darkkhaki" => (189, 183, 107, 255),
1455        "darkmagenta" => (139, 0, 139, 255),
1456        "darkolivegreen" => (85, 107, 47, 255),
1457        "darkorange" => (255, 140, 0, 255),
1458        "darkorchid" => (153, 50, 204, 255),
1459        "darkred" => (139, 0, 0, 255),
1460        "darksalmon" => (233, 150, 122, 255),
1461        "darkseagreen" => (143, 188, 143, 255),
1462        "darkslateblue" => (72, 61, 139, 255),
1463        "darkslategray" | "darkslategrey" => (47, 79, 79, 255),
1464        "darkturquoise" => (0, 206, 209, 255),
1465        "darkviolet" => (148, 0, 211, 255),
1466        "deeppink" => (255, 20, 147, 255),
1467        "deepskyblue" => (0, 191, 255, 255),
1468        "dimgray" | "dimgrey" => (105, 105, 105, 255),
1469        "dodgerblue" => (30, 144, 255, 255),
1470        "firebrick" => (178, 34, 34, 255),
1471        "floralwhite" => (255, 250, 240, 255),
1472        "forestgreen" => (34, 139, 34, 255),
1473        "fuchsia" | "magenta" => (255, 0, 255, 255),
1474        "gainsboro" => (220, 220, 220, 255),
1475        "ghostwhite" => (248, 248, 255, 255),
1476        "gold" => (255, 215, 0, 255),
1477        "goldenrod" => (218, 165, 32, 255),
1478        "gray" | "grey" => (128, 128, 128, 255),
1479        "green" => (0, 128, 0, 255),
1480        "greenyellow" => (173, 255, 47, 255),
1481        "honeydew" => (240, 255, 240, 255),
1482        "hotpink" => (255, 105, 180, 255),
1483        "indianred" => (205, 92, 92, 255),
1484        "indigo" => (75, 0, 130, 255),
1485        "ivory" => (255, 255, 240, 255),
1486        "khaki" => (240, 230, 140, 255),
1487        "lavender" => (230, 230, 250, 255),
1488        "lavenderblush" => (255, 240, 245, 255),
1489        "lawngreen" => (124, 252, 0, 255),
1490        "lemonchiffon" => (255, 250, 205, 255),
1491        "lightblue" => (173, 216, 230, 255),
1492        "lightcoral" => (240, 128, 128, 255),
1493        "lightcyan" => (224, 255, 255, 255),
1494        "lightgoldenrodyellow" => (250, 250, 210, 255),
1495        "lightgray" | "lightgrey" => (211, 211, 211, 255),
1496        "lightgreen" => (144, 238, 144, 255),
1497        "lightpink" => (255, 182, 193, 255),
1498        "lightsalmon" => (255, 160, 122, 255),
1499        "lightseagreen" => (32, 178, 170, 255),
1500        "lightskyblue" => (135, 206, 250, 255),
1501        "lightslategray" | "lightslategrey" => (119, 136, 153, 255),
1502        "lightsteelblue" => (176, 196, 222, 255),
1503        "lightyellow" => (255, 255, 224, 255),
1504        "lime" => (0, 255, 0, 255),
1505        "limegreen" => (50, 205, 50, 255),
1506        "linen" => (250, 240, 230, 255),
1507        "maroon" => (128, 0, 0, 255),
1508        "mediumaquamarine" => (102, 205, 170, 255),
1509        "mediumblue" => (0, 0, 205, 255),
1510        "mediumorchid" => (186, 85, 211, 255),
1511        "mediumpurple" => (147, 112, 219, 255),
1512        "mediumseagreen" => (60, 179, 113, 255),
1513        "mediumslateblue" => (123, 104, 238, 255),
1514        "mediumspringgreen" => (0, 250, 154, 255),
1515        "mediumturquoise" => (72, 209, 204, 255),
1516        "mediumvioletred" => (199, 21, 133, 255),
1517        "midnightblue" => (25, 25, 112, 255),
1518        "mintcream" => (245, 255, 250, 255),
1519        "mistyrose" => (255, 228, 225, 255),
1520        "moccasin" => (255, 228, 181, 255),
1521        "navajowhite" => (255, 222, 173, 255),
1522        "navy" => (0, 0, 128, 255),
1523        "oldlace" => (253, 245, 230, 255),
1524        "olive" => (128, 128, 0, 255),
1525        "olivedrab" => (107, 142, 35, 255),
1526        "orange" => (255, 165, 0, 255),
1527        "orangered" => (255, 69, 0, 255),
1528        "orchid" => (218, 112, 214, 255),
1529        "palegoldenrod" => (238, 232, 170, 255),
1530        "palegreen" => (152, 251, 152, 255),
1531        "paleturquoise" => (175, 238, 238, 255),
1532        "palevioletred" => (219, 112, 147, 255),
1533        "papayawhip" => (255, 239, 213, 255),
1534        "peachpuff" => (255, 218, 185, 255),
1535        "peru" => (205, 133, 63, 255),
1536        "pink" => (255, 192, 203, 255),
1537        "plum" => (221, 160, 221, 255),
1538        "powderblue" => (176, 224, 230, 255),
1539        "purple" => (128, 0, 128, 255),
1540        "rebeccapurple" => (102, 51, 153, 255),
1541        "red" => (255, 0, 0, 255),
1542        "rosybrown" => (188, 143, 143, 255),
1543        "royalblue" => (65, 105, 225, 255),
1544        "saddlebrown" => (139, 69, 19, 255),
1545        "salmon" => (250, 128, 114, 255),
1546        "sandybrown" => (244, 164, 96, 255),
1547        "seagreen" => (46, 139, 87, 255),
1548        "seashell" => (255, 245, 238, 255),
1549        "sienna" => (160, 82, 45, 255),
1550        "silver" => (192, 192, 192, 255),
1551        "skyblue" => (135, 206, 235, 255),
1552        "slateblue" => (106, 90, 205, 255),
1553        "slategray" | "slategrey" => (112, 128, 144, 255),
1554        "snow" => (255, 250, 250, 255),
1555        "springgreen" => (0, 255, 127, 255),
1556        "steelblue" => (70, 130, 180, 255),
1557        "tan" => (210, 180, 140, 255),
1558        "teal" => (0, 128, 128, 255),
1559        "thistle" => (216, 191, 216, 255),
1560        "tomato" => (255, 99, 71, 255),
1561        "transparent" => (0, 0, 0, 0),
1562        "turquoise" => (64, 224, 208, 255),
1563        "violet" => (238, 130, 238, 255),
1564        "wheat" => (245, 222, 179, 255),
1565        "white" => (255, 255, 255, 255),
1566        "whitesmoke" => (245, 245, 245, 255),
1567        "yellow" => (255, 255, 0, 255),
1568        "yellowgreen" => (154, 205, 50, 255),
1569        _ => return Err(CssColorParseError::InvalidColor(input)),
1570    };
1571    Ok(ColorU { r, g, b, a })
1572}
1573
1574#[cfg(all(test, feature = "parser"))]
1575mod tests {
1576    use super::*;
1577
1578    #[test]
1579    fn test_parse_color_keywords() {
1580        assert_eq!(parse_css_color("red").unwrap(), ColorU::RED);
1581        assert_eq!(parse_css_color("blue").unwrap(), ColorU::BLUE);
1582        assert_eq!(parse_css_color("transparent").unwrap(), ColorU::TRANSPARENT);
1583        assert_eq!(
1584            parse_css_color("rebeccapurple").unwrap(),
1585            ColorU::new_rgb(102, 51, 153)
1586        );
1587    }
1588
1589    #[test]
1590    fn test_parse_color_hex() {
1591        // 3-digit
1592        assert_eq!(parse_css_color("#f00").unwrap(), ColorU::RED);
1593        // 4-digit
1594        assert_eq!(
1595            parse_css_color("#f008").unwrap(),
1596            ColorU::new(255, 0, 0, 136)
1597        );
1598        // 6-digit
1599        assert_eq!(parse_css_color("#00ff00").unwrap(), ColorU::GREEN);
1600        // 8-digit
1601        assert_eq!(
1602            parse_css_color("#0000ff80").unwrap(),
1603            ColorU::new(0, 0, 255, 128)
1604        );
1605        // Uppercase
1606        assert_eq!(
1607            parse_css_color("#FFC0CB").unwrap(),
1608            ColorU::new_rgb(255, 192, 203)
1609        ); // Pink
1610    }
1611
1612    #[test]
1613    fn test_parse_color_rgb() {
1614        assert_eq!(parse_css_color("rgb(255, 0, 0)").unwrap(), ColorU::RED);
1615        assert_eq!(
1616            parse_css_color("rgba(0, 255, 0, 0.5)").unwrap(),
1617            ColorU::new(0, 255, 0, 128)
1618        );
1619        assert_eq!(
1620            parse_css_color("rgba(10, 20, 30, 1)").unwrap(),
1621            ColorU::new_rgb(10, 20, 30)
1622        );
1623        assert_eq!(parse_css_color("rgb( 0 , 0 , 0 )").unwrap(), ColorU::BLACK);
1624    }
1625
1626    #[test]
1627    fn test_parse_color_hsl() {
1628        assert_eq!(parse_css_color("hsl(0, 100%, 50%)").unwrap(), ColorU::RED);
1629        assert_eq!(
1630            parse_css_color("hsl(120, 100%, 50%)").unwrap(),
1631            ColorU::GREEN
1632        );
1633        assert_eq!(
1634            parse_css_color("hsla(240, 100%, 50%, 0.5)").unwrap(),
1635            ColorU::new(0, 0, 255, 128)
1636        );
1637        assert_eq!(parse_css_color("hsl(0, 0%, 0%)").unwrap(), ColorU::BLACK);
1638    }
1639
1640    #[test]
1641    fn test_parse_color_errors() {
1642        assert!(parse_css_color("redd").is_err());
1643        assert!(parse_css_color("#12345").is_err()); // Invalid length
1644        assert!(parse_css_color("#ggg").is_err()); // Invalid hex digit
1645        assert!(parse_css_color("rgb(255, 0)").is_err()); // Missing component
1646        assert!(parse_css_color("rgba(255, 0, 0, 2)").is_err()); // Alpha out of range
1647        assert!(parse_css_color("rgb(256, 0, 0)").is_err()); // Value out of range
1648                                                             // Modern CSS allows both hsl(0, 100%, 50%) and hsl(0 100 50)
1649        assert!(parse_css_color("hsl(0, 100, 50%)").is_ok()); // Valid in modern CSS
1650        assert!(parse_css_color("rgb(255 0 0)").is_err()); // Missing commas (this implementation
1651                                                           // requires commas)
1652    }
1653
1654    #[test]
1655    fn test_parse_system_colors() {
1656        // Test parsing system color syntax
1657        assert_eq!(
1658            parse_color_or_system("system:accent").unwrap(),
1659            ColorOrSystem::System(SystemColorRef::Accent)
1660        );
1661        assert_eq!(
1662            parse_color_or_system("system:text").unwrap(),
1663            ColorOrSystem::System(SystemColorRef::Text)
1664        );
1665        assert_eq!(
1666            parse_color_or_system("system:background").unwrap(),
1667            ColorOrSystem::System(SystemColorRef::Background)
1668        );
1669        assert_eq!(
1670            parse_color_or_system("system:selection-background").unwrap(),
1671            ColorOrSystem::System(SystemColorRef::SelectionBackground)
1672        );
1673        assert_eq!(
1674            parse_color_or_system("system:selection-text").unwrap(),
1675            ColorOrSystem::System(SystemColorRef::SelectionText)
1676        );
1677        assert_eq!(
1678            parse_color_or_system("system:accent-text").unwrap(),
1679            ColorOrSystem::System(SystemColorRef::AccentText)
1680        );
1681        assert_eq!(
1682            parse_color_or_system("system:button-face").unwrap(),
1683            ColorOrSystem::System(SystemColorRef::ButtonFace)
1684        );
1685        assert_eq!(
1686            parse_color_or_system("system:button-text").unwrap(),
1687            ColorOrSystem::System(SystemColorRef::ButtonText)
1688        );
1689        assert_eq!(
1690            parse_color_or_system("system:window-background").unwrap(),
1691            ColorOrSystem::System(SystemColorRef::WindowBackground)
1692        );
1693        
1694        // Invalid system color should error
1695        assert!(parse_color_or_system("system:invalid").is_err());
1696        
1697        // Regular colors should still work
1698        assert_eq!(
1699            parse_color_or_system("red").unwrap(),
1700            ColorOrSystem::Color(ColorU::RED)
1701        );
1702        assert_eq!(
1703            parse_color_or_system("#ff0000").unwrap(),
1704            ColorOrSystem::Color(ColorU::RED)
1705        );
1706    }
1707
1708    #[test]
1709    fn test_system_color_resolution() {
1710        use crate::system::SystemColors;
1711        
1712        let system_colors = SystemColors {
1713            text: OptionColorU::Some(ColorU::BLACK),
1714            secondary_text: OptionColorU::None,
1715            tertiary_text: OptionColorU::None,
1716            background: OptionColorU::Some(ColorU::WHITE),
1717            accent: OptionColorU::Some(ColorU::new_rgb(0, 122, 255)), // macOS blue
1718            accent_text: OptionColorU::Some(ColorU::WHITE),
1719            button_face: OptionColorU::Some(ColorU::new_rgb(240, 240, 240)),
1720            button_text: OptionColorU::Some(ColorU::BLACK),
1721            disabled_text: OptionColorU::None,
1722            window_background: OptionColorU::Some(ColorU::WHITE),
1723            under_page_background: OptionColorU::None,
1724            selection_background: OptionColorU::Some(ColorU::new_rgb(0, 120, 215)),
1725            selection_text: OptionColorU::Some(ColorU::WHITE),
1726            selection_background_inactive: OptionColorU::None,
1727            selection_text_inactive: OptionColorU::None,
1728            link: OptionColorU::None,
1729            separator: OptionColorU::None,
1730            grid: OptionColorU::None,
1731            find_highlight: OptionColorU::None,
1732            sidebar_background: OptionColorU::None,
1733            sidebar_selection: OptionColorU::None,
1734        };
1735        
1736        // Test resolution of system colors
1737        let accent_ref = ColorOrSystem::System(SystemColorRef::Accent);
1738        let resolved = accent_ref.resolve(&system_colors, ColorU::GRAY);
1739        assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
1740        
1741        // Test resolution with fallback when color is not set
1742        let empty_colors = SystemColors::default();
1743        let resolved_fallback = accent_ref.resolve(&empty_colors, ColorU::GRAY);
1744        assert_eq!(resolved_fallback, ColorU::GRAY);
1745        
1746        // Test that concrete colors just return themselves
1747        let concrete = ColorOrSystem::Color(ColorU::RED);
1748        let resolved_concrete = concrete.resolve(&system_colors, ColorU::GRAY);
1749        assert_eq!(resolved_concrete, ColorU::RED);
1750    }
1751
1752    #[test]
1753    fn test_system_color_css_str() {
1754        assert_eq!(SystemColorRef::Accent.as_css_str(), "system:accent");
1755        assert_eq!(SystemColorRef::Text.as_css_str(), "system:text");
1756        assert_eq!(SystemColorRef::Background.as_css_str(), "system:background");
1757        assert_eq!(SystemColorRef::SelectionBackground.as_css_str(), "system:selection-background");
1758    }
1759}
1760
1761#[cfg(test)]
1762#[allow(clippy::float_cmp, clippy::unreadable_literal)]
1763mod autotest_generated {
1764    use super::*;
1765
1766    /// Every `ColorU` this module sweeps over. Chosen to hit the interesting
1767    /// channel boundaries (0 / 1 / 127 / 128 / 254 / 255) plus a few real colors.
1768    const SAMPLES: [ColorU; 10] = [
1769        ColorU { r: 0, g: 0, b: 0, a: 0 },
1770        ColorU { r: 0, g: 0, b: 0, a: 255 },
1771        ColorU { r: 255, g: 255, b: 255, a: 255 },
1772        ColorU { r: 255, g: 255, b: 255, a: 0 },
1773        ColorU { r: 1, g: 2, b: 3, a: 4 },
1774        ColorU { r: 127, g: 128, b: 129, a: 254 },
1775        ColorU { r: 254, g: 1, b: 128, a: 1 },
1776        ColorU { r: 128, g: 128, b: 128, a: 255 },
1777        ColorU { r: 255, g: 0, b: 0, a: 255 },
1778        ColorU { r: 13, g: 110, b: 253, a: 200 },
1779    ];
1780
1781    // =====================================================================
1782    // numeric: channel_to_u8 (private) — the one float→int cast in the file
1783    // =====================================================================
1784
1785    #[test]
1786    fn channel_to_u8_zero_and_negative_zero() {
1787        assert_eq!(channel_to_u8(0.0), 0);
1788        assert_eq!(channel_to_u8(-0.0), 0);
1789    }
1790
1791    #[test]
1792    fn channel_to_u8_truncates_toward_zero_and_does_not_round() {
1793        assert_eq!(channel_to_u8(0.9), 0);
1794        assert_eq!(channel_to_u8(127.5), 127);
1795        assert_eq!(channel_to_u8(254.999), 254);
1796        assert_eq!(channel_to_u8(255.0), 255);
1797        assert_eq!(channel_to_u8(255.9), 255);
1798    }
1799
1800    #[test]
1801    fn channel_to_u8_saturates_on_overflow_instead_of_wrapping() {
1802        assert_eq!(channel_to_u8(256.0), 255);
1803        assert_eq!(channel_to_u8(1e30), 255);
1804        assert_eq!(channel_to_u8(f32::MAX), 255);
1805    }
1806
1807    #[test]
1808    fn channel_to_u8_negative_saturates_to_zero() {
1809        assert_eq!(channel_to_u8(-0.5), 0);
1810        assert_eq!(channel_to_u8(-1.0), 0);
1811        assert_eq!(channel_to_u8(-1e30), 0);
1812        assert_eq!(channel_to_u8(f32::MIN), 0);
1813    }
1814
1815    #[test]
1816    fn channel_to_u8_nan_and_inf_are_defined_and_do_not_panic() {
1817        assert_eq!(channel_to_u8(f32::NAN), 0);
1818        assert_eq!(channel_to_u8(-f32::NAN), 0);
1819        assert_eq!(channel_to_u8(f32::INFINITY), 255);
1820        assert_eq!(channel_to_u8(f32::NEG_INFINITY), 0);
1821    }
1822
1823    #[test]
1824    fn channel_to_u8_subnormal_inputs_do_not_panic() {
1825        assert_eq!(channel_to_u8(f32::MIN_POSITIVE), 0);
1826        assert_eq!(channel_to_u8(1e-45), 0);
1827        assert_eq!(channel_to_u8(-1e-45), 0);
1828    }
1829
1830    // =====================================================================
1831    // constructors: rgba / rgb / new / new_rgb / with_alpha / with_alpha_f32
1832    // =====================================================================
1833
1834    #[test]
1835    fn rgba_fields_match_args_at_min_and_max() {
1836        let min = ColorU::rgba(0, 0, 0, 0);
1837        assert_eq!((min.r, min.g, min.b, min.a), (0, 0, 0, 0));
1838        let max = ColorU::rgba(u8::MAX, u8::MAX, u8::MAX, u8::MAX);
1839        assert_eq!((max.r, max.g, max.b, max.a), (255, 255, 255, 255));
1840        let mixed = ColorU::rgba(1, 2, 3, 4);
1841        assert_eq!((mixed.r, mixed.g, mixed.b, mixed.a), (1, 2, 3, 4));
1842    }
1843
1844    #[test]
1845    fn rgb_defaults_alpha_to_opaque() {
1846        assert_eq!(ColorU::rgb(0, 0, 0), ColorU::BLACK);
1847        assert_eq!(ColorU::rgb(1, 2, 3).a, ColorU::ALPHA_OPAQUE);
1848        assert_eq!(ColorU::rgb(u8::MAX, u8::MAX, u8::MAX), ColorU::WHITE);
1849    }
1850
1851    #[test]
1852    fn new_and_new_rgb_are_exact_aliases() {
1853        for c in SAMPLES {
1854            assert_eq!(ColorU::new(c.r, c.g, c.b, c.a), ColorU::rgba(c.r, c.g, c.b, c.a));
1855            assert_eq!(ColorU::new_rgb(c.r, c.g, c.b), ColorU::rgb(c.r, c.g, c.b));
1856        }
1857    }
1858
1859    #[test]
1860    fn with_alpha_keeps_rgb_for_every_alpha() {
1861        let base = ColorU::rgba(13, 110, 253, 7);
1862        for a in 0..=u8::MAX {
1863            let c = base.with_alpha(a);
1864            assert_eq!((c.r, c.g, c.b), (base.r, base.g, base.b));
1865            assert_eq!(c.a, a);
1866        }
1867    }
1868
1869    #[test]
1870    fn with_alpha_f32_clamps_out_of_range_and_nan() {
1871        let base = ColorU::rgb(1, 2, 3);
1872        assert_eq!(base.with_alpha_f32(0.0).a, 0);
1873        assert_eq!(base.with_alpha_f32(1.0).a, 255);
1874        // Out of range clamps rather than wrapping.
1875        assert_eq!(base.with_alpha_f32(-1.0).a, 0);
1876        assert_eq!(base.with_alpha_f32(-1e30).a, 0);
1877        assert_eq!(base.with_alpha_f32(2.0).a, 255);
1878        assert_eq!(base.with_alpha_f32(1e30).a, 255);
1879        assert_eq!(base.with_alpha_f32(f32::INFINITY).a, 255);
1880        assert_eq!(base.with_alpha_f32(f32::NEG_INFINITY).a, 0);
1881        // `clamp` propagates NaN, and `NaN as u8` is 0 — fully transparent, not a panic.
1882        assert_eq!(base.with_alpha_f32(f32::NAN).a, 0);
1883        // RGB is never touched, whatever the alpha input.
1884        for a in [-1.0, 0.0, 0.5, 1.0, 2.0, f32::NAN, f32::INFINITY] {
1885            let c = base.with_alpha_f32(a);
1886            assert_eq!((c.r, c.g, c.b), (1, 2, 3));
1887        }
1888    }
1889
1890    #[test]
1891    fn with_alpha_f32_truncates_rather_than_rounds() {
1892        // 0.5 * 255.0 == 127.5, and `as u8` truncates => 127.
1893        // NOTE: the `rgba(..., 0.5)` parser rounds the same value to 128
1894        // (`parse_alpha_component` calls `.round()` first). See report.
1895        assert_eq!(ColorU::rgb(0, 0, 0).with_alpha_f32(0.5).a, 127);
1896    }
1897
1898    // =====================================================================
1899    // numeric: interpolate / lighten / darken / mix
1900    // =====================================================================
1901
1902    #[test]
1903    fn interpolate_endpoints_are_exact() {
1904        for a in SAMPLES {
1905            for b in SAMPLES {
1906                assert_eq!(a.interpolate(&b, 0.0), a, "t=0 must return self");
1907                assert_eq!(a.interpolate(&b, 1.0), b, "t=1 must return other");
1908            }
1909        }
1910    }
1911
1912    #[test]
1913    fn interpolate_midpoint_rounds_half_away_from_zero() {
1914        // 0 + 255 * 0.5 = 127.5, roundf => 128.
1915        assert_eq!(
1916            ColorU::BLACK.interpolate(&ColorU::WHITE, 0.5),
1917            ColorU::rgba(128, 128, 128, 255)
1918        );
1919    }
1920
1921    #[test]
1922    fn interpolate_is_symmetric_under_swapped_endpoints() {
1923        for a in SAMPLES {
1924            for b in SAMPLES {
1925                assert_eq!(a.interpolate(&b, 0.25), b.interpolate(&a, 0.75));
1926            }
1927        }
1928    }
1929
1930    #[test]
1931    fn interpolate_nan_t_is_defined_and_does_not_panic() {
1932        // t = NaN makes every channel NaN, and `NaN as u8` == 0.
1933        for a in SAMPLES {
1934            for b in SAMPLES {
1935                assert_eq!(a.interpolate(&b, f32::NAN), ColorU::rgba(0, 0, 0, 0));
1936            }
1937        }
1938    }
1939
1940    #[test]
1941    fn interpolate_infinite_t_saturates_differing_channels() {
1942        // Channels that differ run off to +/-inf and saturate at the u8 bounds.
1943        let c = ColorU::rgba(0, 0, 0, 0).interpolate(&ColorU::WHITE, f32::INFINITY);
1944        assert_eq!(c, ColorU::rgba(255, 255, 255, 255));
1945        let c = ColorU::WHITE.interpolate(&ColorU::rgba(0, 0, 0, 0), f32::INFINITY);
1946        assert_eq!(c, ColorU::rgba(0, 0, 0, 0));
1947    }
1948
1949    #[test]
1950    fn interpolate_infinite_t_zeroes_equal_channels() {
1951        // Where a channel is EQUAL in both colors the delta is 0.0, and
1952        // `0.0 * inf == NaN` => that channel collapses to 0 instead of
1953        // staying put. Both endpoints here are alpha=255, so alpha => 0.
1954        let c = ColorU::BLACK.interpolate(&ColorU::WHITE, f32::INFINITY);
1955        assert_eq!(c, ColorU::rgba(255, 255, 255, 0));
1956        // Interpolating a color with ITSELF at t=inf wipes it out entirely.
1957        assert_eq!(
1958            ColorU::RED.interpolate(&ColorU::RED, f32::INFINITY),
1959            ColorU::rgba(0, 0, 0, 0)
1960        );
1961    }
1962
1963    #[test]
1964    fn interpolate_out_of_range_t_saturates_instead_of_wrapping() {
1965        // Extrapolating past the endpoints overshoots the u8 range; the cast must
1966        // saturate, not wrap (0 + 255*2 == 510 -> 255, not 254).
1967        assert_eq!(
1968            ColorU::BLACK.interpolate(&ColorU::WHITE, 2.0),
1969            ColorU::rgba(255, 255, 255, 255)
1970        );
1971        assert_eq!(
1972            ColorU::WHITE.interpolate(&ColorU::BLACK, -1.0),
1973            ColorU::rgba(255, 255, 255, 255)
1974        );
1975        assert_eq!(
1976            ColorU::WHITE.interpolate(&ColorU::BLACK, 2.0),
1977            ColorU::rgba(0, 0, 0, 255)
1978        );
1979        assert_eq!(
1980            ColorU::BLACK.interpolate(&ColorU::WHITE, -1.0),
1981            ColorU::rgba(0, 0, 0, 255)
1982        );
1983        // And the whole sample matrix must stay panic-free and deterministic.
1984        for t in [-1e30, -1.0, -0.5, 1.5, 2.0, 1e30] {
1985            for a in SAMPLES {
1986                for b in SAMPLES {
1987                    assert_eq!(a.interpolate(&b, t), a.interpolate(&b, t));
1988                }
1989            }
1990        }
1991    }
1992
1993    #[test]
1994    fn lighten_and_darken_clamp_the_amount() {
1995        let base = ColorU::rgba(128, 128, 128, 77);
1996        // Below 0 clamps to 0 => unchanged.
1997        assert_eq!(base.lighten(0.0), base);
1998        assert_eq!(base.darken(0.0), base);
1999        assert_eq!(base.lighten(-1.0), base);
2000        assert_eq!(base.darken(-1e30), base);
2001        assert_eq!(base.lighten(f32::NEG_INFINITY), base);
2002        // Above 1 clamps to 1 => full white / full black, alpha preserved.
2003        assert_eq!(base.lighten(1.0), ColorU::rgba(255, 255, 255, 77));
2004        assert_eq!(base.lighten(2.0), ColorU::rgba(255, 255, 255, 77));
2005        assert_eq!(base.lighten(f32::INFINITY), ColorU::rgba(255, 255, 255, 77));
2006        assert_eq!(base.darken(1.0), ColorU::rgba(0, 0, 0, 77));
2007        assert_eq!(base.darken(1e30), ColorU::rgba(0, 0, 0, 77));
2008        assert_eq!(base.darken(f32::INFINITY), ColorU::rgba(0, 0, 0, 77));
2009    }
2010
2011    #[test]
2012    fn lighten_and_darken_always_preserve_alpha() {
2013        for c in SAMPLES {
2014            for amount in [-1.0, 0.0, 0.3, 1.0, 2.0, f32::NAN, f32::INFINITY] {
2015                assert_eq!(c.lighten(amount).a, c.a);
2016                assert_eq!(c.darken(amount).a, c.a);
2017            }
2018        }
2019    }
2020
2021    #[test]
2022    fn lighten_nan_amount_is_defined_and_does_not_panic() {
2023        // `f32::clamp` propagates NaN, so the RGB channels collapse to 0 while
2024        // alpha is explicitly restored afterwards.
2025        let c = ColorU::rgba(255, 0, 0, 200);
2026        assert_eq!(c.lighten(f32::NAN), ColorU::rgba(0, 0, 0, 200));
2027        assert_eq!(c.darken(f32::NAN), ColorU::rgba(0, 0, 0, 200));
2028    }
2029
2030    #[test]
2031    fn mix_clamps_ratio_to_the_endpoints() {
2032        let a = ColorU::rgba(10, 20, 30, 40);
2033        let b = ColorU::rgba(200, 210, 220, 230);
2034        assert_eq!(a.mix(&b, 0.0), a);
2035        assert_eq!(a.mix(&b, 1.0), b);
2036        assert_eq!(a.mix(&b, -1.0), a);
2037        assert_eq!(a.mix(&b, f32::NEG_INFINITY), a);
2038        assert_eq!(a.mix(&b, 2.0), b);
2039        assert_eq!(a.mix(&b, 1e30), b);
2040        assert_eq!(a.mix(&b, f32::INFINITY), b);
2041    }
2042
2043    #[test]
2044    fn mix_nan_ratio_is_defined_and_does_not_panic() {
2045        // Unlike lighten/darken, mix does NOT restore alpha => fully transparent.
2046        assert_eq!(
2047            ColorU::RED.mix(&ColorU::BLUE, f32::NAN),
2048            ColorU::rgba(0, 0, 0, 0)
2049        );
2050    }
2051
2052    // =====================================================================
2053    // numeric: srgb_to_linear (private)
2054    // =====================================================================
2055
2056    #[test]
2057    fn srgb_to_linear_endpoints_and_monotonicity() {
2058        assert_eq!(ColorU::srgb_to_linear(0.0), 0.0);
2059        assert!((ColorU::srgb_to_linear(1.0) - 1.0).abs() < 1e-5);
2060        // Monotonically non-decreasing over the whole 8-bit ramp.
2061        let mut prev = f32::NEG_INFINITY;
2062        for i in 0..=255u16 {
2063            let v = ColorU::srgb_to_linear(f32::from(i) / 255.0);
2064            assert!(v >= prev, "srgb_to_linear not monotonic at {i}");
2065            assert!((0.0..=1.0).contains(&v), "out of range at {i}: {v}");
2066            prev = v;
2067        }
2068    }
2069
2070    #[test]
2071    fn srgb_to_linear_handles_the_piecewise_boundary() {
2072        // The branch flips at c == 0.03928 (linear below, gamma above).
2073        let below = ColorU::srgb_to_linear(0.03928);
2074        assert!((below - 0.03928 / 12.92).abs() < 1e-9);
2075        let above = ColorU::srgb_to_linear(0.03929);
2076        assert!(above > below, "must not go backwards across the boundary");
2077    }
2078
2079    #[test]
2080    fn srgb_to_linear_nan_inf_and_negative_do_not_panic() {
2081        assert!(ColorU::srgb_to_linear(f32::NAN).is_nan());
2082        assert_eq!(ColorU::srgb_to_linear(f32::INFINITY), f32::INFINITY);
2083        assert_eq!(ColorU::srgb_to_linear(f32::NEG_INFINITY), f32::NEG_INFINITY);
2084        // Negative inputs take the linear branch and stay negative (deterministic).
2085        assert!(ColorU::srgb_to_linear(-1.0) < 0.0);
2086        assert_eq!(ColorU::srgb_to_linear(-0.0), -0.0);
2087    }
2088
2089    // =====================================================================
2090    // getters: luminance / relative_luminance / is_light / is_dark
2091    // =====================================================================
2092
2093    #[test]
2094    fn luminance_endpoints_and_range() {
2095        assert!((ColorU::BLACK.luminance() - 0.0).abs() < 1e-6);
2096        assert!((ColorU::WHITE.luminance() - 1.0).abs() < 1e-6);
2097        for r in (0..=255u16).step_by(17) {
2098            for g in (0..=255u16).step_by(51) {
2099                for b in (0..=255u16).step_by(85) {
2100                    #[allow(clippy::cast_possible_truncation)]
2101                    let l = ColorU::rgb(r as u8, g as u8, b as u8).luminance();
2102                    assert!(l.is_finite() && (-1e-6..=1.000_001).contains(&l), "luminance {l}");
2103                }
2104            }
2105        }
2106    }
2107
2108    #[test]
2109    fn luminance_ignores_alpha() {
2110        for a in [0u8, 1, 128, 254, 255] {
2111            assert!((ColorU::rgba(10, 20, 30, a).luminance()
2112                - ColorU::rgba(10, 20, 30, 255).luminance())
2113                .abs()
2114                < 1e-9);
2115        }
2116    }
2117
2118    #[test]
2119    fn relative_luminance_endpoints_and_range() {
2120        assert!((ColorU::BLACK.relative_luminance() - 0.0).abs() < 1e-6);
2121        assert!((ColorU::WHITE.relative_luminance() - 1.0).abs() < 1e-6);
2122        for i in 0..=255u16 {
2123            #[allow(clippy::cast_possible_truncation)]
2124            let l = ColorU::rgb(i as u8, i as u8, i as u8).relative_luminance();
2125            assert!(l.is_finite(), "non-finite relative_luminance at {i}");
2126            assert!((-1e-6..=1.000_001).contains(&l), "out of range at {i}: {l}");
2127        }
2128    }
2129
2130    #[test]
2131    fn relative_luminance_is_monotonic_along_the_gray_ramp() {
2132        let mut prev = f32::NEG_INFINITY;
2133        for i in 0..=255u16 {
2134            #[allow(clippy::cast_possible_truncation)]
2135            let l = ColorU::rgb(i as u8, i as u8, i as u8).relative_luminance();
2136            assert!(l >= prev, "gray ramp not monotonic at {i}");
2137            prev = l;
2138        }
2139    }
2140
2141    #[test]
2142    fn is_light_and_is_dark_are_exact_complements() {
2143        // The two predicates split at exactly 0.5 with no overlap and no gap,
2144        // for every single 8-bit color on the gray ramp plus the samples.
2145        for i in 0..=255u16 {
2146            #[allow(clippy::cast_possible_truncation)]
2147            let c = ColorU::rgb(i as u8, i as u8, i as u8);
2148            assert_ne!(c.is_light(), c.is_dark(), "not complementary at {i}");
2149        }
2150        for c in SAMPLES {
2151            assert_ne!(c.is_light(), c.is_dark());
2152        }
2153    }
2154
2155    #[test]
2156    fn is_light_and_is_dark_known_values() {
2157        assert!(ColorU::WHITE.is_light());
2158        assert!(!ColorU::WHITE.is_dark());
2159        assert!(ColorU::BLACK.is_dark());
2160        assert!(!ColorU::BLACK.is_light());
2161        // Default (BLACK) is dark.
2162        assert!(ColorU::default().is_dark());
2163        // Mid gray is "dark" under WCAG relative luminance (~0.216, not 0.5).
2164        assert!(ColorU::rgb(128, 128, 128).is_dark());
2165    }
2166
2167    // =====================================================================
2168    // contrast: contrast_ratio / meets_wcag_* / best_contrast_text
2169    // =====================================================================
2170
2171    #[test]
2172    fn contrast_ratio_is_symmetric() {
2173        for a in SAMPLES {
2174            for b in SAMPLES {
2175                let ab = a.contrast_ratio(&b);
2176                let ba = b.contrast_ratio(&a);
2177                assert!((ab - ba).abs() < 1e-6, "asymmetric: {ab} vs {ba}");
2178            }
2179        }
2180    }
2181
2182    #[test]
2183    fn contrast_ratio_stays_within_1_and_21() {
2184        for a in SAMPLES {
2185            for b in SAMPLES {
2186                let r = a.contrast_ratio(&b);
2187                assert!(r.is_finite(), "non-finite contrast ratio");
2188                assert!((0.999..=21.001).contains(&r), "contrast ratio out of range: {r}");
2189            }
2190            // Self-contrast is exactly 1.
2191            assert!((a.contrast_ratio(&a) - 1.0).abs() < 1e-6);
2192        }
2193        // Max contrast (fp gives 20.999998, not a clean 21.0).
2194        let max = ColorU::BLACK.contrast_ratio(&ColorU::WHITE);
2195        assert!((max - 21.0).abs() < 0.01, "black/white contrast was {max}");
2196    }
2197
2198    #[test]
2199    fn meets_wcag_thresholds_agree_with_contrast_ratio() {
2200        for a in SAMPLES {
2201            for b in SAMPLES {
2202                let r = a.contrast_ratio(&b);
2203                assert_eq!(a.meets_wcag_aa(&b), r >= 4.5);
2204                assert_eq!(a.meets_wcag_aa_large(&b), r >= 3.0);
2205                assert_eq!(a.meets_wcag_aaa(&b), r >= 7.0);
2206                assert_eq!(a.meets_wcag_aaa_large(&b), r >= 4.5);
2207            }
2208        }
2209    }
2210
2211    #[test]
2212    fn meets_wcag_known_true_and_false() {
2213        assert!(ColorU::BLACK.meets_wcag_aa(&ColorU::WHITE));
2214        assert!(ColorU::BLACK.meets_wcag_aaa(&ColorU::WHITE));
2215        assert!(ColorU::WHITE.meets_wcag_aa_large(&ColorU::BLACK));
2216        // A color has no contrast against itself.
2217        assert!(!ColorU::RED.meets_wcag_aa(&ColorU::RED));
2218        assert!(!ColorU::RED.meets_wcag_aa_large(&ColorU::RED));
2219        assert!(!ColorU::WHITE.meets_wcag_aaa(&ColorU::WHITE));
2220    }
2221
2222    #[test]
2223    fn best_contrast_text_only_ever_returns_black_or_white() {
2224        for c in SAMPLES {
2225            let t = c.best_contrast_text();
2226            assert!(t == ColorU::WHITE || t == ColorU::BLACK, "got {t:?}");
2227            // contrast_text is documented as an alias.
2228            assert_eq!(c.contrast_text(), t);
2229        }
2230        for i in 0..=255u16 {
2231            #[allow(clippy::cast_possible_truncation)]
2232            let c = ColorU::rgb(i as u8, i as u8, i as u8);
2233            let t = c.best_contrast_text();
2234            assert!(t == ColorU::WHITE || t == ColorU::BLACK);
2235        }
2236    }
2237
2238    #[test]
2239    fn best_contrast_text_picks_the_higher_contrast_option() {
2240        assert_eq!(ColorU::WHITE.best_contrast_text(), ColorU::BLACK);
2241        assert_eq!(ColorU::BLACK.best_contrast_text(), ColorU::WHITE);
2242        for c in SAMPLES {
2243            let t = c.best_contrast_text();
2244            let other = if t == ColorU::WHITE { ColorU::BLACK } else { ColorU::WHITE };
2245            assert!(
2246                c.contrast_ratio(&t) >= c.contrast_ratio(&other),
2247                "{c:?} picked the lower-contrast text color"
2248            );
2249        }
2250    }
2251
2252    // =====================================================================
2253    // numeric: ensure_contrast (binary search — must terminate + saturate)
2254    // =====================================================================
2255
2256    #[test]
2257    fn ensure_contrast_returns_self_when_already_compliant() {
2258        // 21:1 already, nothing to do.
2259        assert_eq!(
2260            ColorU::BLACK.ensure_contrast(&ColorU::WHITE, 4.5),
2261            ColorU::BLACK
2262        );
2263        let gray = ColorU::rgb(128, 128, 128);
2264        // 5.3:1 against black already clears 4.5.
2265        assert_eq!(gray.ensure_contrast(&ColorU::BLACK, 4.5), gray);
2266    }
2267
2268    #[test]
2269    fn ensure_contrast_actually_reaches_the_requested_ratio() {
2270        let gray = ColorU::rgb(128, 128, 128);
2271        let fixed = gray.ensure_contrast(&ColorU::WHITE, 4.5);
2272        assert!(
2273            fixed.contrast_ratio(&ColorU::WHITE) >= 4.5,
2274            "adjusted color {fixed:?} still fails 4.5:1"
2275        );
2276        // Darkening against a light background must not make it lighter.
2277        assert!(fixed.r <= gray.r && fixed.g <= gray.g && fixed.b <= gray.b);
2278    }
2279
2280    #[test]
2281    fn ensure_contrast_degenerate_min_ratios_return_self() {
2282        let gray = ColorU::rgb(128, 128, 128);
2283        // <= current ratio: early return.
2284        assert_eq!(gray.ensure_contrast(&ColorU::WHITE, 0.0), gray);
2285        assert_eq!(gray.ensure_contrast(&ColorU::WHITE, -1.0), gray);
2286        assert_eq!(gray.ensure_contrast(&ColorU::WHITE, f32::NEG_INFINITY), gray);
2287        // Unsatisfiable / NaN: every comparison is false, so `result` never
2288        // moves off `*self`. Terminates (fixed 16 iterations), never hangs.
2289        assert_eq!(gray.ensure_contrast(&ColorU::WHITE, f32::INFINITY), gray);
2290        assert_eq!(gray.ensure_contrast(&ColorU::WHITE, f32::NAN), gray);
2291        assert_eq!(gray.ensure_contrast(&ColorU::WHITE, 1e30), gray);
2292    }
2293
2294    #[test]
2295    fn ensure_contrast_terminates_for_every_sample_pair() {
2296        for c in SAMPLES {
2297            for bg in SAMPLES {
2298                for min in [1.0, 3.0, 4.5, 7.0, 21.0, 25.0] {
2299                    let out = c.ensure_contrast(&bg, min);
2300                    // Alpha is carried through lighten/darken untouched.
2301                    assert_eq!(out.a, c.a);
2302                }
2303            }
2304        }
2305    }
2306
2307    // =====================================================================
2308    // APCA
2309    // =====================================================================
2310
2311    #[test]
2312    fn apca_contrast_sign_encodes_polarity() {
2313        let dark_on_light = ColorU::BLACK.apca_contrast(&ColorU::WHITE);
2314        let light_on_dark = ColorU::WHITE.apca_contrast(&ColorU::BLACK);
2315        assert!(dark_on_light > 0.0, "black-on-white should be positive");
2316        assert!(light_on_dark < 0.0, "white-on-black should be negative");
2317        assert!(dark_on_light.is_finite() && light_on_dark.is_finite());
2318    }
2319
2320    #[test]
2321    fn apca_contrast_of_a_color_against_itself_is_zero() {
2322        for c in SAMPLES {
2323            assert_eq!(c.apca_contrast(&c), 0.0, "{c:?} vs itself");
2324        }
2325    }
2326
2327    #[test]
2328    fn apca_contrast_is_finite_for_every_sample_pair() {
2329        for a in SAMPLES {
2330            for b in SAMPLES {
2331                assert!(a.apca_contrast(&b).is_finite(), "{a:?} on {b:?}");
2332            }
2333        }
2334    }
2335
2336    #[test]
2337    fn meets_apca_thresholds_agree_with_apca_contrast() {
2338        for a in SAMPLES {
2339            for b in SAMPLES {
2340                let lc = libm::fabsf(a.apca_contrast(&b));
2341                assert_eq!(a.meets_apca_body(&b), lc >= 60.0);
2342                assert_eq!(a.meets_apca_large(&b), lc >= 45.0);
2343            }
2344        }
2345        assert!(ColorU::BLACK.meets_apca_body(&ColorU::WHITE));
2346        assert!(ColorU::BLACK.meets_apca_large(&ColorU::WHITE));
2347        assert!(!ColorU::RED.meets_apca_body(&ColorU::RED));
2348        assert!(!ColorU::RED.meets_apca_large(&ColorU::RED));
2349    }
2350
2351    // =====================================================================
2352    // getters / predicates: hover_variant, active_variant, invert,
2353    //                       to_grayscale, has_alpha, to_hash
2354    // =====================================================================
2355
2356    #[test]
2357    fn hover_and_active_variants_preserve_alpha_and_never_panic() {
2358        for c in SAMPLES {
2359            assert_eq!(c.hover_variant().a, c.a);
2360            assert_eq!(c.active_variant().a, c.a);
2361        }
2362        // Light colors get darker, dark colors get lighter.
2363        assert!(ColorU::WHITE.hover_variant().r < 255);
2364        assert!(ColorU::BLACK.hover_variant().r > 0);
2365        assert!(ColorU::WHITE.active_variant().r < ColorU::WHITE.hover_variant().r);
2366    }
2367
2368    #[test]
2369    fn invert_is_its_own_inverse() {
2370        for c in SAMPLES {
2371            assert_eq!(c.invert().invert(), c);
2372            assert_eq!(c.invert().a, c.a, "invert must keep alpha");
2373        }
2374        assert_eq!(ColorU::BLACK.invert(), ColorU::WHITE);
2375        assert_eq!(ColorU::WHITE.invert(), ColorU::BLACK);
2376    }
2377
2378    #[test]
2379    fn invert_does_not_underflow_at_the_channel_bounds() {
2380        // `255 - self.r` on u8 would panic in debug on underflow; it cannot,
2381        // but pin the boundary values anyway.
2382        assert_eq!(ColorU::rgba(0, 0, 0, 0).invert(), ColorU::rgba(255, 255, 255, 0));
2383        assert_eq!(
2384            ColorU::rgba(255, 255, 255, 255).invert(),
2385            ColorU::rgba(0, 0, 0, 255)
2386        );
2387    }
2388
2389    #[test]
2390    fn to_grayscale_produces_equal_channels_and_keeps_alpha() {
2391        for c in SAMPLES {
2392            let g = c.to_grayscale();
2393            assert_eq!(g.r, g.g);
2394            assert_eq!(g.g, g.b);
2395            assert_eq!(g.a, c.a);
2396        }
2397    }
2398
2399    #[test]
2400    fn to_grayscale_boundary_values() {
2401        assert_eq!(ColorU::BLACK.to_grayscale(), ColorU::BLACK);
2402        assert_eq!(ColorU::WHITE.to_grayscale(), ColorU::WHITE);
2403        assert_eq!(
2404            ColorU::rgb(128, 128, 128).to_grayscale(),
2405            ColorU::rgb(128, 128, 128)
2406        );
2407        // An already-gray color is (near enough) a fixed point of to_grayscale:
2408        // the BT.601 weights sum to 1.0, so only the truncating cast can shave
2409        // off at most one level.
2410        for i in 0..=255u16 {
2411            #[allow(clippy::cast_possible_truncation)]
2412            let c = ColorU::rgb(i as u8, i as u8, i as u8);
2413            let drift = i32::from(c.r) - i32::from(c.to_grayscale().r);
2414            assert!((0..=1).contains(&drift), "gray {i} drifted by {drift}");
2415        }
2416    }
2417
2418    #[test]
2419    fn has_alpha_is_true_for_everything_but_255() {
2420        assert!(!ColorU::rgba(0, 0, 0, 255).has_alpha());
2421        assert!(!ColorU::WHITE.has_alpha());
2422        assert!(ColorU::rgba(0, 0, 0, 254).has_alpha());
2423        assert!(ColorU::TRANSPARENT.has_alpha());
2424        for a in 0..=u8::MAX {
2425            assert_eq!(ColorU::rgba(1, 2, 3, a).has_alpha(), a != 255);
2426        }
2427    }
2428
2429    #[test]
2430    fn to_hash_is_always_nine_lowercase_chars() {
2431        assert_eq!(ColorU::RED.to_hash(), "#ff0000ff");
2432        assert_eq!(ColorU::TRANSPARENT.to_hash(), "#00000000");
2433        assert_eq!(ColorU::rgba(1, 2, 3, 4).to_hash(), "#01020304");
2434        assert_eq!(ColorU::WHITE.to_hash(), "#ffffffff");
2435        for c in SAMPLES {
2436            let h = c.to_hash();
2437            assert_eq!(h.len(), 9, "{h} is not 9 bytes");
2438            assert!(h.starts_with('#'));
2439            assert!(
2440                h[1..].chars().all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase()),
2441                "{h} is not lowercase hex"
2442            );
2443        }
2444    }
2445
2446    // =====================================================================
2447    // serializer: Display for ColorU / ColorF
2448    // =====================================================================
2449
2450    #[test]
2451    fn coloru_display_is_well_formed() {
2452        assert_eq!(format!("{}", ColorU::RED), "rgba(255, 0, 0, 1)");
2453        assert_eq!(format!("{}", ColorU::TRANSPARENT), "rgba(0, 0, 0, 0)");
2454        assert_eq!(format!("{}", ColorU::default()), "rgba(0, 0, 0, 1)");
2455        // Alpha is normalized to 0.0..=1.0.
2456        assert_eq!(format!("{}", ColorU::rgba(1, 2, 3, 128)), "rgba(1, 2, 3, 0.5019608)");
2457        for c in SAMPLES {
2458            let s = format!("{c}");
2459            assert!(s.starts_with("rgba(") && s.ends_with(')') && s.len() > 6);
2460        }
2461    }
2462
2463    #[test]
2464    fn colorf_display_survives_nan_and_inf() {
2465        assert_eq!(format!("{}", ColorF::BLACK), "rgba(0, 0, 0, 1)");
2466        assert_eq!(format!("{}", ColorF::WHITE), "rgba(255, 255, 255, 1)");
2467        assert_eq!(format!("{}", ColorF::TRANSPARENT), "rgba(0, 0, 0, 0)");
2468        assert_eq!(format!("{}", ColorF::default()), format!("{}", ColorF::BLACK));
2469
2470        let nan = ColorF { r: f32::NAN, g: f32::NAN, b: f32::NAN, a: f32::NAN };
2471        assert_eq!(format!("{nan}"), "rgba(NaN, NaN, NaN, NaN)");
2472
2473        let inf = ColorF {
2474            r: f32::INFINITY,
2475            g: f32::NEG_INFINITY,
2476            b: f32::MAX,
2477            a: f32::INFINITY,
2478        };
2479        let s = format!("{inf}");
2480        assert!(s.starts_with("rgba(inf, -inf, ") && s.ends_with(", inf)"), "{s}");
2481    }
2482
2483    // =====================================================================
2484    // round-trip: ColorU <-> ColorF, to_hash -> parse, Display -> parse
2485    // =====================================================================
2486
2487    #[test]
2488    fn coloru_to_colorf_and_back_is_lossless_for_all_256_channel_values() {
2489        for i in 0..=255u16 {
2490            #[allow(clippy::cast_possible_truncation)]
2491            let c = ColorU::rgba(i as u8, (255 - i) as u8, i as u8, (255 - i) as u8);
2492            let f: ColorF = c.into();
2493            let back: ColorU = f.into();
2494            assert_eq!(back, c, "round-trip lost information at {i}");
2495        }
2496    }
2497
2498    #[test]
2499    fn colorf_to_coloru_clamps_out_of_range_channels() {
2500        // > 1.0 is clamped by `.min(1.0)`.
2501        let over = ColorF { r: 2.0, g: 1e30, b: f32::INFINITY, a: 1.5 };
2502        assert_eq!(ColorU::from(over), ColorU::rgba(255, 255, 255, 255));
2503        // < 0.0 is NOT clamped by `.min`, but `as u8` saturates it to 0 anyway.
2504        let under = ColorF { r: -1.0, g: -1e30, b: f32::NEG_INFINITY, a: -0.5 };
2505        assert_eq!(ColorU::from(under), ColorU::rgba(0, 0, 0, 0));
2506    }
2507
2508    #[test]
2509    fn colorf_to_coloru_maps_nan_channels_to_255() {
2510        // `f32::min` returns the NON-NaN operand, so `NaN.min(1.0) == 1.0`,
2511        // and a NaN channel comes out fully saturated rather than 0.
2512        let nan = ColorF { r: f32::NAN, g: 0.0, b: 0.0, a: f32::NAN };
2513        assert_eq!(ColorU::from(nan), ColorU::rgba(255, 0, 0, 255));
2514    }
2515
2516    #[cfg(feature = "parser")]
2517    #[test]
2518    fn to_hash_round_trips_through_the_parser() {
2519        assert_eq!(parse_css_color(&ColorU::RED.to_hash()).unwrap(), ColorU::RED);
2520        for r in (0..=255u16).step_by(51) {
2521            for g in (0..=255u16).step_by(51) {
2522                for b in (0..=255u16).step_by(85) {
2523                    for a in (0..=255u16).step_by(85) {
2524                        #[allow(clippy::cast_possible_truncation)]
2525                        let c = ColorU::rgba(r as u8, g as u8, b as u8, a as u8);
2526                        let encoded = c.to_hash();
2527                        let decoded = parse_css_color(&encoded)
2528                            .unwrap_or_else(|e| panic!("{encoded} failed to parse: {e}"));
2529                        assert_eq!(decoded, c, "{encoded} decoded to the wrong color");
2530                    }
2531                }
2532            }
2533        }
2534    }
2535
2536    #[cfg(feature = "parser")]
2537    #[test]
2538    fn coloru_display_round_trips_through_the_parser() {
2539        // Display emits `rgba(r, g, b, a/255)`, which parse_css_color accepts.
2540        for a in 0..=255u16 {
2541            #[allow(clippy::cast_possible_truncation)]
2542            let c = ColorU::rgba(13, 110, 253, a as u8);
2543            let encoded = format!("{c}");
2544            let decoded = parse_css_color(&encoded)
2545                .unwrap_or_else(|e| panic!("{encoded} failed to parse: {e}"));
2546            assert_eq!(decoded, c, "{encoded} decoded to the wrong color");
2547        }
2548        for c in SAMPLES {
2549            assert_eq!(parse_css_color(&format!("{c}")).unwrap(), c);
2550        }
2551    }
2552
2553    #[cfg(feature = "parser")]
2554    #[test]
2555    fn system_color_ref_css_str_round_trips_for_every_variant() {
2556        let all = [
2557            SystemColorRef::Text,
2558            SystemColorRef::Background,
2559            SystemColorRef::Accent,
2560            SystemColorRef::AccentText,
2561            SystemColorRef::ButtonFace,
2562            SystemColorRef::ButtonText,
2563            SystemColorRef::WindowBackground,
2564            SystemColorRef::SelectionBackground,
2565            SystemColorRef::SelectionText,
2566        ];
2567        for variant in all {
2568            let encoded = variant.as_css_str();
2569            assert!(encoded.starts_with("system:"), "{encoded}");
2570            assert_eq!(
2571                parse_color_or_system(encoded).unwrap(),
2572                ColorOrSystem::System(variant),
2573                "{encoded} did not round-trip"
2574            );
2575        }
2576    }
2577
2578    // =====================================================================
2579    // ColorOrSystem / SystemColorRef
2580    // =====================================================================
2581
2582    #[test]
2583    fn color_or_system_constructors_and_fallbacks() {
2584        let c = ColorOrSystem::color(ColorU::RED);
2585        assert_eq!(c, ColorOrSystem::Color(ColorU::RED));
2586        assert_eq!(c.to_color_u_with_fallback(ColorU::BLUE), ColorU::RED);
2587        assert_eq!(c.to_color_u_default(), ColorU::RED);
2588
2589        let s = ColorOrSystem::system(SystemColorRef::Accent);
2590        assert_eq!(s, ColorOrSystem::System(SystemColorRef::Accent));
2591        // A system ref has no concrete value, so the fallback wins.
2592        assert_eq!(s.to_color_u_with_fallback(ColorU::BLUE), ColorU::BLUE);
2593        assert_eq!(s.to_color_u_default(), ColorU::rgba(128, 128, 128, 255));
2594
2595        // Default is opaque black, and From<ColorU> agrees with ::color().
2596        assert_eq!(ColorOrSystem::default(), ColorOrSystem::Color(ColorU::BLACK));
2597        assert_eq!(ColorOrSystem::from(ColorU::RED), ColorOrSystem::color(ColorU::RED));
2598    }
2599
2600    #[test]
2601    fn system_color_ref_resolve_falls_back_when_unset() {
2602        use crate::system::SystemColors;
2603
2604        let empty = SystemColors::default();
2605        let all = [
2606            SystemColorRef::Text,
2607            SystemColorRef::Background,
2608            SystemColorRef::Accent,
2609            SystemColorRef::AccentText,
2610            SystemColorRef::ButtonFace,
2611            SystemColorRef::ButtonText,
2612            SystemColorRef::WindowBackground,
2613            SystemColorRef::SelectionBackground,
2614            SystemColorRef::SelectionText,
2615        ];
2616        for variant in all {
2617            assert_eq!(variant.resolve(&empty, ColorU::RED), ColorU::RED, "{variant:?}");
2618            assert_eq!(
2619                ColorOrSystem::System(variant).resolve(&empty, ColorU::RED),
2620                ColorU::RED
2621            );
2622        }
2623        // A concrete color ignores both the SystemColors and the fallback.
2624        assert_eq!(
2625            ColorOrSystem::Color(ColorU::BLUE).resolve(&empty, ColorU::RED),
2626            ColorU::BLUE
2627        );
2628    }
2629
2630    // =====================================================================
2631    // palettes: shade is a `usize`, so every value must land somewhere
2632    // =====================================================================
2633
2634    #[test]
2635    fn palette_shades_are_total_over_usize_and_always_opaque() {
2636        type Palette = fn(usize) -> ColorU;
2637        const PALETTES: [Palette; 12] = [
2638            ColorU::strawberry,
2639            ColorU::palette_orange,
2640            ColorU::banana,
2641            ColorU::palette_lime,
2642            ColorU::mint,
2643            ColorU::blueberry,
2644            ColorU::grape,
2645            ColorU::bubblegum,
2646            ColorU::cocoa,
2647            ColorU::palette_silver,
2648            ColorU::slate,
2649            ColorU::dark,
2650        ];
2651        for p in PALETTES {
2652            for shade in [0, 1, 100, 200, 201, 300, 400, 401, 500, 600, 601, 700, 800, 801, 900, 1000, usize::MAX] {
2653                assert_eq!(p(shade).a, 255, "shade {shade} was not opaque");
2654            }
2655            // Every out-of-band shade collapses into the 900 bucket.
2656            assert_eq!(p(usize::MAX), p(900));
2657            assert_eq!(p(801), p(900));
2658            // The documented buckets are distinct at their boundaries.
2659            assert_eq!(p(0), p(200));
2660            assert_ne!(p(200), p(201));
2661            assert_ne!(p(400), p(401));
2662            assert_ne!(p(600), p(601));
2663            assert_ne!(p(800), p(801));
2664        }
2665    }
2666
2667    #[test]
2668    fn palette_known_values() {
2669        assert_eq!(ColorU::strawberry(100), ColorU::rgb(0xff, 0x8c, 0x82));
2670        assert_eq!(ColorU::strawberry(900), ColorU::rgb(0x7a, 0x00, 0x00));
2671        assert_eq!(ColorU::dark(900), ColorU::BLACK);
2672        assert_eq!(ColorU::dark(usize::MAX), ColorU::BLACK);
2673    }
2674
2675    // =====================================================================
2676    // named / themed constructors: every one must be a valid opaque color
2677    // =====================================================================
2678
2679    #[test]
2680    fn named_constructors_match_their_constants() {
2681        assert_eq!(ColorU::red(), ColorU::RED);
2682        assert_eq!(ColorU::green(), ColorU::GREEN);
2683        assert_eq!(ColorU::blue(), ColorU::BLUE);
2684        assert_eq!(ColorU::white(), ColorU::WHITE);
2685        assert_eq!(ColorU::black(), ColorU::BLACK);
2686        assert_eq!(ColorU::transparent(), ColorU::TRANSPARENT);
2687        assert_eq!(ColorU::yellow(), ColorU::YELLOW);
2688        assert_eq!(ColorU::cyan(), ColorU::CYAN);
2689        assert_eq!(ColorU::magenta(), ColorU::MAGENTA);
2690        assert_eq!(ColorU::orange(), ColorU::ORANGE);
2691        assert_eq!(ColorU::pink(), ColorU::PINK);
2692        assert_eq!(ColorU::purple(), ColorU::PURPLE);
2693        assert_eq!(ColorU::brown(), ColorU::BROWN);
2694        assert_eq!(ColorU::gray(), ColorU::GRAY);
2695        assert_eq!(ColorU::light_gray(), ColorU::LIGHT_GRAY);
2696        assert_eq!(ColorU::dark_gray(), ColorU::DARK_GRAY);
2697        assert_eq!(ColorU::navy(), ColorU::NAVY);
2698        assert_eq!(ColorU::teal(), ColorU::TEAL);
2699        assert_eq!(ColorU::olive(), ColorU::OLIVE);
2700        assert_eq!(ColorU::maroon(), ColorU::MAROON);
2701        assert_eq!(ColorU::lime(), ColorU::LIME);
2702        assert_eq!(ColorU::aqua(), ColorU::AQUA);
2703        assert_eq!(ColorU::silver(), ColorU::SILVER);
2704        assert_eq!(ColorU::fuchsia(), ColorU::FUCHSIA);
2705        assert_eq!(ColorU::indigo(), ColorU::INDIGO);
2706        assert_eq!(ColorU::gold(), ColorU::GOLD);
2707        assert_eq!(ColorU::coral(), ColorU::CORAL);
2708        assert_eq!(ColorU::salmon(), ColorU::SALMON);
2709        assert_eq!(ColorU::turquoise(), ColorU::TURQUOISE);
2710        assert_eq!(ColorU::violet(), ColorU::VIOLET);
2711        assert_eq!(ColorU::crimson(), ColorU::CRIMSON);
2712        assert_eq!(ColorU::chocolate(), ColorU::CHOCOLATE);
2713        assert_eq!(ColorU::sky_blue(), ColorU::SKY_BLUE);
2714        assert_eq!(ColorU::forest_green(), ColorU::FOREST_GREEN);
2715        assert_eq!(ColorU::sea_green(), ColorU::SEA_GREEN);
2716        assert_eq!(ColorU::slate_gray(), ColorU::SLATE_GRAY);
2717        assert_eq!(ColorU::midnight_blue(), ColorU::MIDNIGHT_BLUE);
2718        assert_eq!(ColorU::dark_red(), ColorU::DARK_RED);
2719        assert_eq!(ColorU::dark_green(), ColorU::DARK_GREEN);
2720        assert_eq!(ColorU::dark_blue(), ColorU::DARK_BLUE);
2721        assert_eq!(ColorU::light_blue(), ColorU::LIGHT_BLUE);
2722        assert_eq!(ColorU::light_green(), ColorU::LIGHT_GREEN);
2723        assert_eq!(ColorU::light_yellow(), ColorU::LIGHT_YELLOW);
2724        assert_eq!(ColorU::light_pink(), ColorU::LIGHT_PINK);
2725    }
2726
2727    #[test]
2728    fn every_named_constructor_except_transparent_is_opaque() {
2729        type Ctor = fn() -> ColorU;
2730        const CTORS: [Ctor; 43] = [
2731            ColorU::red, ColorU::green, ColorU::blue, ColorU::white, ColorU::black,
2732            ColorU::yellow, ColorU::cyan, ColorU::magenta, ColorU::orange, ColorU::pink,
2733            ColorU::purple, ColorU::brown, ColorU::gray, ColorU::light_gray, ColorU::dark_gray,
2734            ColorU::navy, ColorU::teal, ColorU::olive, ColorU::maroon, ColorU::lime,
2735            ColorU::aqua, ColorU::silver, ColorU::fuchsia, ColorU::indigo, ColorU::gold,
2736            ColorU::coral, ColorU::salmon, ColorU::turquoise, ColorU::violet, ColorU::crimson,
2737            ColorU::chocolate, ColorU::sky_blue, ColorU::forest_green, ColorU::sea_green,
2738            ColorU::slate_gray, ColorU::midnight_blue, ColorU::dark_red, ColorU::dark_green,
2739            ColorU::dark_blue, ColorU::light_blue, ColorU::light_green, ColorU::light_yellow,
2740            ColorU::light_pink,
2741        ];
2742        for ctor in CTORS {
2743            let c = ctor();
2744            assert_eq!(c.a, ColorU::ALPHA_OPAQUE);
2745            assert!(!c.has_alpha());
2746        }
2747        // The one exception.
2748        assert_eq!(ColorU::transparent().a, ColorU::ALPHA_TRANSPARENT);
2749        assert!(ColorU::transparent().has_alpha());
2750    }
2751
2752    #[test]
2753    fn apple_and_bootstrap_palettes_are_opaque_and_distinct() {
2754        type Ctor = fn() -> ColorU;
2755        const APPLE: [Ctor; 26] = [
2756            ColorU::apple_red, ColorU::apple_red_dark,
2757            ColorU::apple_orange, ColorU::apple_orange_dark,
2758            ColorU::apple_yellow, ColorU::apple_yellow_dark,
2759            ColorU::apple_green, ColorU::apple_green_dark,
2760            ColorU::apple_mint, ColorU::apple_mint_dark,
2761            ColorU::apple_teal, ColorU::apple_teal_dark,
2762            ColorU::apple_cyan, ColorU::apple_cyan_dark,
2763            ColorU::apple_blue, ColorU::apple_blue_dark,
2764            ColorU::apple_indigo, ColorU::apple_indigo_dark,
2765            ColorU::apple_purple, ColorU::apple_purple_dark,
2766            ColorU::apple_pink, ColorU::apple_pink_dark,
2767            ColorU::apple_brown, ColorU::apple_brown_dark,
2768            ColorU::apple_gray, ColorU::apple_gray_dark,
2769        ];
2770        const BOOTSTRAP: [Ctor; 23] = [
2771            ColorU::bootstrap_primary, ColorU::bootstrap_primary_hover, ColorU::bootstrap_primary_active,
2772            ColorU::bootstrap_secondary, ColorU::bootstrap_secondary_hover, ColorU::bootstrap_secondary_active,
2773            ColorU::bootstrap_success, ColorU::bootstrap_success_hover, ColorU::bootstrap_success_active,
2774            ColorU::bootstrap_danger, ColorU::bootstrap_danger_hover, ColorU::bootstrap_danger_active,
2775            ColorU::bootstrap_warning, ColorU::bootstrap_warning_hover, ColorU::bootstrap_warning_active,
2776            ColorU::bootstrap_info, ColorU::bootstrap_info_hover, ColorU::bootstrap_info_active,
2777            ColorU::bootstrap_light, ColorU::bootstrap_light_hover, ColorU::bootstrap_light_active,
2778            ColorU::bootstrap_dark, ColorU::bootstrap_dark_hover,
2779        ];
2780        for ctor in APPLE.iter().chain(BOOTSTRAP.iter()) {
2781            assert_eq!(ctor().a, 255);
2782        }
2783        // Each light/dark pair must actually differ.
2784        for pair in APPLE.chunks_exact(2) {
2785            assert_ne!(pair[0](), pair[1](), "an apple light/dark pair is identical");
2786        }
2787        // bootstrap_link duplicates bootstrap_primary by design; check the hover shifts.
2788        assert_eq!(ColorU::bootstrap_link(), ColorU::bootstrap_primary());
2789        assert_ne!(ColorU::bootstrap_link_hover(), ColorU::bootstrap_link());
2790        assert_ne!(ColorU::bootstrap_dark_active(), ColorU::bootstrap_dark());
2791    }
2792
2793    // =====================================================================
2794    // parser: parse_css_color — malformed / huge / boundary / unicode
2795    // =====================================================================
2796
2797    #[cfg(feature = "parser")]
2798    #[test]
2799    fn parse_css_color_valid_minimal_positive_controls() {
2800        assert_eq!(parse_css_color("red").unwrap(), ColorU::RED);
2801        assert_eq!(parse_css_color("#f00").unwrap(), ColorU::RED);
2802        assert_eq!(parse_css_color("#ff0000").unwrap(), ColorU::RED);
2803        assert_eq!(parse_css_color("rgb(255,0,0)").unwrap(), ColorU::RED);
2804        assert_eq!(parse_css_color("hsl(0,100%,50%)").unwrap(), ColorU::RED);
2805    }
2806
2807    #[cfg(feature = "parser")]
2808    #[test]
2809    fn parse_css_color_empty_and_whitespace_only_are_errors() {
2810        assert!(parse_css_color("").is_err());
2811        assert!(parse_css_color("   ").is_err());
2812        assert!(parse_css_color("\t\n\r ").is_err());
2813        assert!(parse_css_color("#").is_err());
2814        assert_eq!(parse_css_color(""), Err(CssColorParseError::EmptyInput));
2815        assert_eq!(parse_css_color("  \t "), Err(CssColorParseError::EmptyInput));
2816    }
2817
2818    #[cfg(feature = "parser")]
2819    #[test]
2820    fn parse_css_color_garbage_is_rejected_without_panicking() {
2821        for garbage in [
2822            "!@#$%^&*()", "\0\0\0", "rgb", "rgb(", "rgb)", ")(", "()", "#-1", "#+1",
2823            "notacolor", "0", "-0", "1e10", "NaN", "inf", "-inf", ";", ",,,", "\\",
2824            "rgb(,,)", "hsl(,,)", "rgba(,,,)", "#\u{0}\u{0}\u{0}",
2825        ] {
2826            assert!(
2827                parse_css_color(garbage).is_err(),
2828                "{garbage:?} was unexpectedly accepted"
2829            );
2830        }
2831    }
2832
2833    #[cfg(feature = "parser")]
2834    #[test]
2835    fn parse_css_color_extremely_long_input_does_not_hang_or_panic() {
2836        // Hex path: rejected on the length check alone.
2837        let long_hex = format!("#{}", "f".repeat(1_000_000));
2838        assert!(parse_css_color(&long_hex).is_err());
2839        // Named-color path: lowercases 100k bytes, then fails the match.
2840        let long_name = "a".repeat(100_000);
2841        assert!(parse_css_color(&long_name).is_err());
2842        // Function path with a huge component list.
2843        let long_rgb = format!("rgb({})", "1,".repeat(50_000));
2844        assert!(parse_css_color(&long_rgb).is_err());
2845    }
2846
2847    #[cfg(feature = "parser")]
2848    #[test]
2849    fn parse_css_color_deeply_nested_input_does_not_stack_overflow() {
2850        // The parser is iterative, not recursive — these must simply be errors.
2851        let nested_parens = "(".repeat(10_000);
2852        assert!(parse_css_color(&nested_parens).is_err());
2853        let unclosed = "rgb(".repeat(10_000);
2854        assert!(parse_css_color(&unclosed).is_err());
2855        let balanced = format!("{}{}", "rgb(".repeat(5_000), ")".repeat(5_000));
2856        assert!(parse_css_color(&balanced).is_err());
2857    }
2858
2859    #[cfg(feature = "parser")]
2860    #[test]
2861    fn parse_css_color_unicode_input_does_not_panic() {
2862        // The 3/4-byte hex branches read raw bytes, so a multi-byte char must
2863        // fail cleanly rather than slice through a char boundary.
2864        for input in [
2865            "\u{1F600}",        // emoji
2866            "#\u{1F600}",       // 4 bytes after '#' -> hits the len==4 branch
2867            "#\u{e9}1",         // 3 bytes after '#' -> hits the len==3 branch
2868            "#\u{e9}\u{e9}\u{e9}", // 6 bytes -> hits the from_str_radix branch
2869            "r\u{e9}d",
2870            "\u{0301}\u{0301}",  // bare combining marks
2871            "\u{4e2d}\u{6587}",  // CJK
2872            "rgb(\u{1F600},0,0)",
2873            "rgba(0,0,0,\u{1F600})",
2874            "hsl(\u{1F600},100%,50%)",
2875        ] {
2876            assert!(
2877                parse_css_color(input).is_err(),
2878                "{input:?} was unexpectedly accepted"
2879            );
2880        }
2881    }
2882
2883    #[cfg(feature = "parser")]
2884    #[test]
2885    fn parse_css_color_boundary_numbers() {
2886        // rgb components are u8: 0 and 255 in, 256 and -1 out.
2887        assert_eq!(parse_css_color("rgb(0,0,0)").unwrap(), ColorU::BLACK);
2888        assert_eq!(parse_css_color("rgb(255,255,255)").unwrap(), ColorU::WHITE);
2889        assert!(parse_css_color("rgb(256,0,0)").is_err());
2890        assert!(parse_css_color("rgb(-1,0,0)").is_err());
2891        assert!(parse_css_color("rgb(9223372036854775807,0,0)").is_err());
2892        assert!(parse_css_color("rgb(340282350000000000000000000000000000000,0,0)").is_err());
2893        // Alpha is a float clamped to 0.0..=1.0 (inclusive), out-of-range rejected.
2894        assert_eq!(parse_css_color("rgba(0,0,0,0)").unwrap().a, 0);
2895        assert_eq!(parse_css_color("rgba(0,0,0,1)").unwrap().a, 255);
2896        assert_eq!(parse_css_color("rgba(0,0,0,1.0)").unwrap().a, 255);
2897        assert_eq!(parse_css_color("rgba(0,0,0,-0)").unwrap().a, 0);
2898        assert!(parse_css_color("rgba(0,0,0,1.0001)").is_err());
2899        assert!(parse_css_color("rgba(0,0,0,-0.0001)").is_err());
2900        assert!(parse_css_color("rgba(0,0,0,2)").is_err());
2901        // NaN / inf are valid f32 literals to FromStr, but must fail the range check.
2902        assert!(parse_css_color("rgba(0,0,0,NaN)").is_err());
2903        assert!(parse_css_color("rgba(0,0,0,nan)").is_err());
2904        assert!(parse_css_color("rgba(0,0,0,inf)").is_err());
2905        assert!(parse_css_color("rgba(0,0,0,-inf)").is_err());
2906        assert!(parse_css_color("rgba(0,0,0,infinity)").is_err());
2907        // Subnormals round down to a fully transparent alpha rather than panicking.
2908        assert_eq!(parse_css_color("rgba(0,0,0,1e-45)").unwrap().a, 0);
2909    }
2910
2911    #[cfg(feature = "parser")]
2912    #[test]
2913    fn parse_css_color_alpha_rounds_to_nearest() {
2914        // `(a * 255.0).round()` — half rounds away from zero.
2915        assert_eq!(parse_css_color("rgba(0,0,0,0.5)").unwrap().a, 128);
2916        assert_eq!(parse_css_color("rgba(0,0,0,0.0)").unwrap().a, 0);
2917        assert_eq!(parse_css_color("rgba(0,0,0,0.999)").unwrap().a, 255);
2918    }
2919
2920    #[cfg(feature = "parser")]
2921    #[test]
2922    fn parse_css_color_arity_errors() {
2923        assert!(parse_css_color("rgb(255,0)").is_err());       // missing blue
2924        assert!(parse_css_color("rgb(255)").is_err());         // missing green
2925        assert!(parse_css_color("rgb()").is_err());            // missing everything
2926        assert!(parse_css_color("rgb(0,0,0,0)").is_err());     // extra arg to rgb()
2927        assert!(parse_css_color("rgba(0,0,0)").is_err());      // missing alpha
2928        assert!(parse_css_color("rgba(0,0,0,1,1)").is_err());  // extra arg to rgba()
2929        assert!(parse_css_color("hsl(0,100%)").is_err());      // missing lightness
2930        assert!(parse_css_color("hsla(0,100%,50%)").is_err()); // missing alpha
2931        // This implementation requires commas; space-separated CSS4 syntax is not supported.
2932        assert!(parse_css_color("rgb(255 0 0)").is_err());
2933    }
2934
2935    #[cfg(feature = "parser")]
2936    #[test]
2937    fn parse_css_color_leading_and_trailing_whitespace_is_trimmed() {
2938        assert_eq!(parse_css_color("  red  ").unwrap(), ColorU::RED);
2939        assert_eq!(parse_css_color("\t#f00\n").unwrap(), ColorU::RED);
2940        assert_eq!(parse_css_color("  rgb( 255 , 0 , 0 )  ").unwrap(), ColorU::RED);
2941        // Trailing junk after a bare keyword IS rejected.
2942        assert!(parse_css_color("red;garbage").is_err());
2943        assert!(parse_css_color("red red").is_err());
2944        assert!(parse_css_color("#f00;").is_err());
2945    }
2946
2947    #[cfg(feature = "parser")]
2948    #[test]
2949    fn parse_css_color_accepts_trailing_junk_after_a_function_call() {
2950        // KNOWN DEVIATION (pinned, not endorsed): parse_parentheses slices between
2951        // the FIRST '(' and the LAST ')', so anything after the closing paren is
2952        // silently dropped instead of being rejected as an error. See report.
2953        assert_eq!(parse_css_color("rgb(1,2,3)garbage").unwrap(), ColorU::rgb(1, 2, 3));
2954        assert_eq!(parse_css_color("rgb(1,2,3);").unwrap(), ColorU::rgb(1, 2, 3));
2955    }
2956
2957    #[cfg(feature = "parser")]
2958    #[test]
2959    fn parse_css_color_hex_is_case_insensitive_and_length_checked() {
2960        assert_eq!(parse_css_color("#ABCDEF").unwrap(), parse_css_color("#abcdef").unwrap());
2961        assert_eq!(parse_css_color("#FFF").unwrap(), ColorU::WHITE);
2962        // 3/4-digit shorthand expands by *17 (f -> 0xff).
2963        assert_eq!(parse_css_color("#f00f").unwrap(), ColorU::rgba(255, 0, 0, 255));
2964        assert_eq!(parse_css_color("#0008").unwrap(), ColorU::rgba(0, 0, 0, 136));
2965        // Only lengths 3, 4, 6 and 8 are legal.
2966        for bad_len in ["#", "#f", "#ff", "#fffff", "#fffffff", "#fffffffff"] {
2967            assert!(parse_css_color(bad_len).is_err(), "{bad_len} accepted");
2968        }
2969        // Non-hex digits.
2970        assert!(parse_css_color("#ggg").is_err());
2971        assert!(parse_css_color("#gggggg").is_err());
2972        assert!(parse_css_color("#-12345").is_err());
2973        assert!(parse_css_color("#+f0000").is_err());
2974        assert!(parse_css_color("#ff ff").is_err());
2975    }
2976
2977    #[cfg(feature = "parser")]
2978    #[test]
2979    fn parse_css_color_builtin_names_are_case_insensitive() {
2980        assert_eq!(parse_css_color("RED").unwrap(), ColorU::RED);
2981        assert_eq!(parse_css_color("ReD").unwrap(), ColorU::RED);
2982        assert_eq!(parse_css_color("TRANSPARENT").unwrap(), ColorU::TRANSPARENT);
2983        assert_eq!(parse_css_color("transparent").unwrap().a, 0);
2984        // Near-miss names are rejected, not fuzzy-matched.
2985        for near_miss in ["redd", "re", "r ed", "red1", "gray2", "greyish", "blackk"] {
2986            assert!(parse_css_color(near_miss).is_err(), "{near_miss} accepted");
2987        }
2988        // ...but surrounding whitespace really is just trimmed.
2989        assert_eq!(parse_css_color(" grey ").unwrap(), ColorU::GRAY);
2990    }
2991
2992    #[cfg(feature = "parser")]
2993    #[test]
2994    fn parse_css_color_hsl_boundaries_and_hue_wraparound() {
2995        assert_eq!(parse_css_color("hsl(0,100%,50%)").unwrap(), ColorU::RED);
2996        assert_eq!(parse_css_color("hsl(120,100%,50%)").unwrap(), ColorU::GREEN);
2997        assert_eq!(parse_css_color("hsl(240,100%,50%)").unwrap(), ColorU::BLUE);
2998        // A full extra turn lands back on red.
2999        assert_eq!(parse_css_color("hsl(720,100%,50%)").unwrap(), ColorU::RED);
3000        // Achromatic ends.
3001        assert_eq!(parse_css_color("hsl(0,0%,0%)").unwrap(), ColorU::BLACK);
3002        assert_eq!(parse_css_color("hsl(0,0%,100%)").unwrap(), ColorU::WHITE);
3003        // Huge but finite hues must not panic.
3004        for hue in ["1000000", "99999999", "-360"] {
3005            let s = format!("hsl({hue},100%,50%)");
3006            let _ = parse_css_color(&s).map(|c| assert_eq!(c.a, 255));
3007        }
3008    }
3009
3010    #[cfg(feature = "parser")]
3011    #[test]
3012    fn parse_css_color_unitless_hsl_components_are_scaled_wrong() {
3013        // KNOWN DEVIATION (pinned, not endorsed): `parse_percentage_value` turns a
3014        // unitless value into `value * 100` percent, and `percent_from_str` then
3015        // re-normalizes it, so a unitless component is a 0..1 FRACTION rather than
3016        // the CSS Color 4 "number of percent". `hsl(0 100 50)` — plain red in every
3017        // browser — comes out CYAN here. Pinned so a fix shows up as a diff.
3018        assert_eq!(parse_css_color("hsl(0,100,50)").unwrap(), ColorU::rgb(0, 255, 255));
3019        // The fraction spelling is what currently means "100% / 50%".
3020        assert_eq!(parse_css_color("hsl(0,1,0.5)").unwrap(), ColorU::RED);
3021        // The mixed spelling that the existing suite smoke-tests happens to land on
3022        // red by coincidence (the out-of-range saturation clips back into gamut).
3023        assert_eq!(parse_css_color("hsl(0,100,50%)").unwrap(), ColorU::RED);
3024    }
3025
3026    // =====================================================================
3027    // parser: private helpers
3028    // =====================================================================
3029
3030    #[cfg(feature = "parser")]
3031    #[test]
3032    fn parse_color_no_hash_only_accepts_3_4_6_and_8_bytes() {
3033        assert_eq!(parse_color_no_hash("fff").unwrap(), ColorU::WHITE);
3034        assert_eq!(parse_color_no_hash("000f").unwrap(), ColorU::BLACK);
3035        assert_eq!(parse_color_no_hash("ff0000").unwrap(), ColorU::RED);
3036        assert_eq!(parse_color_no_hash("ff000080").unwrap(), ColorU::rgba(255, 0, 0, 128));
3037        for bad in ["", "f", "ff", "fffff", "fffffff", "fffffffff", "   ", "zzz"] {
3038            assert!(parse_color_no_hash(bad).is_err(), "{bad:?} accepted");
3039        }
3040        // `input.len()` is a BYTE length, so a 3-byte multi-byte string reaches
3041        // the byte-reading branch and must error, not slice through a char.
3042        assert!(parse_color_no_hash("\u{e9}1").is_err());
3043        assert!(parse_color_no_hash("\u{1F600}").is_err());
3044    }
3045
3046    #[cfg(feature = "parser")]
3047    #[test]
3048    fn parse_color_rgb_alpha_flag_controls_arity() {
3049        assert_eq!(parse_color_rgb("1,2,3", false).unwrap(), ColorU::rgb(1, 2, 3));
3050        assert_eq!(parse_color_rgb("1,2,3,1", true).unwrap(), ColorU::rgba(1, 2, 3, 255));
3051        // parse_alpha=true but no alpha given.
3052        assert!(parse_color_rgb("1,2,3", true).is_err());
3053        // parse_alpha=false but an alpha given.
3054        assert!(parse_color_rgb("1,2,3,1", false).is_err());
3055        // Empty / whitespace components.
3056        assert!(parse_color_rgb("", false).is_err());
3057        assert!(parse_color_rgb("   ", false).is_err());
3058        assert!(parse_color_rgb(",,", false).is_err());
3059        assert!(parse_color_rgb("1,,3", false).is_err());
3060    }
3061
3062    #[cfg(feature = "parser")]
3063    #[test]
3064    fn parse_color_rgb_components_boundaries() {
3065        let mut ok = ["0", "128", "255"].into_iter();
3066        assert_eq!(
3067            parse_color_rgb_components(&mut ok).unwrap(),
3068            ColorU::rgb(0, 128, 255)
3069        );
3070        // An empty iterator is a missing-component error, not a panic.
3071        let mut empty = core::iter::empty::<&str>();
3072        assert!(parse_color_rgb_components(&mut empty).is_err());
3073        // Too few components.
3074        let mut short = ["1", "2"].into_iter();
3075        assert!(parse_color_rgb_components(&mut short).is_err());
3076        // Overflow / underflow / garbage.
3077        for bad in [
3078            ["256", "0", "0"],
3079            ["-1", "0", "0"],
3080            ["0", "0", "1e3"],
3081            ["0.5", "0", "0"],
3082            ["abc", "0", "0"],
3083            ["", "0", "0"],
3084            ["+0", "0", "999999999999999999999"],
3085        ] {
3086            let mut it = bad.into_iter();
3087            assert!(parse_color_rgb_components(&mut it).is_err(), "{bad:?} accepted");
3088        }
3089        // Extra components past the third are simply not consumed here.
3090        let mut extra = ["1", "2", "3", "4", "5"].into_iter();
3091        assert_eq!(
3092            parse_color_rgb_components(&mut extra).unwrap(),
3093            ColorU::rgb(1, 2, 3)
3094        );
3095        assert_eq!(extra.next(), Some("4"));
3096    }
3097
3098    #[cfg(feature = "parser")]
3099    #[test]
3100    fn parse_color_hsl_components_boundaries() {
3101        let mut red = ["0", "100%", "50%"].into_iter();
3102        assert_eq!(parse_color_hsl_components(&mut red).unwrap(), ColorU::RED);
3103        // KNOWN DEVIATION (pinned, not endorsed): a unitless component is read as
3104        // a 0.0..=1.0 FRACTION, not as a number of percent, so CSS Color 4's
3105        // `hsl(0 100 50)` saturation/lightness are scaled 100x too far. Only the
3106        // fraction spelling currently round-trips to red. See report.
3107        let mut fractions = ["0", "1", "0.5"].into_iter();
3108        assert_eq!(parse_color_hsl_components(&mut fractions).unwrap(), ColorU::RED);
3109        let mut unitless = ["0", "100", "50"].into_iter();
3110        assert_eq!(
3111            parse_color_hsl_components(&mut unitless).unwrap(),
3112            ColorU::rgb(0, 255, 255),
3113            "unitless hsl(0 100 50) should be red, not cyan"
3114        );
3115        // Missing components error rather than panic.
3116        let mut empty = core::iter::empty::<&str>();
3117        assert!(parse_color_hsl_components(&mut empty).is_err());
3118        let mut short = ["0", "100%"].into_iter();
3119        assert!(parse_color_hsl_components(&mut short).is_err());
3120        for bad in [
3121            ["", "100%", "50%"],
3122            ["notanangle", "100%", "50%"],
3123            ["to left", "100%", "50%"], // Direction::FromTo is unsupported for hue
3124            ["0", "", "50%"],
3125            ["0", "100%", ""],
3126        ] {
3127            let mut it = bad.into_iter();
3128            assert!(parse_color_hsl_components(&mut it).is_err(), "{bad:?} accepted");
3129        }
3130    }
3131
3132    #[cfg(feature = "parser")]
3133    #[test]
3134    fn parse_alpha_component_range_and_rounding() {
3135        let cases: [(&str, u8); 5] = [("0", 0), ("0.0", 0), ("0.5", 128), ("1", 255), ("1.0", 255)];
3136        for (input, expected) in cases {
3137            let mut it = [input].into_iter();
3138            assert_eq!(
3139                parse_alpha_component(&mut it).unwrap(),
3140                expected,
3141                "alpha {input}"
3142            );
3143        }
3144        // Out of range / unparseable / NaN / inf all produce Err, never a panic.
3145        for bad in ["", " ", "-0.0001", "1.0001", "2", "-1", "NaN", "inf", "-inf", "abc", "0,5", "50%"] {
3146            let mut it = [bad].into_iter();
3147            assert!(parse_alpha_component(&mut it).is_err(), "{bad:?} accepted");
3148        }
3149        // Missing entirely.
3150        let mut empty = core::iter::empty::<&str>();
3151        assert!(parse_alpha_component(&mut empty).is_err());
3152    }
3153
3154    #[cfg(feature = "parser")]
3155    #[test]
3156    fn parse_color_builtin_rejects_junk_without_panicking() {
3157        assert_eq!(parse_color_builtin("red").unwrap(), ColorU::RED);
3158        assert_eq!(parse_color_builtin("REBECCAPURPLE").unwrap(), ColorU::rgb(102, 51, 153));
3159        assert_eq!(parse_color_builtin("transparent").unwrap(), ColorU::TRANSPARENT);
3160        // Not trimmed at this level — the caller is responsible for that.
3161        assert!(parse_color_builtin(" red").is_err());
3162        assert!(parse_color_builtin("").is_err());
3163        // to_lowercase() on exotic input must not panic (dotted capital I expands).
3164        assert!(parse_color_builtin("\u{130}").is_err());
3165        assert!(parse_color_builtin("\u{1F600}").is_err());
3166        assert!(parse_color_builtin(&"z".repeat(100_000)).is_err());
3167    }
3168
3169    // =====================================================================
3170    // parser: parse_color_or_system
3171    // =====================================================================
3172
3173    #[cfg(feature = "parser")]
3174    #[test]
3175    fn parse_color_or_system_rejects_bad_system_names() {
3176        for bad in [
3177            "system:",
3178            "system:invalid",
3179            "system: ",
3180            "system::text",
3181            "system:text-",
3182            "system:TEXT",      // the variant table is case-SENSITIVE
3183            "SYSTEM:text",      // the prefix is case-SENSITIVE
3184            "system:text;junk",
3185            "system:\u{1F600}",
3186        ] {
3187            assert!(parse_color_or_system(bad).is_err(), "{bad:?} accepted");
3188        }
3189        // Empty / whitespace.
3190        assert!(parse_color_or_system("").is_err());
3191        assert!(parse_color_or_system("   ").is_err());
3192    }
3193
3194    #[cfg(feature = "parser")]
3195    #[test]
3196    fn parse_color_or_system_trims_and_falls_through_to_colors() {
3197        assert_eq!(
3198            parse_color_or_system("  system:accent  ").unwrap(),
3199            ColorOrSystem::System(SystemColorRef::Accent)
3200        );
3201        // The name after the prefix is trimmed too.
3202        assert_eq!(
3203            parse_color_or_system("system: accent ").unwrap(),
3204            ColorOrSystem::System(SystemColorRef::Accent)
3205        );
3206        // Non-system input is delegated to parse_css_color.
3207        assert_eq!(
3208            parse_color_or_system("  #f00 ").unwrap(),
3209            ColorOrSystem::Color(ColorU::RED)
3210        );
3211        assert_eq!(
3212            parse_color_or_system("rgba(0,0,0,0)").unwrap(),
3213            ColorOrSystem::Color(ColorU::TRANSPARENT)
3214        );
3215        assert!(parse_color_or_system("definitely-not-a-color").is_err());
3216    }
3217
3218    #[cfg(feature = "parser")]
3219    #[test]
3220    fn parse_color_or_system_long_and_nested_input_does_not_hang() {
3221        let long = format!("system:{}", "a".repeat(100_000));
3222        assert!(parse_color_or_system(&long).is_err());
3223        let nested = "rgb(".repeat(10_000);
3224        assert!(parse_color_or_system(&nested).is_err());
3225    }
3226
3227    // =====================================================================
3228    // error types: to_contained / to_shared round-trip
3229    // =====================================================================
3230
3231    #[cfg(feature = "parser")]
3232    #[test]
3233    fn css_color_parse_error_round_trips_through_owned() {
3234        // One representative input per reachable error variant.
3235        let errors = [
3236            parse_css_color("notacolor").unwrap_err(),        // InvalidColor
3237            parse_css_color("foo(1,2)").unwrap_err(),          // InvalidFunctionName
3238            parse_css_color("#zzz").unwrap_err(),              // InvalidColorComponent
3239            parse_css_color("rgb(300,0,0)").unwrap_err(),      // IntValueParseErr
3240            parse_css_color("rgba(0,0,0,x)").unwrap_err(),     // FloatValueParseErr
3241            parse_css_color("rgba(0,0,0,2)").unwrap_err(),     // FloatValueOutOfRange
3242            parse_css_color("rgb(1,2)").unwrap_err(),          // MissingColorComponent
3243            parse_css_color("rgb(1,2,3,4)").unwrap_err(),      // ExtraArguments
3244            parse_css_color("rgb(1,2,3").unwrap_err(),         // UnclosedColor
3245            parse_css_color("").unwrap_err(),                  // EmptyInput
3246            parse_css_color("hsl(x,1%,1%)").unwrap_err(),      // DirectionParseError
3247            parse_css_color("hsl(0,x%,1%)").unwrap_err(),      // InvalidPercentage
3248        ];
3249        for e in &errors {
3250            let owned = e.to_contained();
3251            let shared = owned.to_shared();
3252            // Borrowed -> owned -> borrowed -> owned must be a fixed point.
3253            assert_eq!(shared.to_contained(), owned, "error did not round-trip: {e}");
3254            // Debug/Display must both produce something non-empty.
3255            assert!(!format!("{e}").is_empty());
3256            assert!(!format!("{e:?}").is_empty());
3257            assert!(!format!("{owned:?}").is_empty());
3258        }
3259    }
3260
3261    #[cfg(feature = "parser")]
3262    #[test]
3263    fn css_color_parse_error_carries_the_offending_input() {
3264        assert_eq!(
3265            parse_css_color("notacolor"),
3266            Err(CssColorParseError::InvalidColor("notacolor"))
3267        );
3268        assert_eq!(
3269            parse_css_color("rgb(1,2,3,4)"),
3270            Err(CssColorParseError::ExtraArguments("4"))
3271        );
3272        assert_eq!(
3273            parse_css_color("rgb(1,2)"),
3274            Err(CssColorParseError::MissingColorComponent(
3275                CssColorComponent::Blue
3276            ))
3277        );
3278        assert_eq!(
3279            parse_css_color("rgba(1,2,3)"),
3280            Err(CssColorParseError::MissingColorComponent(
3281                CssColorComponent::Alpha
3282            ))
3283        );
3284        assert_eq!(
3285            parse_css_color("rgba(0,0,0,2)"),
3286            Err(CssColorParseError::FloatValueOutOfRange(2.0))
3287        );
3288        // The byte, not the char, is reported for a bad hex digit.
3289        assert_eq!(
3290            parse_css_color("#zzz"),
3291            Err(CssColorParseError::InvalidColorComponent(b'z'))
3292        );
3293    }
3294}