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