1use 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#[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#[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#[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 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 #[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 #[must_use]
520 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
521 Self { r, g, b, a }
522 }
523 #[must_use]
525 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
526 Self { r, g, b, a: 255 }
527 }
528 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
624 pub fn contrast_text(&self) -> Self {
625 self.best_contrast_text()
626 }
627
628 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 #[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 #[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 #[must_use]
673 pub fn meets_wcag_aa(&self, other: &Self) -> bool {
674 self.contrast_ratio(other) >= 4.5
675 }
676
677 #[must_use]
680 pub fn meets_wcag_aa_large(&self, other: &Self) -> bool {
681 self.contrast_ratio(other) >= 3.0
682 }
683
684 #[must_use]
686 pub fn meets_wcag_aaa(&self, other: &Self) -> bool {
687 self.contrast_ratio(other) >= 7.0
688 }
689
690 #[must_use]
692 pub fn meets_wcag_aaa_large(&self, other: &Self) -> bool {
693 self.contrast_ratio(other) >= 4.5
694 }
695
696 #[must_use]
699 pub fn is_light(&self) -> bool {
700 self.relative_luminance() > 0.5
701 }
702
703 #[must_use]
705 pub fn is_dark(&self) -> bool {
706 self.relative_luminance() <= 0.5
707 }
708
709 #[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 #[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 let bg_luminance = background.relative_luminance();
740 let should_lighten = bg_luminance < 0.5;
741
742 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 #[must_use]
777 pub fn apca_contrast(&self, background: &Self) -> f32 {
778 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 let text_y = self.relative_luminance();
789 let bg_y = background.relative_luminance();
790
791 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 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 if bg_clamp > txt_clamp {
809 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 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 #[must_use]
829 pub fn meets_apca_body(&self, background: &Self) -> bool {
830 libm::fabsf(self.apca_contrast(background)) >= 60.0
831 }
832
833 #[must_use]
835 pub fn meets_apca_large(&self, background: &Self) -> bool {
836 libm::fabsf(self.apca_contrast(background)) >= 45.0
837 }
838
839 #[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 #[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 #[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 #[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 #[must_use]
883 pub const fn has_alpha(&self) -> bool {
884 self.a != Self::ALPHA_OPAQUE
885 }
886
887 #[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 #[must_use]
899 pub const fn strawberry(shade: usize) -> Self {
900 match shade {
901 0..=200 => Self::rgb(0xff, 0x8c, 0x82), 201..=400 => Self::rgb(0xed, 0x53, 0x53), 401..=600 => Self::rgb(0xc6, 0x26, 0x2e), 601..=800 => Self::rgb(0xa1, 0x07, 0x05), _ => Self::rgb(0x7a, 0x00, 0x00), }
907 }
908
909 #[must_use]
911 pub const fn palette_orange(shade: usize) -> Self {
912 match shade {
913 0..=200 => Self::rgb(0xff, 0xc2, 0x7d), 201..=400 => Self::rgb(0xff, 0xa1, 0x54), 401..=600 => Self::rgb(0xf3, 0x73, 0x29), 601..=800 => Self::rgb(0xcc, 0x3b, 0x02), _ => Self::rgb(0xa6, 0x21, 0x00), }
919 }
920
921 #[must_use]
923 pub const fn banana(shade: usize) -> Self {
924 match shade {
925 0..=200 => Self::rgb(0xff, 0xf3, 0x94), 201..=400 => Self::rgb(0xff, 0xe1, 0x6b), 401..=600 => Self::rgb(0xf9, 0xc4, 0x40), 601..=800 => Self::rgb(0xd4, 0x8e, 0x15), _ => Self::rgb(0xad, 0x5f, 0x00), }
931 }
932
933 #[must_use]
935 pub const fn palette_lime(shade: usize) -> Self {
936 match shade {
937 0..=200 => Self::rgb(0xd1, 0xff, 0x82), 201..=400 => Self::rgb(0x9b, 0xdb, 0x4d), 401..=600 => Self::rgb(0x68, 0xb7, 0x23), 601..=800 => Self::rgb(0x3a, 0x91, 0x04), _ => Self::rgb(0x20, 0x6b, 0x00), }
943 }
944
945 #[must_use]
947 pub const fn mint(shade: usize) -> Self {
948 match shade {
949 0..=200 => Self::rgb(0x89, 0xff, 0xdd), 201..=400 => Self::rgb(0x43, 0xd6, 0xb5), 401..=600 => Self::rgb(0x28, 0xbc, 0xa3), 601..=800 => Self::rgb(0x0e, 0x9a, 0x83), _ => Self::rgb(0x00, 0x73, 0x67), }
955 }
956
957 #[must_use]
959 pub const fn blueberry(shade: usize) -> Self {
960 match shade {
961 0..=200 => Self::rgb(0x8c, 0xd5, 0xff), 201..=400 => Self::rgb(0x64, 0xba, 0xff), 401..=600 => Self::rgb(0x36, 0x89, 0xe6), 601..=800 => Self::rgb(0x0d, 0x52, 0xbf), _ => Self::rgb(0x00, 0x2e, 0x99), }
967 }
968
969 #[must_use]
971 pub const fn grape(shade: usize) -> Self {
972 match shade {
973 0..=200 => Self::rgb(0xe4, 0xc6, 0xfa), 201..=400 => Self::rgb(0xcd, 0x9e, 0xf7), 401..=600 => Self::rgb(0xa5, 0x6d, 0xe2), 601..=800 => Self::rgb(0x72, 0x39, 0xb3), _ => Self::rgb(0x45, 0x29, 0x81), }
979 }
980
981 #[must_use]
983 pub const fn bubblegum(shade: usize) -> Self {
984 match shade {
985 0..=200 => Self::rgb(0xfe, 0x9a, 0xb8), 201..=400 => Self::rgb(0xf4, 0x67, 0x9d), 401..=600 => Self::rgb(0xde, 0x3e, 0x80), 601..=800 => Self::rgb(0xbc, 0x24, 0x5d), _ => Self::rgb(0x91, 0x0e, 0x38), }
991 }
992
993 #[must_use]
995 pub const fn cocoa(shade: usize) -> Self {
996 match shade {
997 0..=200 => Self::rgb(0xa3, 0x90, 0x7c), 201..=400 => Self::rgb(0x8a, 0x71, 0x5e), 401..=600 => Self::rgb(0x71, 0x53, 0x44), 601..=800 => Self::rgb(0x57, 0x39, 0x2d), _ => Self::rgb(0x3d, 0x21, 0x1b), }
1003 }
1004
1005 #[must_use]
1007 pub const fn palette_silver(shade: usize) -> Self {
1008 match shade {
1009 0..=200 => Self::rgb(0xfa, 0xfa, 0xfa), 201..=400 => Self::rgb(0xd4, 0xd4, 0xd4), 401..=600 => Self::rgb(0xab, 0xac, 0xae), 601..=800 => Self::rgb(0x7e, 0x80, 0x87), _ => Self::rgb(0x55, 0x57, 0x61), }
1015 }
1016
1017 #[must_use]
1019 pub const fn slate(shade: usize) -> Self {
1020 match shade {
1021 0..=200 => Self::rgb(0x95, 0xa3, 0xab), 201..=400 => Self::rgb(0x66, 0x78, 0x85), 401..=600 => Self::rgb(0x48, 0x5a, 0x6c), 601..=800 => Self::rgb(0x27, 0x34, 0x45), _ => Self::rgb(0x0e, 0x14, 0x1f), }
1027 }
1028
1029 #[must_use]
1031 pub const fn dark(shade: usize) -> Self {
1032 match shade {
1033 0..=200 => Self::rgb(0x66, 0x66, 0x66), 201..=400 => Self::rgb(0x4d, 0x4d, 0x4d), 401..=600 => Self::rgb(0x33, 0x33, 0x33), 601..=800 => Self::rgb(0x1a, 0x1a, 0x1a), _ => Self::rgb(0x00, 0x00, 0x00), }
1039 }
1040
1041 #[must_use]
1047 pub const fn apple_red() -> Self {
1048 Self::rgb(255, 59, 48)
1049 }
1050 #[must_use]
1052 pub const fn apple_red_dark() -> Self {
1053 Self::rgb(255, 69, 58)
1054 }
1055 #[must_use]
1057 pub const fn apple_orange() -> Self {
1058 Self::rgb(255, 149, 0)
1059 }
1060 #[must_use]
1062 pub const fn apple_orange_dark() -> Self {
1063 Self::rgb(255, 159, 10)
1064 }
1065 #[must_use]
1067 pub const fn apple_yellow() -> Self {
1068 Self::rgb(255, 204, 0)
1069 }
1070 #[must_use]
1072 pub const fn apple_yellow_dark() -> Self {
1073 Self::rgb(255, 214, 10)
1074 }
1075 #[must_use]
1077 pub const fn apple_green() -> Self {
1078 Self::rgb(40, 205, 65)
1079 }
1080 #[must_use]
1082 pub const fn apple_green_dark() -> Self {
1083 Self::rgb(40, 215, 75)
1084 }
1085 #[must_use]
1087 pub const fn apple_mint() -> Self {
1088 Self::rgb(0, 199, 190)
1089 }
1090 #[must_use]
1092 pub const fn apple_mint_dark() -> Self {
1093 Self::rgb(102, 212, 207)
1094 }
1095 #[must_use]
1097 pub const fn apple_teal() -> Self {
1098 Self::rgb(89, 173, 196)
1099 }
1100 #[must_use]
1102 pub const fn apple_teal_dark() -> Self {
1103 Self::rgb(106, 196, 220)
1104 }
1105 #[must_use]
1107 pub const fn apple_cyan() -> Self {
1108 Self::rgb(85, 190, 240)
1109 }
1110 #[must_use]
1112 pub const fn apple_cyan_dark() -> Self {
1113 Self::rgb(90, 200, 245)
1114 }
1115 #[must_use]
1117 pub const fn apple_blue() -> Self {
1118 Self::rgb(0, 122, 255)
1119 }
1120 #[must_use]
1122 pub const fn apple_blue_dark() -> Self {
1123 Self::rgb(10, 132, 255)
1124 }
1125 #[must_use]
1127 pub const fn apple_indigo() -> Self {
1128 Self::rgb(88, 86, 214)
1129 }
1130 #[must_use]
1132 pub const fn apple_indigo_dark() -> Self {
1133 Self::rgb(94, 92, 230)
1134 }
1135 #[must_use]
1137 pub const fn apple_purple() -> Self {
1138 Self::rgb(175, 82, 222)
1139 }
1140 #[must_use]
1142 pub const fn apple_purple_dark() -> Self {
1143 Self::rgb(191, 90, 242)
1144 }
1145 #[must_use]
1147 pub const fn apple_pink() -> Self {
1148 Self::rgb(255, 45, 85)
1149 }
1150 #[must_use]
1152 pub const fn apple_pink_dark() -> Self {
1153 Self::rgb(255, 55, 95)
1154 }
1155 #[must_use]
1157 pub const fn apple_brown() -> Self {
1158 Self::rgb(162, 132, 94)
1159 }
1160 #[must_use]
1162 pub const fn apple_brown_dark() -> Self {
1163 Self::rgb(172, 142, 104)
1164 }
1165 #[must_use]
1167 pub const fn apple_gray() -> Self {
1168 Self::rgb(142, 142, 147)
1169 }
1170 #[must_use]
1172 pub const fn apple_gray_dark() -> Self {
1173 Self::rgb(152, 152, 157)
1174 }
1175
1176 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1382#[repr(C, u8)]
1383pub enum ColorOrSystem {
1384 Color(ColorU),
1386 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 #[must_use]
1405 pub const fn color(c: ColorU) -> Self {
1406 Self::Color(c)
1407 }
1408
1409 #[must_use]
1411 pub const fn system(s: SystemColorRef) -> Self {
1412 Self::System(s)
1413 }
1414
1415 #[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 #[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 #[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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1450#[repr(C)]
1451pub enum SystemColorRef {
1452 Text,
1454 Background,
1456 Accent,
1458 AccentText,
1460 ButtonFace,
1462 ButtonText,
1464 WindowBackground,
1466 SelectionBackground,
1468 SelectionText,
1470}
1471
1472impl SystemColorRef {
1473 #[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 #[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#[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")]
1676pub 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#[cfg(feature = "parser")]
1721pub fn parse_color_or_system(input: &str) -> Result<ColorOrSystem, CssColorParseError<'_>> {
1725 let input = input.trim();
1726
1727 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 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 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)] fn 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 Ok(parse_percentage_value(c)
1909 .map_err(CssColorParseError::InvalidPercentage)?
1910 .normalized()
1911 * 100.0)
1912 }
1913
1914 #[inline]
1915 #[allow(clippy::suboptimal_flops)] #[allow(clippy::many_single_char_names)] 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)] fn 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 assert_eq!(parse_css_color("#f00").unwrap(), ColorU::RED);
2143 assert_eq!(
2145 parse_css_color("#f008").unwrap(),
2146 ColorU::new(255, 0, 0, 136)
2147 );
2148 assert_eq!(parse_css_color("#00ff00").unwrap(), ColorU::GREEN);
2150 assert_eq!(
2152 parse_css_color("#0000ff80").unwrap(),
2153 ColorU::new(0, 0, 255, 128)
2154 );
2155 assert_eq!(
2157 parse_css_color("#FFC0CB").unwrap(),
2158 ColorU::new_rgb(255, 192, 203)
2159 ); }
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()); assert!(parse_css_color("#ggg").is_err()); assert!(parse_css_color("rgb(255, 0)").is_err()); assert!(parse_css_color("rgba(255, 0, 0, 2)").is_err()); assert!(parse_css_color("rgb(256, 0, 0)").is_err()); assert!(parse_css_color("hsl(0, 100, 50%)").is_ok()); assert!(parse_css_color("rgb(255 0 0)").is_err()); }
2203
2204 #[test]
2205 fn test_parse_system_colors() {
2206 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 assert!(parse_color_or_system("system:invalid").is_err());
2246
2247 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)), 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 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 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 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 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 #[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 #[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 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 assert_eq!(base.with_alpha_f32(f32::NAN).a, 0);
2489 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 assert_eq!(ColorU::rgb(0, 0, 0).with_alpha_f32(0.5).a, 127);
2502 }
2503
2504 #[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 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 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 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 let c = ColorU::BLACK.interpolate(&ColorU::WHITE, f32::INFINITY);
2561 assert_eq!(c, ColorU::rgba(255, 255, 255, 0));
2562 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 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 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 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 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 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 assert_eq!(
2653 ColorU::RED.mix(&ColorU::BLUE, f32::NAN),
2654 ColorU::rgba(0, 0, 0, 0)
2655 );
2656 }
2657
2658 #[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 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 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 assert!(ColorU::srgb_to_linear(-1.0) < 0.0);
2692 assert_eq!(ColorU::srgb_to_linear(-0.0), -0.0);
2693 }
2694
2695 #[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 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 assert!(ColorU::default().is_dark());
2774 assert!(ColorU::rgb(128, 128, 128).is_dark());
2776 }
2777
2778 #[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 assert!((a.contrast_ratio(&a) - 1.0).abs() < 1e-6);
2806 }
2807 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 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 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 #[test]
2875 fn ensure_contrast_returns_self_when_already_compliant() {
2876 assert_eq!(
2878 ColorU::BLACK.ensure_contrast(&ColorU::WHITE, 4.5),
2879 ColorU::BLACK
2880 );
2881 let gray = ColorU::rgb(128, 128, 128);
2882 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 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 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 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 assert_eq!(out.a, c.a);
2923 }
2924 }
2925 }
2926 }
2927
2928 #[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 #[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 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 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 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 #[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 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 #[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 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 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 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 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 #[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 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 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 assert_eq!(
3293 ColorOrSystem::Color(ColorU::BLUE).resolve(&empty, ColorU::RED),
3294 ColorU::BLUE
3295 );
3296 }
3297
3298 #[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 assert_eq!(p(usize::MAX), p(900));
3343 assert_eq!(p(801), p(900));
3344 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 #[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 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 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 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 #[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 let long_hex = format!("#{}", "f".repeat(1_000_000));
3612 assert!(parse_css_color(&long_hex).is_err());
3613 let long_name = "a".repeat(100_000);
3615 assert!(parse_css_color(&long_name).is_err());
3616 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 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 for input in [
3639 "\u{1F600}", "#\u{1F600}", "#\u{e9}1", "#\u{e9}\u{e9}\u{e9}", "r\u{e9}d",
3644 "\u{0301}\u{0301}", "\u{4e2d}\u{6587}", "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 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 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 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 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 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()); assert!(parse_css_color("rgb(255)").is_err()); assert!(parse_css_color("rgb()").is_err()); assert!(parse_css_color("rgb(0,0,0,0)").is_err()); assert!(parse_css_color("rgba(0,0,0)").is_err()); assert!(parse_css_color("rgba(0,0,0,1,1)").is_err()); assert!(parse_css_color("hsl(0,100%)").is_err()); assert!(parse_css_color("hsla(0,100%,50%)").is_err()); 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 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 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 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 for bad_len in ["#", "#f", "#ff", "#fffff", "#fffffff", "#fffffffff"] {
3759 assert!(parse_css_color(bad_len).is_err(), "{bad_len} accepted");
3760 }
3761 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 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 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 assert_eq!(parse_css_color("hsl(720,100%,50%)").unwrap(), ColorU::RED);
3792 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 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 assert_eq!(
3811 parse_css_color("hsl(0,100,50)").unwrap(),
3812 ColorU::rgb(0, 255, 255)
3813 );
3814 assert_eq!(parse_css_color("hsl(0,1,0.5)").unwrap(), ColorU::RED);
3816 assert_eq!(parse_css_color("hsl(0,100,50%)").unwrap(), ColorU::RED);
3819 }
3820
3821 #[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 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 assert!(parse_color_rgb("1,2,3", true).is_err());
3857 assert!(parse_color_rgb("1,2,3,1", false).is_err());
3859 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 let mut empty = core::iter::empty::<&str>();
3876 assert!(parse_color_rgb_components(&mut empty).is_err());
3877 let mut short = ["1", "2"].into_iter();
3879 assert!(parse_color_rgb_components(&mut short).is_err());
3880 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 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 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 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%"], ["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 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 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 assert!(parse_color_builtin(" red").is_err());
3983 assert!(parse_color_builtin("").is_err());
3984 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 #[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", "SYSTEM:text", "system:text;junk",
4006 "system:\u{1F600}",
4007 ] {
4008 assert!(parse_color_or_system(bad).is_err(), "{bad:?} accepted");
4009 }
4010 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 assert_eq!(
4024 parse_color_or_system("system: accent ").unwrap(),
4025 ColorOrSystem::System(SystemColorRef::Accent)
4026 );
4027 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 #[cfg(feature = "parser")]
4053 #[test]
4054 fn css_color_parse_error_round_trips_through_owned() {
4055 let errors = [
4057 parse_css_color("notacolor").unwrap_err(), parse_css_color("foo(1,2)").unwrap_err(), parse_css_color("#zzz").unwrap_err(), parse_css_color("rgb(300,0,0)").unwrap_err(), parse_css_color("rgba(0,0,0,x)").unwrap_err(), parse_css_color("rgba(0,0,0,2)").unwrap_err(), parse_css_color("rgb(1,2)").unwrap_err(), parse_css_color("rgb(1,2,3,4)").unwrap_err(), parse_css_color("rgb(1,2,3").unwrap_err(), parse_css_color("").unwrap_err(), parse_css_color("hsl(x,1%,1%)").unwrap_err(), parse_css_color("hsl(0,x%,1%)").unwrap_err(), ];
4070 for e in &errors {
4071 let owned = e.to_contained();
4072 let shared = owned.to_shared();
4073 assert_eq!(
4075 shared.to_contained(),
4076 owned,
4077 "error did not round-trip: {e}"
4078 );
4079 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 assert_eq!(
4115 parse_css_color("#zzz"),
4116 Err(CssColorParseError::InvalidColorComponent(b'z'))
4117 );
4118 }
4119}