1use alloc::string::{String, ToString};
8use core::fmt;
9use crate::corety::AzString;
10use crate::props::basic::error::{ParseFloatError, ParseIntError};
11
12use crate::{
13 impl_option,
14 props::basic::{
15 direction::{
16 parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
17 },
18 length::{PercentageParseError, PercentageValue},
19 },
20};
21
22#[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 { r: 255, g: 255, b: 0, a: Self::ALPHA_OPAQUE };
112 pub const CYAN: Self = Self { r: 0, g: 255, b: 255, a: Self::ALPHA_OPAQUE };
113 pub const MAGENTA: Self = Self { r: 255, g: 0, b: 255, a: Self::ALPHA_OPAQUE };
114 pub const ORANGE: Self = Self { r: 255, g: 165, b: 0, a: Self::ALPHA_OPAQUE };
115 pub const PINK: Self = Self { r: 255, g: 192, b: 203, a: Self::ALPHA_OPAQUE };
116 pub const PURPLE: Self = Self { r: 128, g: 0, b: 128, a: Self::ALPHA_OPAQUE };
117 pub const BROWN: Self = Self { r: 139, g: 69, b: 19, a: Self::ALPHA_OPAQUE };
118 pub const GRAY: Self = Self { r: 128, g: 128, b: 128, a: Self::ALPHA_OPAQUE };
119 pub const LIGHT_GRAY: Self = Self { r: 211, g: 211, b: 211, a: Self::ALPHA_OPAQUE };
120 pub const DARK_GRAY: Self = Self { r: 64, g: 64, b: 64, a: Self::ALPHA_OPAQUE };
121 pub const NAVY: Self = Self { r: 0, g: 0, b: 128, a: Self::ALPHA_OPAQUE };
122 pub const TEAL: Self = Self { r: 0, g: 128, b: 128, a: Self::ALPHA_OPAQUE };
123 pub const OLIVE: Self = Self { r: 128, g: 128, b: 0, a: Self::ALPHA_OPAQUE };
124 pub const MAROON: Self = Self { r: 128, g: 0, b: 0, a: Self::ALPHA_OPAQUE };
125 pub const LIME: Self = Self { r: 0, g: 255, b: 0, a: Self::ALPHA_OPAQUE };
126 pub const AQUA: Self = Self { r: 0, g: 255, b: 255, a: Self::ALPHA_OPAQUE };
127 pub const SILVER: Self = Self { r: 192, g: 192, b: 192, a: Self::ALPHA_OPAQUE };
128 pub const FUCHSIA: Self = Self { r: 255, g: 0, b: 255, a: Self::ALPHA_OPAQUE };
129 pub const INDIGO: Self = Self { r: 75, g: 0, b: 130, a: Self::ALPHA_OPAQUE };
130 pub const GOLD: Self = Self { r: 255, g: 215, b: 0, a: Self::ALPHA_OPAQUE };
131 pub const CORAL: Self = Self { r: 255, g: 127, b: 80, a: Self::ALPHA_OPAQUE };
132 pub const SALMON: Self = Self { r: 250, g: 128, b: 114, a: Self::ALPHA_OPAQUE };
133 pub const TURQUOISE: Self = Self { r: 64, g: 224, b: 208, a: Self::ALPHA_OPAQUE };
134 pub const VIOLET: Self = Self { r: 238, g: 130, b: 238, a: Self::ALPHA_OPAQUE };
135 pub const CRIMSON: Self = Self { r: 220, g: 20, b: 60, a: Self::ALPHA_OPAQUE };
136 pub const CHOCOLATE: Self = Self { r: 210, g: 105, b: 30, a: Self::ALPHA_OPAQUE };
137 pub const SKY_BLUE: Self = Self { r: 135, g: 206, b: 235, a: Self::ALPHA_OPAQUE };
138 pub const FOREST_GREEN: Self = Self { r: 34, g: 139, b: 34, a: Self::ALPHA_OPAQUE };
139 pub const SEA_GREEN: Self = Self { r: 46, g: 139, b: 87, a: Self::ALPHA_OPAQUE };
140 pub const SLATE_GRAY: Self = Self { r: 112, g: 128, b: 144, a: Self::ALPHA_OPAQUE };
141 pub const MIDNIGHT_BLUE: Self = Self { r: 25, g: 25, b: 112, a: Self::ALPHA_OPAQUE };
142 pub const DARK_RED: Self = Self { r: 139, g: 0, b: 0, a: Self::ALPHA_OPAQUE };
143 pub const DARK_GREEN: Self = Self { r: 0, g: 100, b: 0, a: Self::ALPHA_OPAQUE };
144 pub const DARK_BLUE: Self = Self { r: 0, g: 0, b: 139, a: Self::ALPHA_OPAQUE };
145 pub const LIGHT_BLUE: Self = Self { r: 173, g: 216, b: 230, a: Self::ALPHA_OPAQUE };
146 pub const LIGHT_GREEN: Self = Self { r: 144, g: 238, b: 144, a: Self::ALPHA_OPAQUE };
147 pub const LIGHT_YELLOW: Self = Self { r: 255, g: 255, b: 224, a: Self::ALPHA_OPAQUE };
148 pub const LIGHT_PINK: Self = Self { r: 255, g: 182, b: 193, a: Self::ALPHA_OPAQUE };
149
150 #[must_use] pub const fn red() -> Self { Self::RED }
152 #[must_use] pub const fn green() -> Self { Self::GREEN }
153 #[must_use] pub const fn blue() -> Self { Self::BLUE }
154 #[must_use] pub const fn white() -> Self { Self::WHITE }
155 #[must_use] pub const fn black() -> Self { Self::BLACK }
156 #[must_use] pub const fn transparent() -> Self { Self::TRANSPARENT }
157 #[must_use] pub const fn yellow() -> Self { Self::YELLOW }
158 #[must_use] pub const fn cyan() -> Self { Self::CYAN }
159 #[must_use] pub const fn magenta() -> Self { Self::MAGENTA }
160 #[must_use] pub const fn orange() -> Self { Self::ORANGE }
161 #[must_use] pub const fn pink() -> Self { Self::PINK }
162 #[must_use] pub const fn purple() -> Self { Self::PURPLE }
163 #[must_use] pub const fn brown() -> Self { Self::BROWN }
164 #[must_use] pub const fn gray() -> Self { Self::GRAY }
165 #[must_use] pub const fn light_gray() -> Self { Self::LIGHT_GRAY }
166 #[must_use] pub const fn dark_gray() -> Self { Self::DARK_GRAY }
167 #[must_use] pub const fn navy() -> Self { Self::NAVY }
168 #[must_use] pub const fn teal() -> Self { Self::TEAL }
169 #[must_use] pub const fn olive() -> Self { Self::OLIVE }
170 #[must_use] pub const fn maroon() -> Self { Self::MAROON }
171 #[must_use] pub const fn lime() -> Self { Self::LIME }
172 #[must_use] pub const fn aqua() -> Self { Self::AQUA }
173 #[must_use] pub const fn silver() -> Self { Self::SILVER }
174 #[must_use] pub const fn fuchsia() -> Self { Self::FUCHSIA }
175 #[must_use] pub const fn indigo() -> Self { Self::INDIGO }
176 #[must_use] pub const fn gold() -> Self { Self::GOLD }
177 #[must_use] pub const fn coral() -> Self { Self::CORAL }
178 #[must_use] pub const fn salmon() -> Self { Self::SALMON }
179 #[must_use] pub const fn turquoise() -> Self { Self::TURQUOISE }
180 #[must_use] pub const fn violet() -> Self { Self::VIOLET }
181 #[must_use] pub const fn crimson() -> Self { Self::CRIMSON }
182 #[must_use] pub const fn chocolate() -> Self { Self::CHOCOLATE }
183 #[must_use] pub const fn sky_blue() -> Self { Self::SKY_BLUE }
184 #[must_use] pub const fn forest_green() -> Self { Self::FOREST_GREEN }
185 #[must_use] pub const fn sea_green() -> Self { Self::SEA_GREEN }
186 #[must_use] pub const fn slate_gray() -> Self { Self::SLATE_GRAY }
187 #[must_use] pub const fn midnight_blue() -> Self { Self::MIDNIGHT_BLUE }
188 #[must_use] pub const fn dark_red() -> Self { Self::DARK_RED }
189 #[must_use] pub const fn dark_green() -> Self { Self::DARK_GREEN }
190 #[must_use] pub const fn dark_blue() -> Self { Self::DARK_BLUE }
191 #[must_use] pub const fn light_blue() -> Self { Self::LIGHT_BLUE }
192 #[must_use] pub const fn light_green() -> Self { Self::LIGHT_GREEN }
193 #[must_use] pub const fn light_yellow() -> Self { Self::LIGHT_YELLOW }
194 #[must_use] pub const fn light_pink() -> Self { Self::LIGHT_PINK }
195
196 #[must_use] pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
198 Self { r, g, b, a }
199 }
200 #[must_use] pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
202 Self { r, g, b, a: 255 }
203 }
204 #[inline]
206 #[must_use] pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
207 Self::rgba(r, g, b, a)
208 }
209 #[inline]
211 #[must_use] pub const fn new_rgb(r: u8, g: u8, b: u8) -> Self {
212 Self::rgb(r, g, b)
213 }
214
215 #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
218 Self {
219 r: channel_to_u8(libm::roundf(f32::from(self.r) + (f32::from(other.r) - f32::from(self.r)) * t)),
220 g: channel_to_u8(libm::roundf(f32::from(self.g) + (f32::from(other.g) - f32::from(self.g)) * t)),
221 b: channel_to_u8(libm::roundf(f32::from(self.b) + (f32::from(other.b) - f32::from(self.b)) * t)),
222 a: channel_to_u8(libm::roundf(f32::from(self.a) + (f32::from(other.a) - f32::from(self.a)) * t)),
223 }
224 }
225
226 #[must_use] pub fn lighten(&self, amount: f32) -> Self {
229 let mut c = self.interpolate(&Self::WHITE, amount.clamp(0.0, 1.0));
230 c.a = self.a;
231 c
232 }
233
234 #[must_use] pub fn darken(&self, amount: f32) -> Self {
237 let mut c = self.interpolate(&Self::BLACK, amount.clamp(0.0, 1.0));
238 c.a = self.a;
239 c
240 }
241
242 #[must_use] pub fn mix(&self, other: &Self, ratio: f32) -> Self {
244 self.interpolate(other, ratio.clamp(0.0, 1.0))
245 }
246
247 #[must_use] pub fn hover_variant(&self) -> Self {
250 let luminance = self.relative_luminance();
251 if luminance > 0.5 {
252 self.darken(0.08)
253 } else {
254 self.lighten(0.12)
255 }
256 }
257
258 #[must_use] pub fn active_variant(&self) -> Self {
261 let luminance = self.relative_luminance();
262 if luminance > 0.5 {
263 self.darken(0.15)
264 } else {
265 self.lighten(0.05)
266 }
267 }
268
269 #[must_use] pub fn luminance(&self) -> f32 {
275 let r = f32::from(self.r) / 255.0;
276 let g = f32::from(self.g) / 255.0;
277 let b = f32::from(self.b) / 255.0;
278 0.2126 * r + 0.7152 * g + 0.0722 * b
279 }
280
281 #[must_use] pub fn contrast_text(&self) -> Self {
283 self.best_contrast_text()
284 }
285
286 fn srgb_to_linear(c: f32) -> f32 {
294 if c <= 0.03928 {
295 c / 12.92
296 } else {
297 libm::powf((c + 0.055) / 1.055, 2.4)
298 }
299 }
300
301 #[must_use] pub fn relative_luminance(&self) -> f32 {
305 let r = Self::srgb_to_linear(f32::from(self.r) / 255.0);
306 let g = Self::srgb_to_linear(f32::from(self.g) / 255.0);
307 let b = Self::srgb_to_linear(f32::from(self.b) / 255.0);
308 0.2126 * r + 0.7152 * g + 0.0722 * b
309 }
310
311 #[must_use] pub fn contrast_ratio(&self, other: &Self) -> f32 {
320 let l1 = self.relative_luminance();
321 let l2 = other.relative_luminance();
322 let lighter = if l1 > l2 { l1 } else { l2 };
323 let darker = if l1 > l2 { l2 } else { l1 };
324 (lighter + 0.05) / (darker + 0.05)
325 }
326
327 #[must_use] pub fn meets_wcag_aa(&self, other: &Self) -> bool {
329 self.contrast_ratio(other) >= 4.5
330 }
331
332 #[must_use] pub fn meets_wcag_aa_large(&self, other: &Self) -> bool {
335 self.contrast_ratio(other) >= 3.0
336 }
337
338 #[must_use] pub fn meets_wcag_aaa(&self, other: &Self) -> bool {
340 self.contrast_ratio(other) >= 7.0
341 }
342
343 #[must_use] pub fn meets_wcag_aaa_large(&self, other: &Self) -> bool {
345 self.contrast_ratio(other) >= 4.5
346 }
347
348 #[must_use] pub fn is_light(&self) -> bool {
351 self.relative_luminance() > 0.5
352 }
353
354 #[must_use] pub fn is_dark(&self) -> bool {
356 self.relative_luminance() <= 0.5
357 }
358
359 #[must_use] pub fn best_contrast_text(&self) -> Self {
365 let white_contrast = self.contrast_ratio(&Self::WHITE);
366 let black_contrast = self.contrast_ratio(&Self::BLACK);
367
368 if white_contrast >= black_contrast {
369 Self::WHITE
370 } else {
371 Self::BLACK
372 }
373 }
374
375 #[must_use] pub fn ensure_contrast(&self, background: &Self, min_ratio: f32) -> Self {
381 let current_ratio = self.contrast_ratio(background);
382 if current_ratio >= min_ratio {
383 return *self;
384 }
385
386 let bg_luminance = background.relative_luminance();
388 let should_lighten = bg_luminance < 0.5;
389
390 let mut low = 0.0f32;
392 let mut high = 1.0f32;
393 let mut result = *self;
394
395 for _ in 0..16 {
396 let mid = f32::midpoint(low, high);
397 let candidate = if should_lighten {
398 self.lighten(mid)
399 } else {
400 self.darken(mid)
401 };
402
403 if candidate.contrast_ratio(background) >= min_ratio {
404 result = candidate;
405 high = mid;
406 } else {
407 low = mid;
408 }
409 }
410
411 result
412 }
413
414 #[must_use] pub fn apca_contrast(&self, background: &Self) -> f32 {
425 const NORMBLKTXT: f32 = 0.56;
427 const NORMWHT: f32 = 0.57;
428 const REVTXT: f32 = 0.62;
429 const REVWHT: f32 = 0.65;
430 const BLKTHRS: f32 = 0.022;
431 const SCALEBLKT: f32 = 1.414;
432 const SCALEWHT: f32 = 1.14;
433
434 let text_y = self.relative_luminance();
436 let bg_y = background.relative_luminance();
437
438 let text_y = if text_y < 0.0 { 0.0 } else { text_y };
440 let bg_y = if bg_y < 0.0 { 0.0 } else { bg_y };
441
442
443 let txt_clamp = if text_y < BLKTHRS {
445 text_y + libm::powf(BLKTHRS - text_y, SCALEBLKT)
446 } else {
447 text_y
448 };
449 let bg_clamp = if bg_y < BLKTHRS {
450 bg_y + libm::powf(BLKTHRS - bg_y, SCALEBLKT)
451 } else {
452 bg_y
453 };
454
455 if bg_clamp > txt_clamp {
457 let s = (libm::powf(bg_clamp, NORMWHT) - libm::powf(txt_clamp, NORMBLKTXT)) * SCALEWHT;
459 if s < 0.1 { 0.0 } else { s * 100.0 }
460 } else {
461 let s = (libm::powf(bg_clamp, REVWHT) - libm::powf(txt_clamp, REVTXT)) * SCALEWHT;
463 if s > -0.1 { 0.0 } else { s * 100.0 }
464 }
465 }
466
467 #[must_use] pub fn meets_apca_body(&self, background: &Self) -> bool {
469 libm::fabsf(self.apca_contrast(background)) >= 60.0
470 }
471
472 #[must_use] pub fn meets_apca_large(&self, background: &Self) -> bool {
474 libm::fabsf(self.apca_contrast(background)) >= 45.0
475 }
476
477 #[must_use] pub const fn with_alpha(&self, a: u8) -> Self {
479 Self { r: self.r, g: self.g, b: self.b, a }
480 }
481
482 #[must_use] pub fn with_alpha_f32(&self, a: f32) -> Self {
484 self.with_alpha(channel_to_u8(a.clamp(0.0, 1.0) * 255.0))
485 }
486
487 #[must_use] pub const fn invert(&self) -> Self {
489 Self {
490 r: 255 - self.r,
491 g: 255 - self.g,
492 b: 255 - self.b,
493 a: self.a,
494 }
495 }
496
497 #[must_use] pub fn to_grayscale(&self) -> Self {
499 let gray = channel_to_u8(0.299 * f32::from(self.r) + 0.587 * f32::from(self.g) + 0.114 * f32::from(self.b));
500 Self { r: gray, g: gray, b: gray, a: self.a }
501 }
502
503 #[must_use] pub const fn has_alpha(&self) -> bool {
505 self.a != Self::ALPHA_OPAQUE
506 }
507
508 #[must_use] pub fn to_hash(&self) -> String {
510 format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
511 }
512
513 #[must_use] pub const fn strawberry(shade: usize) -> Self {
519 match shade {
520 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), }
526 }
527
528 #[must_use] pub const fn palette_orange(shade: usize) -> Self {
530 match shade {
531 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), }
537 }
538
539 #[must_use] pub const fn banana(shade: usize) -> Self {
541 match shade {
542 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), }
548 }
549
550 #[must_use] pub const fn palette_lime(shade: usize) -> Self {
552 match shade {
553 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), }
559 }
560
561 #[must_use] pub const fn mint(shade: usize) -> Self {
563 match shade {
564 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), }
570 }
571
572 #[must_use] pub const fn blueberry(shade: usize) -> Self {
574 match shade {
575 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), }
581 }
582
583 #[must_use] pub const fn grape(shade: usize) -> Self {
585 match shade {
586 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), }
592 }
593
594 #[must_use] pub const fn bubblegum(shade: usize) -> Self {
596 match shade {
597 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), }
603 }
604
605 #[must_use] pub const fn cocoa(shade: usize) -> Self {
607 match shade {
608 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), }
614 }
615
616 #[must_use] pub const fn palette_silver(shade: usize) -> Self {
618 match shade {
619 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), }
625 }
626
627 #[must_use] pub const fn slate(shade: usize) -> Self {
629 match shade {
630 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), }
636 }
637
638 #[must_use] pub const fn dark(shade: usize) -> Self {
640 match shade {
641 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), }
647 }
648
649 #[must_use] pub const fn apple_red() -> Self { Self::rgb(255, 59, 48) }
655 #[must_use] pub const fn apple_red_dark() -> Self { Self::rgb(255, 69, 58) }
657 #[must_use] pub const fn apple_orange() -> Self { Self::rgb(255, 149, 0) }
659 #[must_use] pub const fn apple_orange_dark() -> Self { Self::rgb(255, 159, 10) }
661 #[must_use] pub const fn apple_yellow() -> Self { Self::rgb(255, 204, 0) }
663 #[must_use] pub const fn apple_yellow_dark() -> Self { Self::rgb(255, 214, 10) }
665 #[must_use] pub const fn apple_green() -> Self { Self::rgb(40, 205, 65) }
667 #[must_use] pub const fn apple_green_dark() -> Self { Self::rgb(40, 215, 75) }
669 #[must_use] pub const fn apple_mint() -> Self { Self::rgb(0, 199, 190) }
671 #[must_use] pub const fn apple_mint_dark() -> Self { Self::rgb(102, 212, 207) }
673 #[must_use] pub const fn apple_teal() -> Self { Self::rgb(89, 173, 196) }
675 #[must_use] pub const fn apple_teal_dark() -> Self { Self::rgb(106, 196, 220) }
677 #[must_use] pub const fn apple_cyan() -> Self { Self::rgb(85, 190, 240) }
679 #[must_use] pub const fn apple_cyan_dark() -> Self { Self::rgb(90, 200, 245) }
681 #[must_use] pub const fn apple_blue() -> Self { Self::rgb(0, 122, 255) }
683 #[must_use] pub const fn apple_blue_dark() -> Self { Self::rgb(10, 132, 255) }
685 #[must_use] pub const fn apple_indigo() -> Self { Self::rgb(88, 86, 214) }
687 #[must_use] pub const fn apple_indigo_dark() -> Self { Self::rgb(94, 92, 230) }
689 #[must_use] pub const fn apple_purple() -> Self { Self::rgb(175, 82, 222) }
691 #[must_use] pub const fn apple_purple_dark() -> Self { Self::rgb(191, 90, 242) }
693 #[must_use] pub const fn apple_pink() -> Self { Self::rgb(255, 45, 85) }
695 #[must_use] pub const fn apple_pink_dark() -> Self { Self::rgb(255, 55, 95) }
697 #[must_use] pub const fn apple_brown() -> Self { Self::rgb(162, 132, 94) }
699 #[must_use] pub const fn apple_brown_dark() -> Self { Self::rgb(172, 142, 104) }
701 #[must_use] pub const fn apple_gray() -> Self { Self::rgb(142, 142, 147) }
703 #[must_use] pub const fn apple_gray_dark() -> Self { Self::rgb(152, 152, 157) }
705
706 #[must_use] pub const fn bootstrap_primary() -> Self { Self::rgb(13, 110, 253) }
713 #[must_use] pub const fn bootstrap_primary_hover() -> Self { Self::rgb(11, 94, 215) }
714 #[must_use] pub const fn bootstrap_primary_active() -> Self { Self::rgb(10, 88, 202) }
715
716 #[must_use] pub const fn bootstrap_secondary() -> Self { Self::rgb(108, 117, 125) }
718 #[must_use] pub const fn bootstrap_secondary_hover() -> Self { Self::rgb(92, 99, 106) }
719 #[must_use] pub const fn bootstrap_secondary_active() -> Self { Self::rgb(86, 94, 100) }
720
721 #[must_use] pub const fn bootstrap_success() -> Self { Self::rgb(25, 135, 84) }
723 #[must_use] pub const fn bootstrap_success_hover() -> Self { Self::rgb(21, 115, 71) }
724 #[must_use] pub const fn bootstrap_success_active() -> Self { Self::rgb(20, 108, 67) }
725
726 #[must_use] pub const fn bootstrap_danger() -> Self { Self::rgb(220, 53, 69) }
728 #[must_use] pub const fn bootstrap_danger_hover() -> Self { Self::rgb(187, 45, 59) }
729 #[must_use] pub const fn bootstrap_danger_active() -> Self { Self::rgb(176, 42, 55) }
730
731 #[must_use] pub const fn bootstrap_warning() -> Self { Self::rgb(255, 193, 7) }
733 #[must_use] pub const fn bootstrap_warning_hover() -> Self { Self::rgb(255, 202, 44) }
734 #[must_use] pub const fn bootstrap_warning_active() -> Self { Self::rgb(255, 205, 57) }
735
736 #[must_use] pub const fn bootstrap_info() -> Self { Self::rgb(13, 202, 240) }
738 #[must_use] pub const fn bootstrap_info_hover() -> Self { Self::rgb(49, 210, 242) }
739 #[must_use] pub const fn bootstrap_info_active() -> Self { Self::rgb(61, 213, 243) }
740
741 #[must_use] pub const fn bootstrap_light() -> Self { Self::rgb(248, 249, 250) }
743 #[must_use] pub const fn bootstrap_light_hover() -> Self { Self::rgb(233, 236, 239) }
744 #[must_use] pub const fn bootstrap_light_active() -> Self { Self::rgb(218, 222, 226) }
745
746 #[must_use] pub const fn bootstrap_dark() -> Self { Self::rgb(33, 37, 41) }
748 #[must_use] pub const fn bootstrap_dark_hover() -> Self { Self::rgb(66, 70, 73) }
749 #[must_use] pub const fn bootstrap_dark_active() -> Self { Self::rgb(78, 81, 84) }
750
751 #[must_use] pub const fn bootstrap_link() -> Self { Self::rgb(13, 110, 253) }
753 #[must_use] pub const fn bootstrap_link_hover() -> Self { Self::rgb(10, 88, 202) }
754}
755
756#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
758pub struct ColorF {
759 pub r: f32,
760 pub g: f32,
761 pub b: f32,
762 pub a: f32,
763}
764
765impl Default for ColorF {
766 fn default() -> Self {
767 Self::BLACK
768 }
769}
770
771impl fmt::Display for ColorF {
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 write!(
774 f,
775 "rgba({}, {}, {}, {})",
776 self.r * 255.0,
777 self.g * 255.0,
778 self.b * 255.0,
779 self.a
780 )
781 }
782}
783
784impl ColorF {
785 pub const ALPHA_TRANSPARENT: f32 = 0.0;
786 pub const ALPHA_OPAQUE: f32 = 1.0;
787 pub const WHITE: Self = Self {
788 r: 1.0,
789 g: 1.0,
790 b: 1.0,
791 a: Self::ALPHA_OPAQUE,
792 };
793 pub const BLACK: Self = Self {
794 r: 0.0,
795 g: 0.0,
796 b: 0.0,
797 a: Self::ALPHA_OPAQUE,
798 };
799 pub const TRANSPARENT: Self = Self {
800 r: 0.0,
801 g: 0.0,
802 b: 0.0,
803 a: Self::ALPHA_TRANSPARENT,
804 };
805}
806
807impl From<ColorU> for ColorF {
808 fn from(input: ColorU) -> Self {
809 Self {
810 r: f32::from(input.r) / 255.0,
811 g: f32::from(input.g) / 255.0,
812 b: f32::from(input.b) / 255.0,
813 a: f32::from(input.a) / 255.0,
814 }
815 }
816}
817
818impl From<ColorF> for ColorU {
819 fn from(input: ColorF) -> Self {
820 Self {
821 r: channel_to_u8(input.r.min(1.0) * 255.0),
822 g: channel_to_u8(input.g.min(1.0) * 255.0),
823 b: channel_to_u8(input.b.min(1.0) * 255.0),
824 a: channel_to_u8(input.a.min(1.0) * 255.0),
825 }
826 }
827}
828
829#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
834#[repr(C, u8)]
835pub enum ColorOrSystem {
836 Color(ColorU),
838 System(SystemColorRef),
840}
841
842impl Default for ColorOrSystem {
843 fn default() -> Self {
844 Self::Color(ColorU::BLACK)
845 }
846}
847
848impl From<ColorU> for ColorOrSystem {
849 fn from(color: ColorU) -> Self {
850 Self::Color(color)
851 }
852}
853
854impl ColorOrSystem {
855 #[must_use] pub const fn color(c: ColorU) -> Self {
857 Self::Color(c)
858 }
859
860 #[must_use] pub const fn system(s: SystemColorRef) -> Self {
862 Self::System(s)
863 }
864
865 #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
868 match self {
869 Self::Color(c) => *c,
870 Self::System(ref_type) => ref_type.resolve(system_colors, fallback),
871 }
872 }
873
874 #[must_use] pub const fn to_color_u_with_fallback(&self, fallback: ColorU) -> ColorU {
877 match self {
878 Self::Color(c) => *c,
879 Self::System(_) => fallback,
880 }
881 }
882
883 #[must_use] pub const fn to_color_u_default(&self) -> ColorU {
885 self.to_color_u_with_fallback(ColorU { r: 128, g: 128, b: 128, a: 255 })
886 }
887}
888
889#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
892#[repr(C)]
893pub enum SystemColorRef {
894 Text,
896 Background,
898 Accent,
900 AccentText,
902 ButtonFace,
904 ButtonText,
906 WindowBackground,
908 SelectionBackground,
910 SelectionText,
912}
913
914impl SystemColorRef {
915 #[must_use] pub fn resolve(&self, colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
917 match self {
918 Self::Text => colors.text.as_option().copied().unwrap_or(fallback),
919 Self::Background => colors.background.as_option().copied().unwrap_or(fallback),
920 Self::Accent => colors.accent.as_option().copied().unwrap_or(fallback),
921 Self::AccentText => colors.accent_text.as_option().copied().unwrap_or(fallback),
922 Self::ButtonFace => colors.button_face.as_option().copied().unwrap_or(fallback),
923 Self::ButtonText => colors.button_text.as_option().copied().unwrap_or(fallback),
924 Self::WindowBackground => colors.window_background.as_option().copied().unwrap_or(fallback),
925 Self::SelectionBackground => colors.selection_background.as_option().copied().unwrap_or(fallback),
926 Self::SelectionText => colors.selection_text.as_option().copied().unwrap_or(fallback),
927 }
928 }
929
930 #[must_use] pub const fn as_css_str(&self) -> &'static str {
932 match self {
933 Self::Text => "system:text",
934 Self::Background => "system:background",
935 Self::Accent => "system:accent",
936 Self::AccentText => "system:accent-text",
937 Self::ButtonFace => "system:button-face",
938 Self::ButtonText => "system:button-text",
939 Self::WindowBackground => "system:window-background",
940 Self::SelectionBackground => "system:selection-background",
941 Self::SelectionText => "system:selection-text",
942 }
943 }
944}
945
946#[derive(Debug, Copy, Clone, PartialEq, Eq)]
949#[repr(C)]
950pub enum CssColorComponent {
951 Red,
952 Green,
953 Blue,
954 Hue,
955 Saturation,
956 Lightness,
957 Alpha,
958}
959
960#[derive(Clone, PartialEq)]
961pub enum CssColorParseError<'a> {
962 InvalidColor(&'a str),
963 InvalidFunctionName(&'a str),
964 InvalidColorComponent(u8),
965 IntValueParseErr(ParseIntError),
966 FloatValueParseErr(ParseFloatError),
967 FloatValueOutOfRange(f32),
968 MissingColorComponent(CssColorComponent),
969 ExtraArguments(&'a str),
970 UnclosedColor(&'a str),
971 EmptyInput,
972 DirectionParseError(CssDirectionParseError<'a>),
973 UnsupportedDirection(&'a str),
974 InvalidPercentage(PercentageParseError),
975}
976
977impl_debug_as_display!(CssColorParseError<'a>);
978impl_display! {CssColorParseError<'a>, {
979 InvalidColor(i) => format!("Invalid CSS color: \"{}\"", i),
980 InvalidFunctionName(i) => format!("Invalid function name, expected one of: \"rgb\", \"rgba\", \"hsl\", \"hsla\" got: \"{}\"", i),
981 InvalidColorComponent(i) => format!("Invalid color component when parsing CSS color: \"{}\"", i),
982 IntValueParseErr(e) => format!("CSS color component: Value not in range between 00 - FF: \"{}\"", e),
983 FloatValueParseErr(e) => format!("CSS color component: Value cannot be parsed as floating point number: \"{}\"", e),
984 FloatValueOutOfRange(v) => format!("CSS color component: Value not in range between 0.0 - 1.0: \"{}\"", v),
985 MissingColorComponent(c) => format!("CSS color is missing {:?} component", c),
986 ExtraArguments(a) => format!("Extra argument to CSS color: \"{}\"", a),
987 EmptyInput => format!("Empty color string."),
988 UnclosedColor(i) => format!("Unclosed color: \"{}\"", i),
989 DirectionParseError(e) => format!("Could not parse direction argument for CSS color: \"{}\"", e),
990 UnsupportedDirection(d) => format!("Unsupported direction type for CSS color: \"{}\"", d),
991 InvalidPercentage(p) => format!("Invalid percentage when parsing CSS color: \"{}\"", p),
992}}
993
994impl From<ParseIntError> for CssColorParseError<'_> {
995 fn from(e: ParseIntError) -> Self {
996 CssColorParseError::IntValueParseErr(e)
997 }
998}
999impl From<ParseFloatError> for CssColorParseError<'_> {
1000 fn from(e: ParseFloatError) -> Self {
1001 CssColorParseError::FloatValueParseErr(e)
1002 }
1003}
1004impl From<core::num::ParseIntError> for CssColorParseError<'_> {
1005 fn from(e: core::num::ParseIntError) -> Self {
1006 CssColorParseError::IntValueParseErr(ParseIntError::from(e))
1007 }
1008}
1009impl From<core::num::ParseFloatError> for CssColorParseError<'_> {
1010 fn from(e: core::num::ParseFloatError) -> Self {
1011 CssColorParseError::FloatValueParseErr(ParseFloatError::from(e))
1012 }
1013}
1014impl_from!(
1015 CssDirectionParseError<'a>,
1016 CssColorParseError::DirectionParseError
1017);
1018
1019#[derive(Debug, Clone, PartialEq)]
1020#[repr(C, u8)]
1021pub enum CssColorParseErrorOwned {
1022 InvalidColor(AzString),
1023 InvalidFunctionName(AzString),
1024 InvalidColorComponent(u8),
1025 IntValueParseErr(ParseIntError),
1026 FloatValueParseErr(ParseFloatError),
1027 FloatValueOutOfRange(f32),
1028 MissingColorComponent(CssColorComponent),
1029 ExtraArguments(AzString),
1030 UnclosedColor(AzString),
1031 EmptyInput,
1032 DirectionParseError(CssDirectionParseErrorOwned),
1033 UnsupportedDirection(AzString),
1034 InvalidPercentage(PercentageParseError),
1035}
1036
1037impl CssColorParseError<'_> {
1038 #[must_use] pub fn to_contained(&self) -> CssColorParseErrorOwned {
1039 match self {
1040 CssColorParseError::InvalidColor(s) => {
1041 CssColorParseErrorOwned::InvalidColor((*s).to_string().into())
1042 }
1043 CssColorParseError::InvalidFunctionName(s) => {
1044 CssColorParseErrorOwned::InvalidFunctionName((*s).to_string().into())
1045 }
1046 CssColorParseError::InvalidColorComponent(n) => {
1047 CssColorParseErrorOwned::InvalidColorComponent(*n)
1048 }
1049 CssColorParseError::IntValueParseErr(e) => {
1050 CssColorParseErrorOwned::IntValueParseErr(*e)
1051 }
1052 CssColorParseError::FloatValueParseErr(e) => {
1053 CssColorParseErrorOwned::FloatValueParseErr(*e)
1054 }
1055 CssColorParseError::FloatValueOutOfRange(n) => {
1056 CssColorParseErrorOwned::FloatValueOutOfRange(*n)
1057 }
1058 CssColorParseError::MissingColorComponent(c) => {
1059 CssColorParseErrorOwned::MissingColorComponent(*c)
1060 }
1061 CssColorParseError::ExtraArguments(s) => {
1062 CssColorParseErrorOwned::ExtraArguments((*s).to_string().into())
1063 }
1064 CssColorParseError::UnclosedColor(s) => {
1065 CssColorParseErrorOwned::UnclosedColor((*s).to_string().into())
1066 }
1067 CssColorParseError::EmptyInput => CssColorParseErrorOwned::EmptyInput,
1068 CssColorParseError::DirectionParseError(e) => {
1069 CssColorParseErrorOwned::DirectionParseError(e.to_contained())
1070 }
1071 CssColorParseError::UnsupportedDirection(s) => {
1072 CssColorParseErrorOwned::UnsupportedDirection((*s).to_string().into())
1073 }
1074 CssColorParseError::InvalidPercentage(e) => {
1075 CssColorParseErrorOwned::InvalidPercentage(e.clone())
1076 }
1077 }
1078 }
1079}
1080
1081impl CssColorParseErrorOwned {
1082 #[must_use] pub fn to_shared(&self) -> CssColorParseError<'_> {
1083 match self {
1084 Self::InvalidColor(s) => CssColorParseError::InvalidColor(s),
1085 Self::InvalidFunctionName(s) => {
1086 CssColorParseError::InvalidFunctionName(s)
1087 }
1088 Self::InvalidColorComponent(n) => {
1089 CssColorParseError::InvalidColorComponent(*n)
1090 }
1091 Self::IntValueParseErr(e) => {
1092 CssColorParseError::IntValueParseErr(*e)
1093 }
1094 Self::FloatValueParseErr(e) => {
1095 CssColorParseError::FloatValueParseErr(*e)
1096 }
1097 Self::FloatValueOutOfRange(n) => {
1098 CssColorParseError::FloatValueOutOfRange(*n)
1099 }
1100 Self::MissingColorComponent(c) => {
1101 CssColorParseError::MissingColorComponent(*c)
1102 }
1103 Self::ExtraArguments(s) => CssColorParseError::ExtraArguments(s),
1104 Self::UnclosedColor(s) => CssColorParseError::UnclosedColor(s),
1105 Self::EmptyInput => CssColorParseError::EmptyInput,
1106 Self::DirectionParseError(e) => {
1107 CssColorParseError::DirectionParseError(e.to_shared())
1108 }
1109 Self::UnsupportedDirection(s) => {
1110 CssColorParseError::UnsupportedDirection(s)
1111 }
1112 Self::InvalidPercentage(e) => {
1113 CssColorParseError::InvalidPercentage(e.clone())
1114 }
1115 }
1116 }
1117}
1118
1119#[cfg(feature = "parser")]
1120pub fn parse_css_color(input: &str) -> Result<ColorU, CssColorParseError<'_>> {
1124 use crate::props::basic::parse::{parse_parentheses, ParenthesisParseError};
1125
1126 let input = input.trim();
1127 if let Some(rest) = input.strip_prefix('#') {
1128 return parse_color_no_hash(rest);
1129 }
1130
1131 match parse_parentheses(input, &["rgba", "rgb", "hsla", "hsl"]) {
1132 Ok((stopword, inner_value)) => match stopword {
1133 "rgba" => parse_color_rgb(inner_value, true),
1134 "rgb" => parse_color_rgb(inner_value, false),
1135 "hsla" => parse_color_hsl(inner_value, true),
1136 "hsl" => parse_color_hsl(inner_value, false),
1137 _ => unreachable!(),
1138 },
1139 Err(e) => match e {
1140 ParenthesisParseError::UnclosedBraces | ParenthesisParseError::NoClosingBraceFound => {
1141 Err(CssColorParseError::UnclosedColor(input))
1142 }
1143 ParenthesisParseError::EmptyInput => Err(CssColorParseError::EmptyInput),
1144 ParenthesisParseError::StopWordNotFound(stopword) => {
1145 Err(CssColorParseError::InvalidFunctionName(stopword))
1146 }
1147 ParenthesisParseError::NoOpeningBraceFound => parse_color_builtin(input),
1148 },
1149 }
1150}
1151
1152#[cfg(feature = "parser")]
1165pub fn parse_color_or_system(input: &str) -> Result<ColorOrSystem, CssColorParseError<'_>> {
1169 let input = input.trim();
1170
1171 if let Some(system_name) = input.strip_prefix("system:") {
1173 let system_ref = match system_name.trim() {
1174 "text" => SystemColorRef::Text,
1175 "background" => SystemColorRef::Background,
1176 "accent" => SystemColorRef::Accent,
1177 "accent-text" => SystemColorRef::AccentText,
1178 "button-face" => SystemColorRef::ButtonFace,
1179 "button-text" => SystemColorRef::ButtonText,
1180 "window-background" => SystemColorRef::WindowBackground,
1181 "selection-background" => SystemColorRef::SelectionBackground,
1182 "selection-text" => SystemColorRef::SelectionText,
1183 _ => return Err(CssColorParseError::InvalidColor(input)),
1184 };
1185 return Ok(ColorOrSystem::System(system_ref));
1186 }
1187
1188 parse_css_color(input).map(ColorOrSystem::Color)
1190}
1191
1192#[cfg(feature = "parser")]
1193fn parse_color_no_hash(input: &str) -> Result<ColorU, CssColorParseError<'_>> {
1194 #[inline]
1195 const fn from_hex<'a>(c: u8) -> Result<u8, CssColorParseError<'a>> {
1196 match c {
1197 b'0'..=b'9' => Ok(c - b'0'),
1198 b'a'..=b'f' => Ok(c - b'a' + 10),
1199 b'A'..=b'F' => Ok(c - b'A' + 10),
1200 _ => Err(CssColorParseError::InvalidColorComponent(c)),
1201 }
1202 }
1203
1204 match input.len() {
1205 3 => {
1206 let mut bytes = input.bytes();
1207 let r = bytes.next().unwrap();
1208 let g = bytes.next().unwrap();
1209 let b = bytes.next().unwrap();
1210 Ok(ColorU::new_rgb(
1211 from_hex(r)? * 17,
1212 from_hex(g)? * 17,
1213 from_hex(b)? * 17,
1214 ))
1215 }
1216 4 => {
1217 let mut bytes = input.bytes();
1218 let r = bytes.next().unwrap();
1219 let g = bytes.next().unwrap();
1220 let b = bytes.next().unwrap();
1221 let a = bytes.next().unwrap();
1222 Ok(ColorU::new(
1223 from_hex(r)? * 17,
1224 from_hex(g)? * 17,
1225 from_hex(b)? * 17,
1226 from_hex(a)? * 17,
1227 ))
1228 }
1229 6 => {
1230 if !input.bytes().all(|b| b.is_ascii_hexdigit()) {
1234 return Err(CssColorParseError::InvalidColor(input));
1235 }
1236 let val = u32::from_str_radix(input, 16)?;
1237 Ok(ColorU::new_rgb(
1238 ((val >> 16) & 0xFF) as u8,
1239 ((val >> 8) & 0xFF) as u8,
1240 (val & 0xFF) as u8,
1241 ))
1242 }
1243 8 => {
1244 if !input.bytes().all(|b| b.is_ascii_hexdigit()) {
1245 return Err(CssColorParseError::InvalidColor(input));
1246 }
1247 let val = u32::from_str_radix(input, 16)?;
1248 Ok(ColorU::new(
1249 ((val >> 24) & 0xFF) as u8,
1250 ((val >> 16) & 0xFF) as u8,
1251 ((val >> 8) & 0xFF) as u8,
1252 (val & 0xFF) as u8,
1253 ))
1254 }
1255 _ => Err(CssColorParseError::InvalidColor(input)),
1256 }
1257}
1258
1259#[cfg(feature = "parser")]
1260fn parse_color_rgb(
1261 input: &str,
1262 parse_alpha: bool,
1263) -> Result<ColorU, CssColorParseError<'_>> {
1264 let mut components = input.split(',').map(str::trim);
1265 let rgb_color = parse_color_rgb_components(&mut components)?;
1266 let a = if parse_alpha {
1267 parse_alpha_component(&mut components)?
1268 } else {
1269 255
1270 };
1271 if let Some(arg) = components.next() {
1272 return Err(CssColorParseError::ExtraArguments(arg));
1273 }
1274 Ok(ColorU { a, ..rgb_color })
1275}
1276
1277#[cfg(feature = "parser")]
1278fn parse_color_rgb_components<'a>(
1279 components: &mut dyn Iterator<Item = &'a str>,
1280) -> Result<ColorU, CssColorParseError<'a>> {
1281 #[inline]
1282 fn component_from_str<'a>(
1283 components: &mut dyn Iterator<Item = &'a str>,
1284 which: CssColorComponent,
1285 ) -> Result<u8, CssColorParseError<'a>> {
1286 let c = components
1287 .next()
1288 .ok_or(CssColorParseError::MissingColorComponent(which))?;
1289 if c.is_empty() {
1290 return Err(CssColorParseError::MissingColorComponent(which));
1291 }
1292 Ok(c.parse::<u8>()?)
1293 }
1294 Ok(ColorU {
1295 r: component_from_str(components, CssColorComponent::Red)?,
1296 g: component_from_str(components, CssColorComponent::Green)?,
1297 b: component_from_str(components, CssColorComponent::Blue)?,
1298 a: 255,
1299 })
1300}
1301
1302#[cfg(feature = "parser")]
1303fn parse_color_hsl(
1304 input: &str,
1305 parse_alpha: bool,
1306) -> Result<ColorU, CssColorParseError<'_>> {
1307 let mut components = input.split(',').map(str::trim);
1308 let rgb_color = parse_color_hsl_components(&mut components)?;
1309 let a = if parse_alpha {
1310 parse_alpha_component(&mut components)?
1311 } else {
1312 255
1313 };
1314 if let Some(arg) = components.next() {
1315 return Err(CssColorParseError::ExtraArguments(arg));
1316 }
1317 Ok(ColorU { a, ..rgb_color })
1318}
1319
1320#[cfg(feature = "parser")]
1321#[allow(clippy::many_single_char_names)] fn parse_color_hsl_components<'a>(
1323 components: &mut dyn Iterator<Item = &'a str>,
1324) -> Result<ColorU, CssColorParseError<'a>> {
1325 #[inline]
1326 fn angle_from_str<'a>(
1327 components: &mut dyn Iterator<Item = &'a str>,
1328 which: CssColorComponent,
1329 ) -> Result<f32, CssColorParseError<'a>> {
1330 let c = components
1331 .next()
1332 .ok_or(CssColorParseError::MissingColorComponent(which))?;
1333 if c.is_empty() {
1334 return Err(CssColorParseError::MissingColorComponent(which));
1335 }
1336 let dir = parse_direction(c)?;
1337 match dir {
1338 Direction::Angle(deg) => Ok(deg.to_degrees()),
1339 Direction::FromTo(_) => Err(CssColorParseError::UnsupportedDirection(c)),
1340 }
1341 }
1342
1343 #[inline]
1344 fn percent_from_str<'a>(
1345 components: &mut dyn Iterator<Item = &'a str>,
1346 which: CssColorComponent,
1347 ) -> Result<f32, CssColorParseError<'a>> {
1348 use crate::props::basic::parse_percentage_value;
1349
1350 let c = components
1351 .next()
1352 .ok_or(CssColorParseError::MissingColorComponent(which))?;
1353 if c.is_empty() {
1354 return Err(CssColorParseError::MissingColorComponent(which));
1355 }
1356
1357 Ok(parse_percentage_value(c)
1359 .map_err(CssColorParseError::InvalidPercentage)?
1360 .normalized()
1361 * 100.0)
1362 }
1363
1364 #[inline]
1365 #[allow(clippy::suboptimal_flops)] #[allow(clippy::many_single_char_names)] fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
1368 let s = s / 100.0;
1369 let l = l / 100.0;
1370 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
1371 let h_prime = h / 60.0;
1372 let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
1373 let (r1, g1, b1) = if (0.0..1.0).contains(&h_prime) {
1374 (c, x, 0.0)
1375 } else if (1.0..2.0).contains(&h_prime) {
1376 (x, c, 0.0)
1377 } else if (2.0..3.0).contains(&h_prime) {
1378 (0.0, c, x)
1379 } else if (3.0..4.0).contains(&h_prime) {
1380 (0.0, x, c)
1381 } else if (4.0..5.0).contains(&h_prime) {
1382 (x, 0.0, c)
1383 } else {
1384 (c, 0.0, x)
1385 };
1386 let m = l - c / 2.0;
1387 (
1388 channel_to_u8((r1 + m) * 255.0),
1389 channel_to_u8((g1 + m) * 255.0),
1390 channel_to_u8((b1 + m) * 255.0),
1391 )
1392 }
1393
1394 let (h, s, l) = (
1395 angle_from_str(components, CssColorComponent::Hue)?,
1396 percent_from_str(components, CssColorComponent::Saturation)?,
1397 percent_from_str(components, CssColorComponent::Lightness)?,
1398 );
1399
1400 let (r, g, b) = hsl_to_rgb(h, s, l);
1401 Ok(ColorU { r, g, b, a: 255 })
1402}
1403
1404#[cfg(feature = "parser")]
1405fn parse_alpha_component<'a>(
1406 components: &mut dyn Iterator<Item = &'a str>,
1407) -> Result<u8, CssColorParseError<'a>> {
1408 let a_str = components
1409 .next()
1410 .ok_or(CssColorParseError::MissingColorComponent(
1411 CssColorComponent::Alpha,
1412 ))?;
1413 if a_str.is_empty() {
1414 return Err(CssColorParseError::MissingColorComponent(
1415 CssColorComponent::Alpha,
1416 ));
1417 }
1418 let a = a_str.parse::<f32>()?;
1419 if !(0.0..=1.0).contains(&a) {
1420 return Err(CssColorParseError::FloatValueOutOfRange(a));
1421 }
1422 Ok(channel_to_u8((a * 255.0).round()))
1423}
1424
1425#[cfg(feature = "parser")]
1426#[allow(clippy::too_many_lines)] fn parse_color_builtin(input: &str) -> Result<ColorU, CssColorParseError<'_>> {
1428 let (r, g, b, a) = match input.to_lowercase().as_str() {
1429 "aliceblue" => (240, 248, 255, 255),
1430 "antiquewhite" => (250, 235, 215, 255),
1431 "aqua" | "cyan" => (0, 255, 255, 255),
1432 "aquamarine" => (127, 255, 212, 255),
1433 "azure" => (240, 255, 255, 255),
1434 "beige" => (245, 245, 220, 255),
1435 "bisque" => (255, 228, 196, 255),
1436 "black" => (0, 0, 0, 255),
1437 "blanchedalmond" => (255, 235, 205, 255),
1438 "blue" => (0, 0, 255, 255),
1439 "blueviolet" => (138, 43, 226, 255),
1440 "brown" => (165, 42, 42, 255),
1441 "burlywood" => (222, 184, 135, 255),
1442 "cadetblue" => (95, 158, 160, 255),
1443 "chartreuse" => (127, 255, 0, 255),
1444 "chocolate" => (210, 105, 30, 255),
1445 "coral" => (255, 127, 80, 255),
1446 "cornflowerblue" => (100, 149, 237, 255),
1447 "cornsilk" => (255, 248, 220, 255),
1448 "crimson" => (220, 20, 60, 255),
1449 "darkblue" => (0, 0, 139, 255),
1450 "darkcyan" => (0, 139, 139, 255),
1451 "darkgoldenrod" => (184, 134, 11, 255),
1452 "darkgray" | "darkgrey" => (169, 169, 169, 255),
1453 "darkgreen" => (0, 100, 0, 255),
1454 "darkkhaki" => (189, 183, 107, 255),
1455 "darkmagenta" => (139, 0, 139, 255),
1456 "darkolivegreen" => (85, 107, 47, 255),
1457 "darkorange" => (255, 140, 0, 255),
1458 "darkorchid" => (153, 50, 204, 255),
1459 "darkred" => (139, 0, 0, 255),
1460 "darksalmon" => (233, 150, 122, 255),
1461 "darkseagreen" => (143, 188, 143, 255),
1462 "darkslateblue" => (72, 61, 139, 255),
1463 "darkslategray" | "darkslategrey" => (47, 79, 79, 255),
1464 "darkturquoise" => (0, 206, 209, 255),
1465 "darkviolet" => (148, 0, 211, 255),
1466 "deeppink" => (255, 20, 147, 255),
1467 "deepskyblue" => (0, 191, 255, 255),
1468 "dimgray" | "dimgrey" => (105, 105, 105, 255),
1469 "dodgerblue" => (30, 144, 255, 255),
1470 "firebrick" => (178, 34, 34, 255),
1471 "floralwhite" => (255, 250, 240, 255),
1472 "forestgreen" => (34, 139, 34, 255),
1473 "fuchsia" | "magenta" => (255, 0, 255, 255),
1474 "gainsboro" => (220, 220, 220, 255),
1475 "ghostwhite" => (248, 248, 255, 255),
1476 "gold" => (255, 215, 0, 255),
1477 "goldenrod" => (218, 165, 32, 255),
1478 "gray" | "grey" => (128, 128, 128, 255),
1479 "green" => (0, 128, 0, 255),
1480 "greenyellow" => (173, 255, 47, 255),
1481 "honeydew" => (240, 255, 240, 255),
1482 "hotpink" => (255, 105, 180, 255),
1483 "indianred" => (205, 92, 92, 255),
1484 "indigo" => (75, 0, 130, 255),
1485 "ivory" => (255, 255, 240, 255),
1486 "khaki" => (240, 230, 140, 255),
1487 "lavender" => (230, 230, 250, 255),
1488 "lavenderblush" => (255, 240, 245, 255),
1489 "lawngreen" => (124, 252, 0, 255),
1490 "lemonchiffon" => (255, 250, 205, 255),
1491 "lightblue" => (173, 216, 230, 255),
1492 "lightcoral" => (240, 128, 128, 255),
1493 "lightcyan" => (224, 255, 255, 255),
1494 "lightgoldenrodyellow" => (250, 250, 210, 255),
1495 "lightgray" | "lightgrey" => (211, 211, 211, 255),
1496 "lightgreen" => (144, 238, 144, 255),
1497 "lightpink" => (255, 182, 193, 255),
1498 "lightsalmon" => (255, 160, 122, 255),
1499 "lightseagreen" => (32, 178, 170, 255),
1500 "lightskyblue" => (135, 206, 250, 255),
1501 "lightslategray" | "lightslategrey" => (119, 136, 153, 255),
1502 "lightsteelblue" => (176, 196, 222, 255),
1503 "lightyellow" => (255, 255, 224, 255),
1504 "lime" => (0, 255, 0, 255),
1505 "limegreen" => (50, 205, 50, 255),
1506 "linen" => (250, 240, 230, 255),
1507 "maroon" => (128, 0, 0, 255),
1508 "mediumaquamarine" => (102, 205, 170, 255),
1509 "mediumblue" => (0, 0, 205, 255),
1510 "mediumorchid" => (186, 85, 211, 255),
1511 "mediumpurple" => (147, 112, 219, 255),
1512 "mediumseagreen" => (60, 179, 113, 255),
1513 "mediumslateblue" => (123, 104, 238, 255),
1514 "mediumspringgreen" => (0, 250, 154, 255),
1515 "mediumturquoise" => (72, 209, 204, 255),
1516 "mediumvioletred" => (199, 21, 133, 255),
1517 "midnightblue" => (25, 25, 112, 255),
1518 "mintcream" => (245, 255, 250, 255),
1519 "mistyrose" => (255, 228, 225, 255),
1520 "moccasin" => (255, 228, 181, 255),
1521 "navajowhite" => (255, 222, 173, 255),
1522 "navy" => (0, 0, 128, 255),
1523 "oldlace" => (253, 245, 230, 255),
1524 "olive" => (128, 128, 0, 255),
1525 "olivedrab" => (107, 142, 35, 255),
1526 "orange" => (255, 165, 0, 255),
1527 "orangered" => (255, 69, 0, 255),
1528 "orchid" => (218, 112, 214, 255),
1529 "palegoldenrod" => (238, 232, 170, 255),
1530 "palegreen" => (152, 251, 152, 255),
1531 "paleturquoise" => (175, 238, 238, 255),
1532 "palevioletred" => (219, 112, 147, 255),
1533 "papayawhip" => (255, 239, 213, 255),
1534 "peachpuff" => (255, 218, 185, 255),
1535 "peru" => (205, 133, 63, 255),
1536 "pink" => (255, 192, 203, 255),
1537 "plum" => (221, 160, 221, 255),
1538 "powderblue" => (176, 224, 230, 255),
1539 "purple" => (128, 0, 128, 255),
1540 "rebeccapurple" => (102, 51, 153, 255),
1541 "red" => (255, 0, 0, 255),
1542 "rosybrown" => (188, 143, 143, 255),
1543 "royalblue" => (65, 105, 225, 255),
1544 "saddlebrown" => (139, 69, 19, 255),
1545 "salmon" => (250, 128, 114, 255),
1546 "sandybrown" => (244, 164, 96, 255),
1547 "seagreen" => (46, 139, 87, 255),
1548 "seashell" => (255, 245, 238, 255),
1549 "sienna" => (160, 82, 45, 255),
1550 "silver" => (192, 192, 192, 255),
1551 "skyblue" => (135, 206, 235, 255),
1552 "slateblue" => (106, 90, 205, 255),
1553 "slategray" | "slategrey" => (112, 128, 144, 255),
1554 "snow" => (255, 250, 250, 255),
1555 "springgreen" => (0, 255, 127, 255),
1556 "steelblue" => (70, 130, 180, 255),
1557 "tan" => (210, 180, 140, 255),
1558 "teal" => (0, 128, 128, 255),
1559 "thistle" => (216, 191, 216, 255),
1560 "tomato" => (255, 99, 71, 255),
1561 "transparent" => (0, 0, 0, 0),
1562 "turquoise" => (64, 224, 208, 255),
1563 "violet" => (238, 130, 238, 255),
1564 "wheat" => (245, 222, 179, 255),
1565 "white" => (255, 255, 255, 255),
1566 "whitesmoke" => (245, 245, 245, 255),
1567 "yellow" => (255, 255, 0, 255),
1568 "yellowgreen" => (154, 205, 50, 255),
1569 _ => return Err(CssColorParseError::InvalidColor(input)),
1570 };
1571 Ok(ColorU { r, g, b, a })
1572}
1573
1574#[cfg(all(test, feature = "parser"))]
1575mod tests {
1576 use super::*;
1577
1578 #[test]
1579 fn test_parse_color_keywords() {
1580 assert_eq!(parse_css_color("red").unwrap(), ColorU::RED);
1581 assert_eq!(parse_css_color("blue").unwrap(), ColorU::BLUE);
1582 assert_eq!(parse_css_color("transparent").unwrap(), ColorU::TRANSPARENT);
1583 assert_eq!(
1584 parse_css_color("rebeccapurple").unwrap(),
1585 ColorU::new_rgb(102, 51, 153)
1586 );
1587 }
1588
1589 #[test]
1590 fn test_parse_color_hex() {
1591 assert_eq!(parse_css_color("#f00").unwrap(), ColorU::RED);
1593 assert_eq!(
1595 parse_css_color("#f008").unwrap(),
1596 ColorU::new(255, 0, 0, 136)
1597 );
1598 assert_eq!(parse_css_color("#00ff00").unwrap(), ColorU::GREEN);
1600 assert_eq!(
1602 parse_css_color("#0000ff80").unwrap(),
1603 ColorU::new(0, 0, 255, 128)
1604 );
1605 assert_eq!(
1607 parse_css_color("#FFC0CB").unwrap(),
1608 ColorU::new_rgb(255, 192, 203)
1609 ); }
1611
1612 #[test]
1613 fn test_parse_color_rgb() {
1614 assert_eq!(parse_css_color("rgb(255, 0, 0)").unwrap(), ColorU::RED);
1615 assert_eq!(
1616 parse_css_color("rgba(0, 255, 0, 0.5)").unwrap(),
1617 ColorU::new(0, 255, 0, 128)
1618 );
1619 assert_eq!(
1620 parse_css_color("rgba(10, 20, 30, 1)").unwrap(),
1621 ColorU::new_rgb(10, 20, 30)
1622 );
1623 assert_eq!(parse_css_color("rgb( 0 , 0 , 0 )").unwrap(), ColorU::BLACK);
1624 }
1625
1626 #[test]
1627 fn test_parse_color_hsl() {
1628 assert_eq!(parse_css_color("hsl(0, 100%, 50%)").unwrap(), ColorU::RED);
1629 assert_eq!(
1630 parse_css_color("hsl(120, 100%, 50%)").unwrap(),
1631 ColorU::GREEN
1632 );
1633 assert_eq!(
1634 parse_css_color("hsla(240, 100%, 50%, 0.5)").unwrap(),
1635 ColorU::new(0, 0, 255, 128)
1636 );
1637 assert_eq!(parse_css_color("hsl(0, 0%, 0%)").unwrap(), ColorU::BLACK);
1638 }
1639
1640 #[test]
1641 fn test_parse_color_errors() {
1642 assert!(parse_css_color("redd").is_err());
1643 assert!(parse_css_color("#12345").is_err()); 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()); }
1653
1654 #[test]
1655 fn test_parse_system_colors() {
1656 assert_eq!(
1658 parse_color_or_system("system:accent").unwrap(),
1659 ColorOrSystem::System(SystemColorRef::Accent)
1660 );
1661 assert_eq!(
1662 parse_color_or_system("system:text").unwrap(),
1663 ColorOrSystem::System(SystemColorRef::Text)
1664 );
1665 assert_eq!(
1666 parse_color_or_system("system:background").unwrap(),
1667 ColorOrSystem::System(SystemColorRef::Background)
1668 );
1669 assert_eq!(
1670 parse_color_or_system("system:selection-background").unwrap(),
1671 ColorOrSystem::System(SystemColorRef::SelectionBackground)
1672 );
1673 assert_eq!(
1674 parse_color_or_system("system:selection-text").unwrap(),
1675 ColorOrSystem::System(SystemColorRef::SelectionText)
1676 );
1677 assert_eq!(
1678 parse_color_or_system("system:accent-text").unwrap(),
1679 ColorOrSystem::System(SystemColorRef::AccentText)
1680 );
1681 assert_eq!(
1682 parse_color_or_system("system:button-face").unwrap(),
1683 ColorOrSystem::System(SystemColorRef::ButtonFace)
1684 );
1685 assert_eq!(
1686 parse_color_or_system("system:button-text").unwrap(),
1687 ColorOrSystem::System(SystemColorRef::ButtonText)
1688 );
1689 assert_eq!(
1690 parse_color_or_system("system:window-background").unwrap(),
1691 ColorOrSystem::System(SystemColorRef::WindowBackground)
1692 );
1693
1694 assert!(parse_color_or_system("system:invalid").is_err());
1696
1697 assert_eq!(
1699 parse_color_or_system("red").unwrap(),
1700 ColorOrSystem::Color(ColorU::RED)
1701 );
1702 assert_eq!(
1703 parse_color_or_system("#ff0000").unwrap(),
1704 ColorOrSystem::Color(ColorU::RED)
1705 );
1706 }
1707
1708 #[test]
1709 fn test_system_color_resolution() {
1710 use crate::system::SystemColors;
1711
1712 let system_colors = SystemColors {
1713 text: OptionColorU::Some(ColorU::BLACK),
1714 secondary_text: OptionColorU::None,
1715 tertiary_text: OptionColorU::None,
1716 background: OptionColorU::Some(ColorU::WHITE),
1717 accent: OptionColorU::Some(ColorU::new_rgb(0, 122, 255)), accent_text: OptionColorU::Some(ColorU::WHITE),
1719 button_face: OptionColorU::Some(ColorU::new_rgb(240, 240, 240)),
1720 button_text: OptionColorU::Some(ColorU::BLACK),
1721 disabled_text: OptionColorU::None,
1722 window_background: OptionColorU::Some(ColorU::WHITE),
1723 under_page_background: OptionColorU::None,
1724 selection_background: OptionColorU::Some(ColorU::new_rgb(0, 120, 215)),
1725 selection_text: OptionColorU::Some(ColorU::WHITE),
1726 selection_background_inactive: OptionColorU::None,
1727 selection_text_inactive: OptionColorU::None,
1728 link: OptionColorU::None,
1729 separator: OptionColorU::None,
1730 grid: OptionColorU::None,
1731 find_highlight: OptionColorU::None,
1732 sidebar_background: OptionColorU::None,
1733 sidebar_selection: OptionColorU::None,
1734 };
1735
1736 let accent_ref = ColorOrSystem::System(SystemColorRef::Accent);
1738 let resolved = accent_ref.resolve(&system_colors, ColorU::GRAY);
1739 assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
1740
1741 let empty_colors = SystemColors::default();
1743 let resolved_fallback = accent_ref.resolve(&empty_colors, ColorU::GRAY);
1744 assert_eq!(resolved_fallback, ColorU::GRAY);
1745
1746 let concrete = ColorOrSystem::Color(ColorU::RED);
1748 let resolved_concrete = concrete.resolve(&system_colors, ColorU::GRAY);
1749 assert_eq!(resolved_concrete, ColorU::RED);
1750 }
1751
1752 #[test]
1753 fn test_system_color_css_str() {
1754 assert_eq!(SystemColorRef::Accent.as_css_str(), "system:accent");
1755 assert_eq!(SystemColorRef::Text.as_css_str(), "system:text");
1756 assert_eq!(SystemColorRef::Background.as_css_str(), "system:background");
1757 assert_eq!(SystemColorRef::SelectionBackground.as_css_str(), "system:selection-background");
1758 }
1759}
1760
1761#[cfg(test)]
1762#[allow(clippy::float_cmp, clippy::unreadable_literal)]
1763mod autotest_generated {
1764 use super::*;
1765
1766 const SAMPLES: [ColorU; 10] = [
1769 ColorU { r: 0, g: 0, b: 0, a: 0 },
1770 ColorU { r: 0, g: 0, b: 0, a: 255 },
1771 ColorU { r: 255, g: 255, b: 255, a: 255 },
1772 ColorU { r: 255, g: 255, b: 255, a: 0 },
1773 ColorU { r: 1, g: 2, b: 3, a: 4 },
1774 ColorU { r: 127, g: 128, b: 129, a: 254 },
1775 ColorU { r: 254, g: 1, b: 128, a: 1 },
1776 ColorU { r: 128, g: 128, b: 128, a: 255 },
1777 ColorU { r: 255, g: 0, b: 0, a: 255 },
1778 ColorU { r: 13, g: 110, b: 253, a: 200 },
1779 ];
1780
1781 #[test]
1786 fn channel_to_u8_zero_and_negative_zero() {
1787 assert_eq!(channel_to_u8(0.0), 0);
1788 assert_eq!(channel_to_u8(-0.0), 0);
1789 }
1790
1791 #[test]
1792 fn channel_to_u8_truncates_toward_zero_and_does_not_round() {
1793 assert_eq!(channel_to_u8(0.9), 0);
1794 assert_eq!(channel_to_u8(127.5), 127);
1795 assert_eq!(channel_to_u8(254.999), 254);
1796 assert_eq!(channel_to_u8(255.0), 255);
1797 assert_eq!(channel_to_u8(255.9), 255);
1798 }
1799
1800 #[test]
1801 fn channel_to_u8_saturates_on_overflow_instead_of_wrapping() {
1802 assert_eq!(channel_to_u8(256.0), 255);
1803 assert_eq!(channel_to_u8(1e30), 255);
1804 assert_eq!(channel_to_u8(f32::MAX), 255);
1805 }
1806
1807 #[test]
1808 fn channel_to_u8_negative_saturates_to_zero() {
1809 assert_eq!(channel_to_u8(-0.5), 0);
1810 assert_eq!(channel_to_u8(-1.0), 0);
1811 assert_eq!(channel_to_u8(-1e30), 0);
1812 assert_eq!(channel_to_u8(f32::MIN), 0);
1813 }
1814
1815 #[test]
1816 fn channel_to_u8_nan_and_inf_are_defined_and_do_not_panic() {
1817 assert_eq!(channel_to_u8(f32::NAN), 0);
1818 assert_eq!(channel_to_u8(-f32::NAN), 0);
1819 assert_eq!(channel_to_u8(f32::INFINITY), 255);
1820 assert_eq!(channel_to_u8(f32::NEG_INFINITY), 0);
1821 }
1822
1823 #[test]
1824 fn channel_to_u8_subnormal_inputs_do_not_panic() {
1825 assert_eq!(channel_to_u8(f32::MIN_POSITIVE), 0);
1826 assert_eq!(channel_to_u8(1e-45), 0);
1827 assert_eq!(channel_to_u8(-1e-45), 0);
1828 }
1829
1830 #[test]
1835 fn rgba_fields_match_args_at_min_and_max() {
1836 let min = ColorU::rgba(0, 0, 0, 0);
1837 assert_eq!((min.r, min.g, min.b, min.a), (0, 0, 0, 0));
1838 let max = ColorU::rgba(u8::MAX, u8::MAX, u8::MAX, u8::MAX);
1839 assert_eq!((max.r, max.g, max.b, max.a), (255, 255, 255, 255));
1840 let mixed = ColorU::rgba(1, 2, 3, 4);
1841 assert_eq!((mixed.r, mixed.g, mixed.b, mixed.a), (1, 2, 3, 4));
1842 }
1843
1844 #[test]
1845 fn rgb_defaults_alpha_to_opaque() {
1846 assert_eq!(ColorU::rgb(0, 0, 0), ColorU::BLACK);
1847 assert_eq!(ColorU::rgb(1, 2, 3).a, ColorU::ALPHA_OPAQUE);
1848 assert_eq!(ColorU::rgb(u8::MAX, u8::MAX, u8::MAX), ColorU::WHITE);
1849 }
1850
1851 #[test]
1852 fn new_and_new_rgb_are_exact_aliases() {
1853 for c in SAMPLES {
1854 assert_eq!(ColorU::new(c.r, c.g, c.b, c.a), ColorU::rgba(c.r, c.g, c.b, c.a));
1855 assert_eq!(ColorU::new_rgb(c.r, c.g, c.b), ColorU::rgb(c.r, c.g, c.b));
1856 }
1857 }
1858
1859 #[test]
1860 fn with_alpha_keeps_rgb_for_every_alpha() {
1861 let base = ColorU::rgba(13, 110, 253, 7);
1862 for a in 0..=u8::MAX {
1863 let c = base.with_alpha(a);
1864 assert_eq!((c.r, c.g, c.b), (base.r, base.g, base.b));
1865 assert_eq!(c.a, a);
1866 }
1867 }
1868
1869 #[test]
1870 fn with_alpha_f32_clamps_out_of_range_and_nan() {
1871 let base = ColorU::rgb(1, 2, 3);
1872 assert_eq!(base.with_alpha_f32(0.0).a, 0);
1873 assert_eq!(base.with_alpha_f32(1.0).a, 255);
1874 assert_eq!(base.with_alpha_f32(-1.0).a, 0);
1876 assert_eq!(base.with_alpha_f32(-1e30).a, 0);
1877 assert_eq!(base.with_alpha_f32(2.0).a, 255);
1878 assert_eq!(base.with_alpha_f32(1e30).a, 255);
1879 assert_eq!(base.with_alpha_f32(f32::INFINITY).a, 255);
1880 assert_eq!(base.with_alpha_f32(f32::NEG_INFINITY).a, 0);
1881 assert_eq!(base.with_alpha_f32(f32::NAN).a, 0);
1883 for a in [-1.0, 0.0, 0.5, 1.0, 2.0, f32::NAN, f32::INFINITY] {
1885 let c = base.with_alpha_f32(a);
1886 assert_eq!((c.r, c.g, c.b), (1, 2, 3));
1887 }
1888 }
1889
1890 #[test]
1891 fn with_alpha_f32_truncates_rather_than_rounds() {
1892 assert_eq!(ColorU::rgb(0, 0, 0).with_alpha_f32(0.5).a, 127);
1896 }
1897
1898 #[test]
1903 fn interpolate_endpoints_are_exact() {
1904 for a in SAMPLES {
1905 for b in SAMPLES {
1906 assert_eq!(a.interpolate(&b, 0.0), a, "t=0 must return self");
1907 assert_eq!(a.interpolate(&b, 1.0), b, "t=1 must return other");
1908 }
1909 }
1910 }
1911
1912 #[test]
1913 fn interpolate_midpoint_rounds_half_away_from_zero() {
1914 assert_eq!(
1916 ColorU::BLACK.interpolate(&ColorU::WHITE, 0.5),
1917 ColorU::rgba(128, 128, 128, 255)
1918 );
1919 }
1920
1921 #[test]
1922 fn interpolate_is_symmetric_under_swapped_endpoints() {
1923 for a in SAMPLES {
1924 for b in SAMPLES {
1925 assert_eq!(a.interpolate(&b, 0.25), b.interpolate(&a, 0.75));
1926 }
1927 }
1928 }
1929
1930 #[test]
1931 fn interpolate_nan_t_is_defined_and_does_not_panic() {
1932 for a in SAMPLES {
1934 for b in SAMPLES {
1935 assert_eq!(a.interpolate(&b, f32::NAN), ColorU::rgba(0, 0, 0, 0));
1936 }
1937 }
1938 }
1939
1940 #[test]
1941 fn interpolate_infinite_t_saturates_differing_channels() {
1942 let c = ColorU::rgba(0, 0, 0, 0).interpolate(&ColorU::WHITE, f32::INFINITY);
1944 assert_eq!(c, ColorU::rgba(255, 255, 255, 255));
1945 let c = ColorU::WHITE.interpolate(&ColorU::rgba(0, 0, 0, 0), f32::INFINITY);
1946 assert_eq!(c, ColorU::rgba(0, 0, 0, 0));
1947 }
1948
1949 #[test]
1950 fn interpolate_infinite_t_zeroes_equal_channels() {
1951 let c = ColorU::BLACK.interpolate(&ColorU::WHITE, f32::INFINITY);
1955 assert_eq!(c, ColorU::rgba(255, 255, 255, 0));
1956 assert_eq!(
1958 ColorU::RED.interpolate(&ColorU::RED, f32::INFINITY),
1959 ColorU::rgba(0, 0, 0, 0)
1960 );
1961 }
1962
1963 #[test]
1964 fn interpolate_out_of_range_t_saturates_instead_of_wrapping() {
1965 assert_eq!(
1968 ColorU::BLACK.interpolate(&ColorU::WHITE, 2.0),
1969 ColorU::rgba(255, 255, 255, 255)
1970 );
1971 assert_eq!(
1972 ColorU::WHITE.interpolate(&ColorU::BLACK, -1.0),
1973 ColorU::rgba(255, 255, 255, 255)
1974 );
1975 assert_eq!(
1976 ColorU::WHITE.interpolate(&ColorU::BLACK, 2.0),
1977 ColorU::rgba(0, 0, 0, 255)
1978 );
1979 assert_eq!(
1980 ColorU::BLACK.interpolate(&ColorU::WHITE, -1.0),
1981 ColorU::rgba(0, 0, 0, 255)
1982 );
1983 for t in [-1e30, -1.0, -0.5, 1.5, 2.0, 1e30] {
1985 for a in SAMPLES {
1986 for b in SAMPLES {
1987 assert_eq!(a.interpolate(&b, t), a.interpolate(&b, t));
1988 }
1989 }
1990 }
1991 }
1992
1993 #[test]
1994 fn lighten_and_darken_clamp_the_amount() {
1995 let base = ColorU::rgba(128, 128, 128, 77);
1996 assert_eq!(base.lighten(0.0), base);
1998 assert_eq!(base.darken(0.0), base);
1999 assert_eq!(base.lighten(-1.0), base);
2000 assert_eq!(base.darken(-1e30), base);
2001 assert_eq!(base.lighten(f32::NEG_INFINITY), base);
2002 assert_eq!(base.lighten(1.0), ColorU::rgba(255, 255, 255, 77));
2004 assert_eq!(base.lighten(2.0), ColorU::rgba(255, 255, 255, 77));
2005 assert_eq!(base.lighten(f32::INFINITY), ColorU::rgba(255, 255, 255, 77));
2006 assert_eq!(base.darken(1.0), ColorU::rgba(0, 0, 0, 77));
2007 assert_eq!(base.darken(1e30), ColorU::rgba(0, 0, 0, 77));
2008 assert_eq!(base.darken(f32::INFINITY), ColorU::rgba(0, 0, 0, 77));
2009 }
2010
2011 #[test]
2012 fn lighten_and_darken_always_preserve_alpha() {
2013 for c in SAMPLES {
2014 for amount in [-1.0, 0.0, 0.3, 1.0, 2.0, f32::NAN, f32::INFINITY] {
2015 assert_eq!(c.lighten(amount).a, c.a);
2016 assert_eq!(c.darken(amount).a, c.a);
2017 }
2018 }
2019 }
2020
2021 #[test]
2022 fn lighten_nan_amount_is_defined_and_does_not_panic() {
2023 let c = ColorU::rgba(255, 0, 0, 200);
2026 assert_eq!(c.lighten(f32::NAN), ColorU::rgba(0, 0, 0, 200));
2027 assert_eq!(c.darken(f32::NAN), ColorU::rgba(0, 0, 0, 200));
2028 }
2029
2030 #[test]
2031 fn mix_clamps_ratio_to_the_endpoints() {
2032 let a = ColorU::rgba(10, 20, 30, 40);
2033 let b = ColorU::rgba(200, 210, 220, 230);
2034 assert_eq!(a.mix(&b, 0.0), a);
2035 assert_eq!(a.mix(&b, 1.0), b);
2036 assert_eq!(a.mix(&b, -1.0), a);
2037 assert_eq!(a.mix(&b, f32::NEG_INFINITY), a);
2038 assert_eq!(a.mix(&b, 2.0), b);
2039 assert_eq!(a.mix(&b, 1e30), b);
2040 assert_eq!(a.mix(&b, f32::INFINITY), b);
2041 }
2042
2043 #[test]
2044 fn mix_nan_ratio_is_defined_and_does_not_panic() {
2045 assert_eq!(
2047 ColorU::RED.mix(&ColorU::BLUE, f32::NAN),
2048 ColorU::rgba(0, 0, 0, 0)
2049 );
2050 }
2051
2052 #[test]
2057 fn srgb_to_linear_endpoints_and_monotonicity() {
2058 assert_eq!(ColorU::srgb_to_linear(0.0), 0.0);
2059 assert!((ColorU::srgb_to_linear(1.0) - 1.0).abs() < 1e-5);
2060 let mut prev = f32::NEG_INFINITY;
2062 for i in 0..=255u16 {
2063 let v = ColorU::srgb_to_linear(f32::from(i) / 255.0);
2064 assert!(v >= prev, "srgb_to_linear not monotonic at {i}");
2065 assert!((0.0..=1.0).contains(&v), "out of range at {i}: {v}");
2066 prev = v;
2067 }
2068 }
2069
2070 #[test]
2071 fn srgb_to_linear_handles_the_piecewise_boundary() {
2072 let below = ColorU::srgb_to_linear(0.03928);
2074 assert!((below - 0.03928 / 12.92).abs() < 1e-9);
2075 let above = ColorU::srgb_to_linear(0.03929);
2076 assert!(above > below, "must not go backwards across the boundary");
2077 }
2078
2079 #[test]
2080 fn srgb_to_linear_nan_inf_and_negative_do_not_panic() {
2081 assert!(ColorU::srgb_to_linear(f32::NAN).is_nan());
2082 assert_eq!(ColorU::srgb_to_linear(f32::INFINITY), f32::INFINITY);
2083 assert_eq!(ColorU::srgb_to_linear(f32::NEG_INFINITY), f32::NEG_INFINITY);
2084 assert!(ColorU::srgb_to_linear(-1.0) < 0.0);
2086 assert_eq!(ColorU::srgb_to_linear(-0.0), -0.0);
2087 }
2088
2089 #[test]
2094 fn luminance_endpoints_and_range() {
2095 assert!((ColorU::BLACK.luminance() - 0.0).abs() < 1e-6);
2096 assert!((ColorU::WHITE.luminance() - 1.0).abs() < 1e-6);
2097 for r in (0..=255u16).step_by(17) {
2098 for g in (0..=255u16).step_by(51) {
2099 for b in (0..=255u16).step_by(85) {
2100 #[allow(clippy::cast_possible_truncation)]
2101 let l = ColorU::rgb(r as u8, g as u8, b as u8).luminance();
2102 assert!(l.is_finite() && (-1e-6..=1.000_001).contains(&l), "luminance {l}");
2103 }
2104 }
2105 }
2106 }
2107
2108 #[test]
2109 fn luminance_ignores_alpha() {
2110 for a in [0u8, 1, 128, 254, 255] {
2111 assert!((ColorU::rgba(10, 20, 30, a).luminance()
2112 - ColorU::rgba(10, 20, 30, 255).luminance())
2113 .abs()
2114 < 1e-9);
2115 }
2116 }
2117
2118 #[test]
2119 fn relative_luminance_endpoints_and_range() {
2120 assert!((ColorU::BLACK.relative_luminance() - 0.0).abs() < 1e-6);
2121 assert!((ColorU::WHITE.relative_luminance() - 1.0).abs() < 1e-6);
2122 for i in 0..=255u16 {
2123 #[allow(clippy::cast_possible_truncation)]
2124 let l = ColorU::rgb(i as u8, i as u8, i as u8).relative_luminance();
2125 assert!(l.is_finite(), "non-finite relative_luminance at {i}");
2126 assert!((-1e-6..=1.000_001).contains(&l), "out of range at {i}: {l}");
2127 }
2128 }
2129
2130 #[test]
2131 fn relative_luminance_is_monotonic_along_the_gray_ramp() {
2132 let mut prev = f32::NEG_INFINITY;
2133 for i in 0..=255u16 {
2134 #[allow(clippy::cast_possible_truncation)]
2135 let l = ColorU::rgb(i as u8, i as u8, i as u8).relative_luminance();
2136 assert!(l >= prev, "gray ramp not monotonic at {i}");
2137 prev = l;
2138 }
2139 }
2140
2141 #[test]
2142 fn is_light_and_is_dark_are_exact_complements() {
2143 for i in 0..=255u16 {
2146 #[allow(clippy::cast_possible_truncation)]
2147 let c = ColorU::rgb(i as u8, i as u8, i as u8);
2148 assert_ne!(c.is_light(), c.is_dark(), "not complementary at {i}");
2149 }
2150 for c in SAMPLES {
2151 assert_ne!(c.is_light(), c.is_dark());
2152 }
2153 }
2154
2155 #[test]
2156 fn is_light_and_is_dark_known_values() {
2157 assert!(ColorU::WHITE.is_light());
2158 assert!(!ColorU::WHITE.is_dark());
2159 assert!(ColorU::BLACK.is_dark());
2160 assert!(!ColorU::BLACK.is_light());
2161 assert!(ColorU::default().is_dark());
2163 assert!(ColorU::rgb(128, 128, 128).is_dark());
2165 }
2166
2167 #[test]
2172 fn contrast_ratio_is_symmetric() {
2173 for a in SAMPLES {
2174 for b in SAMPLES {
2175 let ab = a.contrast_ratio(&b);
2176 let ba = b.contrast_ratio(&a);
2177 assert!((ab - ba).abs() < 1e-6, "asymmetric: {ab} vs {ba}");
2178 }
2179 }
2180 }
2181
2182 #[test]
2183 fn contrast_ratio_stays_within_1_and_21() {
2184 for a in SAMPLES {
2185 for b in SAMPLES {
2186 let r = a.contrast_ratio(&b);
2187 assert!(r.is_finite(), "non-finite contrast ratio");
2188 assert!((0.999..=21.001).contains(&r), "contrast ratio out of range: {r}");
2189 }
2190 assert!((a.contrast_ratio(&a) - 1.0).abs() < 1e-6);
2192 }
2193 let max = ColorU::BLACK.contrast_ratio(&ColorU::WHITE);
2195 assert!((max - 21.0).abs() < 0.01, "black/white contrast was {max}");
2196 }
2197
2198 #[test]
2199 fn meets_wcag_thresholds_agree_with_contrast_ratio() {
2200 for a in SAMPLES {
2201 for b in SAMPLES {
2202 let r = a.contrast_ratio(&b);
2203 assert_eq!(a.meets_wcag_aa(&b), r >= 4.5);
2204 assert_eq!(a.meets_wcag_aa_large(&b), r >= 3.0);
2205 assert_eq!(a.meets_wcag_aaa(&b), r >= 7.0);
2206 assert_eq!(a.meets_wcag_aaa_large(&b), r >= 4.5);
2207 }
2208 }
2209 }
2210
2211 #[test]
2212 fn meets_wcag_known_true_and_false() {
2213 assert!(ColorU::BLACK.meets_wcag_aa(&ColorU::WHITE));
2214 assert!(ColorU::BLACK.meets_wcag_aaa(&ColorU::WHITE));
2215 assert!(ColorU::WHITE.meets_wcag_aa_large(&ColorU::BLACK));
2216 assert!(!ColorU::RED.meets_wcag_aa(&ColorU::RED));
2218 assert!(!ColorU::RED.meets_wcag_aa_large(&ColorU::RED));
2219 assert!(!ColorU::WHITE.meets_wcag_aaa(&ColorU::WHITE));
2220 }
2221
2222 #[test]
2223 fn best_contrast_text_only_ever_returns_black_or_white() {
2224 for c in SAMPLES {
2225 let t = c.best_contrast_text();
2226 assert!(t == ColorU::WHITE || t == ColorU::BLACK, "got {t:?}");
2227 assert_eq!(c.contrast_text(), t);
2229 }
2230 for i in 0..=255u16 {
2231 #[allow(clippy::cast_possible_truncation)]
2232 let c = ColorU::rgb(i as u8, i as u8, i as u8);
2233 let t = c.best_contrast_text();
2234 assert!(t == ColorU::WHITE || t == ColorU::BLACK);
2235 }
2236 }
2237
2238 #[test]
2239 fn best_contrast_text_picks_the_higher_contrast_option() {
2240 assert_eq!(ColorU::WHITE.best_contrast_text(), ColorU::BLACK);
2241 assert_eq!(ColorU::BLACK.best_contrast_text(), ColorU::WHITE);
2242 for c in SAMPLES {
2243 let t = c.best_contrast_text();
2244 let other = if t == ColorU::WHITE { ColorU::BLACK } else { ColorU::WHITE };
2245 assert!(
2246 c.contrast_ratio(&t) >= c.contrast_ratio(&other),
2247 "{c:?} picked the lower-contrast text color"
2248 );
2249 }
2250 }
2251
2252 #[test]
2257 fn ensure_contrast_returns_self_when_already_compliant() {
2258 assert_eq!(
2260 ColorU::BLACK.ensure_contrast(&ColorU::WHITE, 4.5),
2261 ColorU::BLACK
2262 );
2263 let gray = ColorU::rgb(128, 128, 128);
2264 assert_eq!(gray.ensure_contrast(&ColorU::BLACK, 4.5), gray);
2266 }
2267
2268 #[test]
2269 fn ensure_contrast_actually_reaches_the_requested_ratio() {
2270 let gray = ColorU::rgb(128, 128, 128);
2271 let fixed = gray.ensure_contrast(&ColorU::WHITE, 4.5);
2272 assert!(
2273 fixed.contrast_ratio(&ColorU::WHITE) >= 4.5,
2274 "adjusted color {fixed:?} still fails 4.5:1"
2275 );
2276 assert!(fixed.r <= gray.r && fixed.g <= gray.g && fixed.b <= gray.b);
2278 }
2279
2280 #[test]
2281 fn ensure_contrast_degenerate_min_ratios_return_self() {
2282 let gray = ColorU::rgb(128, 128, 128);
2283 assert_eq!(gray.ensure_contrast(&ColorU::WHITE, 0.0), gray);
2285 assert_eq!(gray.ensure_contrast(&ColorU::WHITE, -1.0), gray);
2286 assert_eq!(gray.ensure_contrast(&ColorU::WHITE, f32::NEG_INFINITY), gray);
2287 assert_eq!(gray.ensure_contrast(&ColorU::WHITE, f32::INFINITY), gray);
2290 assert_eq!(gray.ensure_contrast(&ColorU::WHITE, f32::NAN), gray);
2291 assert_eq!(gray.ensure_contrast(&ColorU::WHITE, 1e30), gray);
2292 }
2293
2294 #[test]
2295 fn ensure_contrast_terminates_for_every_sample_pair() {
2296 for c in SAMPLES {
2297 for bg in SAMPLES {
2298 for min in [1.0, 3.0, 4.5, 7.0, 21.0, 25.0] {
2299 let out = c.ensure_contrast(&bg, min);
2300 assert_eq!(out.a, c.a);
2302 }
2303 }
2304 }
2305 }
2306
2307 #[test]
2312 fn apca_contrast_sign_encodes_polarity() {
2313 let dark_on_light = ColorU::BLACK.apca_contrast(&ColorU::WHITE);
2314 let light_on_dark = ColorU::WHITE.apca_contrast(&ColorU::BLACK);
2315 assert!(dark_on_light > 0.0, "black-on-white should be positive");
2316 assert!(light_on_dark < 0.0, "white-on-black should be negative");
2317 assert!(dark_on_light.is_finite() && light_on_dark.is_finite());
2318 }
2319
2320 #[test]
2321 fn apca_contrast_of_a_color_against_itself_is_zero() {
2322 for c in SAMPLES {
2323 assert_eq!(c.apca_contrast(&c), 0.0, "{c:?} vs itself");
2324 }
2325 }
2326
2327 #[test]
2328 fn apca_contrast_is_finite_for_every_sample_pair() {
2329 for a in SAMPLES {
2330 for b in SAMPLES {
2331 assert!(a.apca_contrast(&b).is_finite(), "{a:?} on {b:?}");
2332 }
2333 }
2334 }
2335
2336 #[test]
2337 fn meets_apca_thresholds_agree_with_apca_contrast() {
2338 for a in SAMPLES {
2339 for b in SAMPLES {
2340 let lc = libm::fabsf(a.apca_contrast(&b));
2341 assert_eq!(a.meets_apca_body(&b), lc >= 60.0);
2342 assert_eq!(a.meets_apca_large(&b), lc >= 45.0);
2343 }
2344 }
2345 assert!(ColorU::BLACK.meets_apca_body(&ColorU::WHITE));
2346 assert!(ColorU::BLACK.meets_apca_large(&ColorU::WHITE));
2347 assert!(!ColorU::RED.meets_apca_body(&ColorU::RED));
2348 assert!(!ColorU::RED.meets_apca_large(&ColorU::RED));
2349 }
2350
2351 #[test]
2357 fn hover_and_active_variants_preserve_alpha_and_never_panic() {
2358 for c in SAMPLES {
2359 assert_eq!(c.hover_variant().a, c.a);
2360 assert_eq!(c.active_variant().a, c.a);
2361 }
2362 assert!(ColorU::WHITE.hover_variant().r < 255);
2364 assert!(ColorU::BLACK.hover_variant().r > 0);
2365 assert!(ColorU::WHITE.active_variant().r < ColorU::WHITE.hover_variant().r);
2366 }
2367
2368 #[test]
2369 fn invert_is_its_own_inverse() {
2370 for c in SAMPLES {
2371 assert_eq!(c.invert().invert(), c);
2372 assert_eq!(c.invert().a, c.a, "invert must keep alpha");
2373 }
2374 assert_eq!(ColorU::BLACK.invert(), ColorU::WHITE);
2375 assert_eq!(ColorU::WHITE.invert(), ColorU::BLACK);
2376 }
2377
2378 #[test]
2379 fn invert_does_not_underflow_at_the_channel_bounds() {
2380 assert_eq!(ColorU::rgba(0, 0, 0, 0).invert(), ColorU::rgba(255, 255, 255, 0));
2383 assert_eq!(
2384 ColorU::rgba(255, 255, 255, 255).invert(),
2385 ColorU::rgba(0, 0, 0, 255)
2386 );
2387 }
2388
2389 #[test]
2390 fn to_grayscale_produces_equal_channels_and_keeps_alpha() {
2391 for c in SAMPLES {
2392 let g = c.to_grayscale();
2393 assert_eq!(g.r, g.g);
2394 assert_eq!(g.g, g.b);
2395 assert_eq!(g.a, c.a);
2396 }
2397 }
2398
2399 #[test]
2400 fn to_grayscale_boundary_values() {
2401 assert_eq!(ColorU::BLACK.to_grayscale(), ColorU::BLACK);
2402 assert_eq!(ColorU::WHITE.to_grayscale(), ColorU::WHITE);
2403 assert_eq!(
2404 ColorU::rgb(128, 128, 128).to_grayscale(),
2405 ColorU::rgb(128, 128, 128)
2406 );
2407 for i in 0..=255u16 {
2411 #[allow(clippy::cast_possible_truncation)]
2412 let c = ColorU::rgb(i as u8, i as u8, i as u8);
2413 let drift = i32::from(c.r) - i32::from(c.to_grayscale().r);
2414 assert!((0..=1).contains(&drift), "gray {i} drifted by {drift}");
2415 }
2416 }
2417
2418 #[test]
2419 fn has_alpha_is_true_for_everything_but_255() {
2420 assert!(!ColorU::rgba(0, 0, 0, 255).has_alpha());
2421 assert!(!ColorU::WHITE.has_alpha());
2422 assert!(ColorU::rgba(0, 0, 0, 254).has_alpha());
2423 assert!(ColorU::TRANSPARENT.has_alpha());
2424 for a in 0..=u8::MAX {
2425 assert_eq!(ColorU::rgba(1, 2, 3, a).has_alpha(), a != 255);
2426 }
2427 }
2428
2429 #[test]
2430 fn to_hash_is_always_nine_lowercase_chars() {
2431 assert_eq!(ColorU::RED.to_hash(), "#ff0000ff");
2432 assert_eq!(ColorU::TRANSPARENT.to_hash(), "#00000000");
2433 assert_eq!(ColorU::rgba(1, 2, 3, 4).to_hash(), "#01020304");
2434 assert_eq!(ColorU::WHITE.to_hash(), "#ffffffff");
2435 for c in SAMPLES {
2436 let h = c.to_hash();
2437 assert_eq!(h.len(), 9, "{h} is not 9 bytes");
2438 assert!(h.starts_with('#'));
2439 assert!(
2440 h[1..].chars().all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase()),
2441 "{h} is not lowercase hex"
2442 );
2443 }
2444 }
2445
2446 #[test]
2451 fn coloru_display_is_well_formed() {
2452 assert_eq!(format!("{}", ColorU::RED), "rgba(255, 0, 0, 1)");
2453 assert_eq!(format!("{}", ColorU::TRANSPARENT), "rgba(0, 0, 0, 0)");
2454 assert_eq!(format!("{}", ColorU::default()), "rgba(0, 0, 0, 1)");
2455 assert_eq!(format!("{}", ColorU::rgba(1, 2, 3, 128)), "rgba(1, 2, 3, 0.5019608)");
2457 for c in SAMPLES {
2458 let s = format!("{c}");
2459 assert!(s.starts_with("rgba(") && s.ends_with(')') && s.len() > 6);
2460 }
2461 }
2462
2463 #[test]
2464 fn colorf_display_survives_nan_and_inf() {
2465 assert_eq!(format!("{}", ColorF::BLACK), "rgba(0, 0, 0, 1)");
2466 assert_eq!(format!("{}", ColorF::WHITE), "rgba(255, 255, 255, 1)");
2467 assert_eq!(format!("{}", ColorF::TRANSPARENT), "rgba(0, 0, 0, 0)");
2468 assert_eq!(format!("{}", ColorF::default()), format!("{}", ColorF::BLACK));
2469
2470 let nan = ColorF { r: f32::NAN, g: f32::NAN, b: f32::NAN, a: f32::NAN };
2471 assert_eq!(format!("{nan}"), "rgba(NaN, NaN, NaN, NaN)");
2472
2473 let inf = ColorF {
2474 r: f32::INFINITY,
2475 g: f32::NEG_INFINITY,
2476 b: f32::MAX,
2477 a: f32::INFINITY,
2478 };
2479 let s = format!("{inf}");
2480 assert!(s.starts_with("rgba(inf, -inf, ") && s.ends_with(", inf)"), "{s}");
2481 }
2482
2483 #[test]
2488 fn coloru_to_colorf_and_back_is_lossless_for_all_256_channel_values() {
2489 for i in 0..=255u16 {
2490 #[allow(clippy::cast_possible_truncation)]
2491 let c = ColorU::rgba(i as u8, (255 - i) as u8, i as u8, (255 - i) as u8);
2492 let f: ColorF = c.into();
2493 let back: ColorU = f.into();
2494 assert_eq!(back, c, "round-trip lost information at {i}");
2495 }
2496 }
2497
2498 #[test]
2499 fn colorf_to_coloru_clamps_out_of_range_channels() {
2500 let over = ColorF { r: 2.0, g: 1e30, b: f32::INFINITY, a: 1.5 };
2502 assert_eq!(ColorU::from(over), ColorU::rgba(255, 255, 255, 255));
2503 let under = ColorF { r: -1.0, g: -1e30, b: f32::NEG_INFINITY, a: -0.5 };
2505 assert_eq!(ColorU::from(under), ColorU::rgba(0, 0, 0, 0));
2506 }
2507
2508 #[test]
2509 fn colorf_to_coloru_maps_nan_channels_to_255() {
2510 let nan = ColorF { r: f32::NAN, g: 0.0, b: 0.0, a: f32::NAN };
2513 assert_eq!(ColorU::from(nan), ColorU::rgba(255, 0, 0, 255));
2514 }
2515
2516 #[cfg(feature = "parser")]
2517 #[test]
2518 fn to_hash_round_trips_through_the_parser() {
2519 assert_eq!(parse_css_color(&ColorU::RED.to_hash()).unwrap(), ColorU::RED);
2520 for r in (0..=255u16).step_by(51) {
2521 for g in (0..=255u16).step_by(51) {
2522 for b in (0..=255u16).step_by(85) {
2523 for a in (0..=255u16).step_by(85) {
2524 #[allow(clippy::cast_possible_truncation)]
2525 let c = ColorU::rgba(r as u8, g as u8, b as u8, a as u8);
2526 let encoded = c.to_hash();
2527 let decoded = parse_css_color(&encoded)
2528 .unwrap_or_else(|e| panic!("{encoded} failed to parse: {e}"));
2529 assert_eq!(decoded, c, "{encoded} decoded to the wrong color");
2530 }
2531 }
2532 }
2533 }
2534 }
2535
2536 #[cfg(feature = "parser")]
2537 #[test]
2538 fn coloru_display_round_trips_through_the_parser() {
2539 for a in 0..=255u16 {
2541 #[allow(clippy::cast_possible_truncation)]
2542 let c = ColorU::rgba(13, 110, 253, a as u8);
2543 let encoded = format!("{c}");
2544 let decoded = parse_css_color(&encoded)
2545 .unwrap_or_else(|e| panic!("{encoded} failed to parse: {e}"));
2546 assert_eq!(decoded, c, "{encoded} decoded to the wrong color");
2547 }
2548 for c in SAMPLES {
2549 assert_eq!(parse_css_color(&format!("{c}")).unwrap(), c);
2550 }
2551 }
2552
2553 #[cfg(feature = "parser")]
2554 #[test]
2555 fn system_color_ref_css_str_round_trips_for_every_variant() {
2556 let all = [
2557 SystemColorRef::Text,
2558 SystemColorRef::Background,
2559 SystemColorRef::Accent,
2560 SystemColorRef::AccentText,
2561 SystemColorRef::ButtonFace,
2562 SystemColorRef::ButtonText,
2563 SystemColorRef::WindowBackground,
2564 SystemColorRef::SelectionBackground,
2565 SystemColorRef::SelectionText,
2566 ];
2567 for variant in all {
2568 let encoded = variant.as_css_str();
2569 assert!(encoded.starts_with("system:"), "{encoded}");
2570 assert_eq!(
2571 parse_color_or_system(encoded).unwrap(),
2572 ColorOrSystem::System(variant),
2573 "{encoded} did not round-trip"
2574 );
2575 }
2576 }
2577
2578 #[test]
2583 fn color_or_system_constructors_and_fallbacks() {
2584 let c = ColorOrSystem::color(ColorU::RED);
2585 assert_eq!(c, ColorOrSystem::Color(ColorU::RED));
2586 assert_eq!(c.to_color_u_with_fallback(ColorU::BLUE), ColorU::RED);
2587 assert_eq!(c.to_color_u_default(), ColorU::RED);
2588
2589 let s = ColorOrSystem::system(SystemColorRef::Accent);
2590 assert_eq!(s, ColorOrSystem::System(SystemColorRef::Accent));
2591 assert_eq!(s.to_color_u_with_fallback(ColorU::BLUE), ColorU::BLUE);
2593 assert_eq!(s.to_color_u_default(), ColorU::rgba(128, 128, 128, 255));
2594
2595 assert_eq!(ColorOrSystem::default(), ColorOrSystem::Color(ColorU::BLACK));
2597 assert_eq!(ColorOrSystem::from(ColorU::RED), ColorOrSystem::color(ColorU::RED));
2598 }
2599
2600 #[test]
2601 fn system_color_ref_resolve_falls_back_when_unset() {
2602 use crate::system::SystemColors;
2603
2604 let empty = SystemColors::default();
2605 let all = [
2606 SystemColorRef::Text,
2607 SystemColorRef::Background,
2608 SystemColorRef::Accent,
2609 SystemColorRef::AccentText,
2610 SystemColorRef::ButtonFace,
2611 SystemColorRef::ButtonText,
2612 SystemColorRef::WindowBackground,
2613 SystemColorRef::SelectionBackground,
2614 SystemColorRef::SelectionText,
2615 ];
2616 for variant in all {
2617 assert_eq!(variant.resolve(&empty, ColorU::RED), ColorU::RED, "{variant:?}");
2618 assert_eq!(
2619 ColorOrSystem::System(variant).resolve(&empty, ColorU::RED),
2620 ColorU::RED
2621 );
2622 }
2623 assert_eq!(
2625 ColorOrSystem::Color(ColorU::BLUE).resolve(&empty, ColorU::RED),
2626 ColorU::BLUE
2627 );
2628 }
2629
2630 #[test]
2635 fn palette_shades_are_total_over_usize_and_always_opaque() {
2636 type Palette = fn(usize) -> ColorU;
2637 const PALETTES: [Palette; 12] = [
2638 ColorU::strawberry,
2639 ColorU::palette_orange,
2640 ColorU::banana,
2641 ColorU::palette_lime,
2642 ColorU::mint,
2643 ColorU::blueberry,
2644 ColorU::grape,
2645 ColorU::bubblegum,
2646 ColorU::cocoa,
2647 ColorU::palette_silver,
2648 ColorU::slate,
2649 ColorU::dark,
2650 ];
2651 for p in PALETTES {
2652 for shade in [0, 1, 100, 200, 201, 300, 400, 401, 500, 600, 601, 700, 800, 801, 900, 1000, usize::MAX] {
2653 assert_eq!(p(shade).a, 255, "shade {shade} was not opaque");
2654 }
2655 assert_eq!(p(usize::MAX), p(900));
2657 assert_eq!(p(801), p(900));
2658 assert_eq!(p(0), p(200));
2660 assert_ne!(p(200), p(201));
2661 assert_ne!(p(400), p(401));
2662 assert_ne!(p(600), p(601));
2663 assert_ne!(p(800), p(801));
2664 }
2665 }
2666
2667 #[test]
2668 fn palette_known_values() {
2669 assert_eq!(ColorU::strawberry(100), ColorU::rgb(0xff, 0x8c, 0x82));
2670 assert_eq!(ColorU::strawberry(900), ColorU::rgb(0x7a, 0x00, 0x00));
2671 assert_eq!(ColorU::dark(900), ColorU::BLACK);
2672 assert_eq!(ColorU::dark(usize::MAX), ColorU::BLACK);
2673 }
2674
2675 #[test]
2680 fn named_constructors_match_their_constants() {
2681 assert_eq!(ColorU::red(), ColorU::RED);
2682 assert_eq!(ColorU::green(), ColorU::GREEN);
2683 assert_eq!(ColorU::blue(), ColorU::BLUE);
2684 assert_eq!(ColorU::white(), ColorU::WHITE);
2685 assert_eq!(ColorU::black(), ColorU::BLACK);
2686 assert_eq!(ColorU::transparent(), ColorU::TRANSPARENT);
2687 assert_eq!(ColorU::yellow(), ColorU::YELLOW);
2688 assert_eq!(ColorU::cyan(), ColorU::CYAN);
2689 assert_eq!(ColorU::magenta(), ColorU::MAGENTA);
2690 assert_eq!(ColorU::orange(), ColorU::ORANGE);
2691 assert_eq!(ColorU::pink(), ColorU::PINK);
2692 assert_eq!(ColorU::purple(), ColorU::PURPLE);
2693 assert_eq!(ColorU::brown(), ColorU::BROWN);
2694 assert_eq!(ColorU::gray(), ColorU::GRAY);
2695 assert_eq!(ColorU::light_gray(), ColorU::LIGHT_GRAY);
2696 assert_eq!(ColorU::dark_gray(), ColorU::DARK_GRAY);
2697 assert_eq!(ColorU::navy(), ColorU::NAVY);
2698 assert_eq!(ColorU::teal(), ColorU::TEAL);
2699 assert_eq!(ColorU::olive(), ColorU::OLIVE);
2700 assert_eq!(ColorU::maroon(), ColorU::MAROON);
2701 assert_eq!(ColorU::lime(), ColorU::LIME);
2702 assert_eq!(ColorU::aqua(), ColorU::AQUA);
2703 assert_eq!(ColorU::silver(), ColorU::SILVER);
2704 assert_eq!(ColorU::fuchsia(), ColorU::FUCHSIA);
2705 assert_eq!(ColorU::indigo(), ColorU::INDIGO);
2706 assert_eq!(ColorU::gold(), ColorU::GOLD);
2707 assert_eq!(ColorU::coral(), ColorU::CORAL);
2708 assert_eq!(ColorU::salmon(), ColorU::SALMON);
2709 assert_eq!(ColorU::turquoise(), ColorU::TURQUOISE);
2710 assert_eq!(ColorU::violet(), ColorU::VIOLET);
2711 assert_eq!(ColorU::crimson(), ColorU::CRIMSON);
2712 assert_eq!(ColorU::chocolate(), ColorU::CHOCOLATE);
2713 assert_eq!(ColorU::sky_blue(), ColorU::SKY_BLUE);
2714 assert_eq!(ColorU::forest_green(), ColorU::FOREST_GREEN);
2715 assert_eq!(ColorU::sea_green(), ColorU::SEA_GREEN);
2716 assert_eq!(ColorU::slate_gray(), ColorU::SLATE_GRAY);
2717 assert_eq!(ColorU::midnight_blue(), ColorU::MIDNIGHT_BLUE);
2718 assert_eq!(ColorU::dark_red(), ColorU::DARK_RED);
2719 assert_eq!(ColorU::dark_green(), ColorU::DARK_GREEN);
2720 assert_eq!(ColorU::dark_blue(), ColorU::DARK_BLUE);
2721 assert_eq!(ColorU::light_blue(), ColorU::LIGHT_BLUE);
2722 assert_eq!(ColorU::light_green(), ColorU::LIGHT_GREEN);
2723 assert_eq!(ColorU::light_yellow(), ColorU::LIGHT_YELLOW);
2724 assert_eq!(ColorU::light_pink(), ColorU::LIGHT_PINK);
2725 }
2726
2727 #[test]
2728 fn every_named_constructor_except_transparent_is_opaque() {
2729 type Ctor = fn() -> ColorU;
2730 const CTORS: [Ctor; 43] = [
2731 ColorU::red, ColorU::green, ColorU::blue, ColorU::white, ColorU::black,
2732 ColorU::yellow, ColorU::cyan, ColorU::magenta, ColorU::orange, ColorU::pink,
2733 ColorU::purple, ColorU::brown, ColorU::gray, ColorU::light_gray, ColorU::dark_gray,
2734 ColorU::navy, ColorU::teal, ColorU::olive, ColorU::maroon, ColorU::lime,
2735 ColorU::aqua, ColorU::silver, ColorU::fuchsia, ColorU::indigo, ColorU::gold,
2736 ColorU::coral, ColorU::salmon, ColorU::turquoise, ColorU::violet, ColorU::crimson,
2737 ColorU::chocolate, ColorU::sky_blue, ColorU::forest_green, ColorU::sea_green,
2738 ColorU::slate_gray, ColorU::midnight_blue, ColorU::dark_red, ColorU::dark_green,
2739 ColorU::dark_blue, ColorU::light_blue, ColorU::light_green, ColorU::light_yellow,
2740 ColorU::light_pink,
2741 ];
2742 for ctor in CTORS {
2743 let c = ctor();
2744 assert_eq!(c.a, ColorU::ALPHA_OPAQUE);
2745 assert!(!c.has_alpha());
2746 }
2747 assert_eq!(ColorU::transparent().a, ColorU::ALPHA_TRANSPARENT);
2749 assert!(ColorU::transparent().has_alpha());
2750 }
2751
2752 #[test]
2753 fn apple_and_bootstrap_palettes_are_opaque_and_distinct() {
2754 type Ctor = fn() -> ColorU;
2755 const APPLE: [Ctor; 26] = [
2756 ColorU::apple_red, ColorU::apple_red_dark,
2757 ColorU::apple_orange, ColorU::apple_orange_dark,
2758 ColorU::apple_yellow, ColorU::apple_yellow_dark,
2759 ColorU::apple_green, ColorU::apple_green_dark,
2760 ColorU::apple_mint, ColorU::apple_mint_dark,
2761 ColorU::apple_teal, ColorU::apple_teal_dark,
2762 ColorU::apple_cyan, ColorU::apple_cyan_dark,
2763 ColorU::apple_blue, ColorU::apple_blue_dark,
2764 ColorU::apple_indigo, ColorU::apple_indigo_dark,
2765 ColorU::apple_purple, ColorU::apple_purple_dark,
2766 ColorU::apple_pink, ColorU::apple_pink_dark,
2767 ColorU::apple_brown, ColorU::apple_brown_dark,
2768 ColorU::apple_gray, ColorU::apple_gray_dark,
2769 ];
2770 const BOOTSTRAP: [Ctor; 23] = [
2771 ColorU::bootstrap_primary, ColorU::bootstrap_primary_hover, ColorU::bootstrap_primary_active,
2772 ColorU::bootstrap_secondary, ColorU::bootstrap_secondary_hover, ColorU::bootstrap_secondary_active,
2773 ColorU::bootstrap_success, ColorU::bootstrap_success_hover, ColorU::bootstrap_success_active,
2774 ColorU::bootstrap_danger, ColorU::bootstrap_danger_hover, ColorU::bootstrap_danger_active,
2775 ColorU::bootstrap_warning, ColorU::bootstrap_warning_hover, ColorU::bootstrap_warning_active,
2776 ColorU::bootstrap_info, ColorU::bootstrap_info_hover, ColorU::bootstrap_info_active,
2777 ColorU::bootstrap_light, ColorU::bootstrap_light_hover, ColorU::bootstrap_light_active,
2778 ColorU::bootstrap_dark, ColorU::bootstrap_dark_hover,
2779 ];
2780 for ctor in APPLE.iter().chain(BOOTSTRAP.iter()) {
2781 assert_eq!(ctor().a, 255);
2782 }
2783 for pair in APPLE.chunks_exact(2) {
2785 assert_ne!(pair[0](), pair[1](), "an apple light/dark pair is identical");
2786 }
2787 assert_eq!(ColorU::bootstrap_link(), ColorU::bootstrap_primary());
2789 assert_ne!(ColorU::bootstrap_link_hover(), ColorU::bootstrap_link());
2790 assert_ne!(ColorU::bootstrap_dark_active(), ColorU::bootstrap_dark());
2791 }
2792
2793 #[cfg(feature = "parser")]
2798 #[test]
2799 fn parse_css_color_valid_minimal_positive_controls() {
2800 assert_eq!(parse_css_color("red").unwrap(), ColorU::RED);
2801 assert_eq!(parse_css_color("#f00").unwrap(), ColorU::RED);
2802 assert_eq!(parse_css_color("#ff0000").unwrap(), ColorU::RED);
2803 assert_eq!(parse_css_color("rgb(255,0,0)").unwrap(), ColorU::RED);
2804 assert_eq!(parse_css_color("hsl(0,100%,50%)").unwrap(), ColorU::RED);
2805 }
2806
2807 #[cfg(feature = "parser")]
2808 #[test]
2809 fn parse_css_color_empty_and_whitespace_only_are_errors() {
2810 assert!(parse_css_color("").is_err());
2811 assert!(parse_css_color(" ").is_err());
2812 assert!(parse_css_color("\t\n\r ").is_err());
2813 assert!(parse_css_color("#").is_err());
2814 assert_eq!(parse_css_color(""), Err(CssColorParseError::EmptyInput));
2815 assert_eq!(parse_css_color(" \t "), Err(CssColorParseError::EmptyInput));
2816 }
2817
2818 #[cfg(feature = "parser")]
2819 #[test]
2820 fn parse_css_color_garbage_is_rejected_without_panicking() {
2821 for garbage in [
2822 "!@#$%^&*()", "\0\0\0", "rgb", "rgb(", "rgb)", ")(", "()", "#-1", "#+1",
2823 "notacolor", "0", "-0", "1e10", "NaN", "inf", "-inf", ";", ",,,", "\\",
2824 "rgb(,,)", "hsl(,,)", "rgba(,,,)", "#\u{0}\u{0}\u{0}",
2825 ] {
2826 assert!(
2827 parse_css_color(garbage).is_err(),
2828 "{garbage:?} was unexpectedly accepted"
2829 );
2830 }
2831 }
2832
2833 #[cfg(feature = "parser")]
2834 #[test]
2835 fn parse_css_color_extremely_long_input_does_not_hang_or_panic() {
2836 let long_hex = format!("#{}", "f".repeat(1_000_000));
2838 assert!(parse_css_color(&long_hex).is_err());
2839 let long_name = "a".repeat(100_000);
2841 assert!(parse_css_color(&long_name).is_err());
2842 let long_rgb = format!("rgb({})", "1,".repeat(50_000));
2844 assert!(parse_css_color(&long_rgb).is_err());
2845 }
2846
2847 #[cfg(feature = "parser")]
2848 #[test]
2849 fn parse_css_color_deeply_nested_input_does_not_stack_overflow() {
2850 let nested_parens = "(".repeat(10_000);
2852 assert!(parse_css_color(&nested_parens).is_err());
2853 let unclosed = "rgb(".repeat(10_000);
2854 assert!(parse_css_color(&unclosed).is_err());
2855 let balanced = format!("{}{}", "rgb(".repeat(5_000), ")".repeat(5_000));
2856 assert!(parse_css_color(&balanced).is_err());
2857 }
2858
2859 #[cfg(feature = "parser")]
2860 #[test]
2861 fn parse_css_color_unicode_input_does_not_panic() {
2862 for input in [
2865 "\u{1F600}", "#\u{1F600}", "#\u{e9}1", "#\u{e9}\u{e9}\u{e9}", "r\u{e9}d",
2870 "\u{0301}\u{0301}", "\u{4e2d}\u{6587}", "rgb(\u{1F600},0,0)",
2873 "rgba(0,0,0,\u{1F600})",
2874 "hsl(\u{1F600},100%,50%)",
2875 ] {
2876 assert!(
2877 parse_css_color(input).is_err(),
2878 "{input:?} was unexpectedly accepted"
2879 );
2880 }
2881 }
2882
2883 #[cfg(feature = "parser")]
2884 #[test]
2885 fn parse_css_color_boundary_numbers() {
2886 assert_eq!(parse_css_color("rgb(0,0,0)").unwrap(), ColorU::BLACK);
2888 assert_eq!(parse_css_color("rgb(255,255,255)").unwrap(), ColorU::WHITE);
2889 assert!(parse_css_color("rgb(256,0,0)").is_err());
2890 assert!(parse_css_color("rgb(-1,0,0)").is_err());
2891 assert!(parse_css_color("rgb(9223372036854775807,0,0)").is_err());
2892 assert!(parse_css_color("rgb(340282350000000000000000000000000000000,0,0)").is_err());
2893 assert_eq!(parse_css_color("rgba(0,0,0,0)").unwrap().a, 0);
2895 assert_eq!(parse_css_color("rgba(0,0,0,1)").unwrap().a, 255);
2896 assert_eq!(parse_css_color("rgba(0,0,0,1.0)").unwrap().a, 255);
2897 assert_eq!(parse_css_color("rgba(0,0,0,-0)").unwrap().a, 0);
2898 assert!(parse_css_color("rgba(0,0,0,1.0001)").is_err());
2899 assert!(parse_css_color("rgba(0,0,0,-0.0001)").is_err());
2900 assert!(parse_css_color("rgba(0,0,0,2)").is_err());
2901 assert!(parse_css_color("rgba(0,0,0,NaN)").is_err());
2903 assert!(parse_css_color("rgba(0,0,0,nan)").is_err());
2904 assert!(parse_css_color("rgba(0,0,0,inf)").is_err());
2905 assert!(parse_css_color("rgba(0,0,0,-inf)").is_err());
2906 assert!(parse_css_color("rgba(0,0,0,infinity)").is_err());
2907 assert_eq!(parse_css_color("rgba(0,0,0,1e-45)").unwrap().a, 0);
2909 }
2910
2911 #[cfg(feature = "parser")]
2912 #[test]
2913 fn parse_css_color_alpha_rounds_to_nearest() {
2914 assert_eq!(parse_css_color("rgba(0,0,0,0.5)").unwrap().a, 128);
2916 assert_eq!(parse_css_color("rgba(0,0,0,0.0)").unwrap().a, 0);
2917 assert_eq!(parse_css_color("rgba(0,0,0,0.999)").unwrap().a, 255);
2918 }
2919
2920 #[cfg(feature = "parser")]
2921 #[test]
2922 fn parse_css_color_arity_errors() {
2923 assert!(parse_css_color("rgb(255,0)").is_err()); 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());
2933 }
2934
2935 #[cfg(feature = "parser")]
2936 #[test]
2937 fn parse_css_color_leading_and_trailing_whitespace_is_trimmed() {
2938 assert_eq!(parse_css_color(" red ").unwrap(), ColorU::RED);
2939 assert_eq!(parse_css_color("\t#f00\n").unwrap(), ColorU::RED);
2940 assert_eq!(parse_css_color(" rgb( 255 , 0 , 0 ) ").unwrap(), ColorU::RED);
2941 assert!(parse_css_color("red;garbage").is_err());
2943 assert!(parse_css_color("red red").is_err());
2944 assert!(parse_css_color("#f00;").is_err());
2945 }
2946
2947 #[cfg(feature = "parser")]
2948 #[test]
2949 fn parse_css_color_accepts_trailing_junk_after_a_function_call() {
2950 assert_eq!(parse_css_color("rgb(1,2,3)garbage").unwrap(), ColorU::rgb(1, 2, 3));
2954 assert_eq!(parse_css_color("rgb(1,2,3);").unwrap(), ColorU::rgb(1, 2, 3));
2955 }
2956
2957 #[cfg(feature = "parser")]
2958 #[test]
2959 fn parse_css_color_hex_is_case_insensitive_and_length_checked() {
2960 assert_eq!(parse_css_color("#ABCDEF").unwrap(), parse_css_color("#abcdef").unwrap());
2961 assert_eq!(parse_css_color("#FFF").unwrap(), ColorU::WHITE);
2962 assert_eq!(parse_css_color("#f00f").unwrap(), ColorU::rgba(255, 0, 0, 255));
2964 assert_eq!(parse_css_color("#0008").unwrap(), ColorU::rgba(0, 0, 0, 136));
2965 for bad_len in ["#", "#f", "#ff", "#fffff", "#fffffff", "#fffffffff"] {
2967 assert!(parse_css_color(bad_len).is_err(), "{bad_len} accepted");
2968 }
2969 assert!(parse_css_color("#ggg").is_err());
2971 assert!(parse_css_color("#gggggg").is_err());
2972 assert!(parse_css_color("#-12345").is_err());
2973 assert!(parse_css_color("#+f0000").is_err());
2974 assert!(parse_css_color("#ff ff").is_err());
2975 }
2976
2977 #[cfg(feature = "parser")]
2978 #[test]
2979 fn parse_css_color_builtin_names_are_case_insensitive() {
2980 assert_eq!(parse_css_color("RED").unwrap(), ColorU::RED);
2981 assert_eq!(parse_css_color("ReD").unwrap(), ColorU::RED);
2982 assert_eq!(parse_css_color("TRANSPARENT").unwrap(), ColorU::TRANSPARENT);
2983 assert_eq!(parse_css_color("transparent").unwrap().a, 0);
2984 for near_miss in ["redd", "re", "r ed", "red1", "gray2", "greyish", "blackk"] {
2986 assert!(parse_css_color(near_miss).is_err(), "{near_miss} accepted");
2987 }
2988 assert_eq!(parse_css_color(" grey ").unwrap(), ColorU::GRAY);
2990 }
2991
2992 #[cfg(feature = "parser")]
2993 #[test]
2994 fn parse_css_color_hsl_boundaries_and_hue_wraparound() {
2995 assert_eq!(parse_css_color("hsl(0,100%,50%)").unwrap(), ColorU::RED);
2996 assert_eq!(parse_css_color("hsl(120,100%,50%)").unwrap(), ColorU::GREEN);
2997 assert_eq!(parse_css_color("hsl(240,100%,50%)").unwrap(), ColorU::BLUE);
2998 assert_eq!(parse_css_color("hsl(720,100%,50%)").unwrap(), ColorU::RED);
3000 assert_eq!(parse_css_color("hsl(0,0%,0%)").unwrap(), ColorU::BLACK);
3002 assert_eq!(parse_css_color("hsl(0,0%,100%)").unwrap(), ColorU::WHITE);
3003 for hue in ["1000000", "99999999", "-360"] {
3005 let s = format!("hsl({hue},100%,50%)");
3006 let _ = parse_css_color(&s).map(|c| assert_eq!(c.a, 255));
3007 }
3008 }
3009
3010 #[cfg(feature = "parser")]
3011 #[test]
3012 fn parse_css_color_unitless_hsl_components_are_scaled_wrong() {
3013 assert_eq!(parse_css_color("hsl(0,100,50)").unwrap(), ColorU::rgb(0, 255, 255));
3019 assert_eq!(parse_css_color("hsl(0,1,0.5)").unwrap(), ColorU::RED);
3021 assert_eq!(parse_css_color("hsl(0,100,50%)").unwrap(), ColorU::RED);
3024 }
3025
3026 #[cfg(feature = "parser")]
3031 #[test]
3032 fn parse_color_no_hash_only_accepts_3_4_6_and_8_bytes() {
3033 assert_eq!(parse_color_no_hash("fff").unwrap(), ColorU::WHITE);
3034 assert_eq!(parse_color_no_hash("000f").unwrap(), ColorU::BLACK);
3035 assert_eq!(parse_color_no_hash("ff0000").unwrap(), ColorU::RED);
3036 assert_eq!(parse_color_no_hash("ff000080").unwrap(), ColorU::rgba(255, 0, 0, 128));
3037 for bad in ["", "f", "ff", "fffff", "fffffff", "fffffffff", " ", "zzz"] {
3038 assert!(parse_color_no_hash(bad).is_err(), "{bad:?} accepted");
3039 }
3040 assert!(parse_color_no_hash("\u{e9}1").is_err());
3043 assert!(parse_color_no_hash("\u{1F600}").is_err());
3044 }
3045
3046 #[cfg(feature = "parser")]
3047 #[test]
3048 fn parse_color_rgb_alpha_flag_controls_arity() {
3049 assert_eq!(parse_color_rgb("1,2,3", false).unwrap(), ColorU::rgb(1, 2, 3));
3050 assert_eq!(parse_color_rgb("1,2,3,1", true).unwrap(), ColorU::rgba(1, 2, 3, 255));
3051 assert!(parse_color_rgb("1,2,3", true).is_err());
3053 assert!(parse_color_rgb("1,2,3,1", false).is_err());
3055 assert!(parse_color_rgb("", false).is_err());
3057 assert!(parse_color_rgb(" ", false).is_err());
3058 assert!(parse_color_rgb(",,", false).is_err());
3059 assert!(parse_color_rgb("1,,3", false).is_err());
3060 }
3061
3062 #[cfg(feature = "parser")]
3063 #[test]
3064 fn parse_color_rgb_components_boundaries() {
3065 let mut ok = ["0", "128", "255"].into_iter();
3066 assert_eq!(
3067 parse_color_rgb_components(&mut ok).unwrap(),
3068 ColorU::rgb(0, 128, 255)
3069 );
3070 let mut empty = core::iter::empty::<&str>();
3072 assert!(parse_color_rgb_components(&mut empty).is_err());
3073 let mut short = ["1", "2"].into_iter();
3075 assert!(parse_color_rgb_components(&mut short).is_err());
3076 for bad in [
3078 ["256", "0", "0"],
3079 ["-1", "0", "0"],
3080 ["0", "0", "1e3"],
3081 ["0.5", "0", "0"],
3082 ["abc", "0", "0"],
3083 ["", "0", "0"],
3084 ["+0", "0", "999999999999999999999"],
3085 ] {
3086 let mut it = bad.into_iter();
3087 assert!(parse_color_rgb_components(&mut it).is_err(), "{bad:?} accepted");
3088 }
3089 let mut extra = ["1", "2", "3", "4", "5"].into_iter();
3091 assert_eq!(
3092 parse_color_rgb_components(&mut extra).unwrap(),
3093 ColorU::rgb(1, 2, 3)
3094 );
3095 assert_eq!(extra.next(), Some("4"));
3096 }
3097
3098 #[cfg(feature = "parser")]
3099 #[test]
3100 fn parse_color_hsl_components_boundaries() {
3101 let mut red = ["0", "100%", "50%"].into_iter();
3102 assert_eq!(parse_color_hsl_components(&mut red).unwrap(), ColorU::RED);
3103 let mut fractions = ["0", "1", "0.5"].into_iter();
3108 assert_eq!(parse_color_hsl_components(&mut fractions).unwrap(), ColorU::RED);
3109 let mut unitless = ["0", "100", "50"].into_iter();
3110 assert_eq!(
3111 parse_color_hsl_components(&mut unitless).unwrap(),
3112 ColorU::rgb(0, 255, 255),
3113 "unitless hsl(0 100 50) should be red, not cyan"
3114 );
3115 let mut empty = core::iter::empty::<&str>();
3117 assert!(parse_color_hsl_components(&mut empty).is_err());
3118 let mut short = ["0", "100%"].into_iter();
3119 assert!(parse_color_hsl_components(&mut short).is_err());
3120 for bad in [
3121 ["", "100%", "50%"],
3122 ["notanangle", "100%", "50%"],
3123 ["to left", "100%", "50%"], ["0", "", "50%"],
3125 ["0", "100%", ""],
3126 ] {
3127 let mut it = bad.into_iter();
3128 assert!(parse_color_hsl_components(&mut it).is_err(), "{bad:?} accepted");
3129 }
3130 }
3131
3132 #[cfg(feature = "parser")]
3133 #[test]
3134 fn parse_alpha_component_range_and_rounding() {
3135 let cases: [(&str, u8); 5] = [("0", 0), ("0.0", 0), ("0.5", 128), ("1", 255), ("1.0", 255)];
3136 for (input, expected) in cases {
3137 let mut it = [input].into_iter();
3138 assert_eq!(
3139 parse_alpha_component(&mut it).unwrap(),
3140 expected,
3141 "alpha {input}"
3142 );
3143 }
3144 for bad in ["", " ", "-0.0001", "1.0001", "2", "-1", "NaN", "inf", "-inf", "abc", "0,5", "50%"] {
3146 let mut it = [bad].into_iter();
3147 assert!(parse_alpha_component(&mut it).is_err(), "{bad:?} accepted");
3148 }
3149 let mut empty = core::iter::empty::<&str>();
3151 assert!(parse_alpha_component(&mut empty).is_err());
3152 }
3153
3154 #[cfg(feature = "parser")]
3155 #[test]
3156 fn parse_color_builtin_rejects_junk_without_panicking() {
3157 assert_eq!(parse_color_builtin("red").unwrap(), ColorU::RED);
3158 assert_eq!(parse_color_builtin("REBECCAPURPLE").unwrap(), ColorU::rgb(102, 51, 153));
3159 assert_eq!(parse_color_builtin("transparent").unwrap(), ColorU::TRANSPARENT);
3160 assert!(parse_color_builtin(" red").is_err());
3162 assert!(parse_color_builtin("").is_err());
3163 assert!(parse_color_builtin("\u{130}").is_err());
3165 assert!(parse_color_builtin("\u{1F600}").is_err());
3166 assert!(parse_color_builtin(&"z".repeat(100_000)).is_err());
3167 }
3168
3169 #[cfg(feature = "parser")]
3174 #[test]
3175 fn parse_color_or_system_rejects_bad_system_names() {
3176 for bad in [
3177 "system:",
3178 "system:invalid",
3179 "system: ",
3180 "system::text",
3181 "system:text-",
3182 "system:TEXT", "SYSTEM:text", "system:text;junk",
3185 "system:\u{1F600}",
3186 ] {
3187 assert!(parse_color_or_system(bad).is_err(), "{bad:?} accepted");
3188 }
3189 assert!(parse_color_or_system("").is_err());
3191 assert!(parse_color_or_system(" ").is_err());
3192 }
3193
3194 #[cfg(feature = "parser")]
3195 #[test]
3196 fn parse_color_or_system_trims_and_falls_through_to_colors() {
3197 assert_eq!(
3198 parse_color_or_system(" system:accent ").unwrap(),
3199 ColorOrSystem::System(SystemColorRef::Accent)
3200 );
3201 assert_eq!(
3203 parse_color_or_system("system: accent ").unwrap(),
3204 ColorOrSystem::System(SystemColorRef::Accent)
3205 );
3206 assert_eq!(
3208 parse_color_or_system(" #f00 ").unwrap(),
3209 ColorOrSystem::Color(ColorU::RED)
3210 );
3211 assert_eq!(
3212 parse_color_or_system("rgba(0,0,0,0)").unwrap(),
3213 ColorOrSystem::Color(ColorU::TRANSPARENT)
3214 );
3215 assert!(parse_color_or_system("definitely-not-a-color").is_err());
3216 }
3217
3218 #[cfg(feature = "parser")]
3219 #[test]
3220 fn parse_color_or_system_long_and_nested_input_does_not_hang() {
3221 let long = format!("system:{}", "a".repeat(100_000));
3222 assert!(parse_color_or_system(&long).is_err());
3223 let nested = "rgb(".repeat(10_000);
3224 assert!(parse_color_or_system(&nested).is_err());
3225 }
3226
3227 #[cfg(feature = "parser")]
3232 #[test]
3233 fn css_color_parse_error_round_trips_through_owned() {
3234 let errors = [
3236 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(), ];
3249 for e in &errors {
3250 let owned = e.to_contained();
3251 let shared = owned.to_shared();
3252 assert_eq!(shared.to_contained(), owned, "error did not round-trip: {e}");
3254 assert!(!format!("{e}").is_empty());
3256 assert!(!format!("{e:?}").is_empty());
3257 assert!(!format!("{owned:?}").is_empty());
3258 }
3259 }
3260
3261 #[cfg(feature = "parser")]
3262 #[test]
3263 fn css_color_parse_error_carries_the_offending_input() {
3264 assert_eq!(
3265 parse_css_color("notacolor"),
3266 Err(CssColorParseError::InvalidColor("notacolor"))
3267 );
3268 assert_eq!(
3269 parse_css_color("rgb(1,2,3,4)"),
3270 Err(CssColorParseError::ExtraArguments("4"))
3271 );
3272 assert_eq!(
3273 parse_css_color("rgb(1,2)"),
3274 Err(CssColorParseError::MissingColorComponent(
3275 CssColorComponent::Blue
3276 ))
3277 );
3278 assert_eq!(
3279 parse_css_color("rgba(1,2,3)"),
3280 Err(CssColorParseError::MissingColorComponent(
3281 CssColorComponent::Alpha
3282 ))
3283 );
3284 assert_eq!(
3285 parse_css_color("rgba(0,0,0,2)"),
3286 Err(CssColorParseError::FloatValueOutOfRange(2.0))
3287 );
3288 assert_eq!(
3290 parse_css_color("#zzz"),
3291 Err(CssColorParseError::InvalidColorComponent(b'z'))
3292 );
3293 }
3294}