1#[non_exhaustive]
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum Color {
9 Reset,
11 Black,
13 Red,
15 Green,
17 Yellow,
19 Blue,
21 Magenta,
23 Cyan,
25 White,
27 DarkGray,
29 LightRed,
31 LightGreen,
33 LightYellow,
35 LightBlue,
37 LightMagenta,
39 LightCyan,
41 LightWhite,
43 Rgb(u8, u8, u8),
45 Indexed(u8),
47}
48
49#[inline]
50fn to_linear(c: f64) -> f64 {
51 if c <= 0.04045 {
52 c / 12.92
53 } else {
54 ((c + 0.055) / 1.055).powf(2.4)
55 }
56}
57
58impl Color {
59 pub(crate) fn to_rgb(self) -> (u8, u8, u8) {
64 match self {
65 Color::Rgb(r, g, b) => (r, g, b),
66 Color::Black => (0, 0, 0),
67 Color::Red => (205, 49, 49),
68 Color::Green => (13, 188, 121),
69 Color::Yellow => (229, 229, 16),
70 Color::Blue => (36, 114, 200),
71 Color::Magenta => (188, 63, 188),
72 Color::Cyan => (17, 168, 205),
73 Color::White => (229, 229, 229),
74 Color::DarkGray => (128, 128, 128),
75 Color::LightRed => (255, 0, 0),
76 Color::LightGreen => (0, 255, 0),
77 Color::LightYellow => (255, 255, 0),
78 Color::LightBlue => (0, 0, 255),
79 Color::LightMagenta => (255, 0, 255),
80 Color::LightCyan => (0, 255, 255),
81 Color::LightWhite => (255, 255, 255),
82 Color::Reset => (0, 0, 0),
83 Color::Indexed(idx) => xterm256_to_rgb(idx),
84 }
85 }
86
87 pub fn luminance_f64(self) -> f64 {
105 let (r, g, b) = self.to_rgb();
106 let rf = to_linear(f64::from(r) / 255.0);
107 let gf = to_linear(f64::from(g) / 255.0);
108 let bf = to_linear(f64::from(b) / 255.0);
109 0.2126 * rf + 0.7152 * gf + 0.0722 * bf
110 }
111
112 #[deprecated(
114 since = "0.22.2",
115 note = "use Color::luminance_f64() to keep public float APIs on f64"
116 )]
117 pub fn luminance(self) -> f32 {
118 self.luminance_f64() as f32
119 }
120
121 pub fn contrast_fg(bg: Color) -> Color {
137 if bg.luminance_f64() > 0.179 {
138 Color::Rgb(0, 0, 0)
139 } else {
140 Color::Rgb(255, 255, 255)
141 }
142 }
143
144 pub fn blend_f64(self, other: Color, alpha: f64) -> Color {
160 let alpha = if alpha.is_finite() {
161 alpha.clamp(0.0, 1.0)
162 } else {
163 0.0
164 };
165 if alpha <= 0.0 {
166 return other;
167 }
168 if alpha >= 1.0 {
169 return self;
170 }
171 let (r1, g1, b1) = self.to_rgb();
172 let (r2, g2, b2) = other.to_rgb();
173 let r = (f64::from(r1) * alpha + f64::from(r2) * (1.0 - alpha)).round() as u8;
174 let g = (f64::from(g1) * alpha + f64::from(g2) * (1.0 - alpha)).round() as u8;
175 let b = (f64::from(b1) * alpha + f64::from(b2) * (1.0 - alpha)).round() as u8;
176 Color::Rgb(r, g, b)
177 }
178
179 #[deprecated(
181 since = "0.22.2",
182 note = "use Color::blend_f64() to keep public float APIs on f64"
183 )]
184 pub fn blend(self, other: Color, alpha: f32) -> Color {
185 self.blend_f64(other, f64::from(alpha))
186 }
187
188 pub fn lighten_f64(self, amount: f64) -> Color {
193 Color::Rgb(255, 255, 255).blend_f64(self, amount)
194 }
195
196 #[deprecated(
198 since = "0.22.2",
199 note = "use Color::lighten_f64() to keep public float APIs on f64"
200 )]
201 pub fn lighten(self, amount: f32) -> Color {
202 self.lighten_f64(f64::from(amount))
203 }
204
205 pub fn darken_f64(self, amount: f64) -> Color {
210 Color::Rgb(0, 0, 0).blend_f64(self, amount)
211 }
212
213 #[deprecated(
215 since = "0.22.2",
216 note = "use Color::darken_f64() to keep public float APIs on f64"
217 )]
218 pub fn darken(self, amount: f32) -> Color {
219 self.darken_f64(f64::from(amount))
220 }
221
222 pub fn contrast_ratio_f64(a: Color, b: Color) -> f64 {
236 let la = a.luminance_f64() + 0.05;
237 let lb = b.luminance_f64() + 0.05;
238 if la > lb { la / lb } else { lb / la }
239 }
240
241 #[deprecated(
243 since = "0.22.2",
244 note = "use Color::contrast_ratio_f64() to keep public float APIs on f64"
245 )]
246 pub fn contrast_ratio(a: Color, b: Color) -> f32 {
247 Self::contrast_ratio_f64(a, b) as f32
248 }
249
250 pub fn meets_contrast_aa(fg: Color, bg: Color) -> bool {
253 Self::contrast_ratio_f64(fg, bg) >= 4.5
254 }
255
256 pub fn downsampled(self, depth: ColorDepth) -> Color {
266 match depth {
267 ColorDepth::TrueColor => self,
268 ColorDepth::EightBit => match self {
269 Color::Rgb(r, g, b) => Color::Indexed(rgb_to_ansi256(r, g, b)),
270 other => other,
271 },
272 ColorDepth::Basic => match self {
273 Color::Rgb(r, g, b) => rgb_to_ansi16(r, g, b),
274 Color::Indexed(i) => {
275 let (r, g, b) = xterm256_to_rgb(i);
276 rgb_to_ansi16(r, g, b)
277 }
278 other => other,
279 },
280 ColorDepth::NoColor => Color::Reset,
281 }
282 }
283
284 #[doc(alias = "parse")]
301 pub fn from_hex(s: &str) -> Option<Color> {
302 let hex = s.strip_prefix('#')?;
303 match hex.len() {
304 3 => {
305 let mut it = hex.chars().map(|c| c.to_digit(16));
306 let r = it.next()??;
307 let g = it.next()??;
308 let b = it.next()??;
309 Some(Color::Rgb((r * 17) as u8, (g * 17) as u8, (b * 17) as u8))
311 }
312 6 => {
313 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
314 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
315 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
316 Some(Color::Rgb(r, g, b))
317 }
318 _ => None,
319 }
320 }
321
322 pub fn to_hex(self) -> String {
335 let (r, g, b) = self.to_rgb();
336 format!("#{r:02x}{g:02x}{b:02x}")
337 }
338
339 pub fn from_hsl_f64(h: f64, s: f64, l: f64) -> Color {
354 let (r, g, b) = hsl_to_rgb(h, s.clamp(0.0, 1.0), l.clamp(0.0, 1.0));
355 Color::Rgb(r, g, b)
356 }
357
358 #[deprecated(
360 since = "0.22.2",
361 note = "use Color::from_hsl_f64() to keep public float APIs on f64"
362 )]
363 pub fn from_hsl(h: f32, s: f32, l: f32) -> Color {
364 Self::from_hsl_f64(f64::from(h), f64::from(s), f64::from(l))
365 }
366
367 pub fn from_hsv_f64(h: f64, s: f64, v: f64) -> Color {
382 let (r, g, b) = hsv_to_rgb(h, s.clamp(0.0, 1.0), v.clamp(0.0, 1.0));
383 Color::Rgb(r, g, b)
384 }
385
386 #[deprecated(
388 since = "0.22.2",
389 note = "use Color::from_hsv_f64() to keep public float APIs on f64"
390 )]
391 pub fn from_hsv(h: f32, s: f32, v: f32) -> Color {
392 Self::from_hsv_f64(f64::from(h), f64::from(s), f64::from(v))
393 }
394
395 pub fn rotate_hue_f64(self, degrees: f64) -> Color {
412 let (r, g, b) = self.to_rgb();
413 let (h, s, l) = rgb_to_hsl(r, g, b);
414 let (nr, ng, nb) = hsl_to_rgb(h + degrees, s, l);
415 Color::Rgb(nr, ng, nb)
416 }
417
418 #[deprecated(
420 since = "0.22.2",
421 note = "use Color::rotate_hue_f64() to keep public float APIs on f64"
422 )]
423 pub fn rotate_hue(self, degrees: f32) -> Color {
424 self.rotate_hue_f64(f64::from(degrees))
425 }
426}
427
428impl From<(u8, u8, u8)> for Color {
429 fn from((r, g, b): (u8, u8, u8)) -> Color {
431 Color::Rgb(r, g, b)
432 }
433}
434
435impl From<[u8; 3]> for Color {
436 fn from([r, g, b]: [u8; 3]) -> Color {
438 Color::Rgb(r, g, b)
439 }
440}
441
442impl From<u32> for Color {
443 fn from(value: u32) -> Color {
455 let r = ((value >> 16) & 0xff) as u8;
456 let g = ((value >> 8) & 0xff) as u8;
457 let b = (value & 0xff) as u8;
458 Color::Rgb(r, g, b)
459 }
460}
461
462#[non_exhaustive]
476#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
477pub enum ColorParseError {
478 InvalidLength,
481 InvalidHexDigit,
483 Unknown,
485}
486
487impl std::fmt::Display for ColorParseError {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 let msg = match self {
490 ColorParseError::InvalidLength => "invalid color: hex form must have 3 or 6 digits",
491 ColorParseError::InvalidHexDigit => "invalid color: non-hex digit in hex form",
492 ColorParseError::Unknown => {
493 "invalid color: expected #rgb/#rrggbb, rrggbb, or a named color"
494 }
495 };
496 f.write_str(msg)
497 }
498}
499
500impl core::error::Error for ColorParseError {}
501
502impl std::str::FromStr for Color {
503 type Err = ColorParseError;
504
505 fn from_str(s: &str) -> Result<Color, ColorParseError> {
530 let trimmed = s.trim();
531
532 if let Some(c) = named_color(trimmed) {
535 return Ok(c);
536 }
537
538 let had_hash = trimmed.starts_with('#');
539 let hex = trimmed.strip_prefix('#').unwrap_or(trimmed);
540
541 match hex.len() {
542 3 => {
543 let mut it = hex.chars().map(|c| c.to_digit(16));
544 let r = it
545 .next()
546 .flatten()
547 .ok_or(ColorParseError::InvalidHexDigit)?;
548 let g = it
549 .next()
550 .flatten()
551 .ok_or(ColorParseError::InvalidHexDigit)?;
552 let b = it
553 .next()
554 .flatten()
555 .ok_or(ColorParseError::InvalidHexDigit)?;
556 Ok(Color::Rgb((r * 17) as u8, (g * 17) as u8, (b * 17) as u8))
557 }
558 6 => {
559 let r = u8::from_str_radix(&hex[0..2], 16)
560 .map_err(|_| ColorParseError::InvalidHexDigit)?;
561 let g = u8::from_str_radix(&hex[2..4], 16)
562 .map_err(|_| ColorParseError::InvalidHexDigit)?;
563 let b = u8::from_str_radix(&hex[4..6], 16)
564 .map_err(|_| ColorParseError::InvalidHexDigit)?;
565 Ok(Color::Rgb(r, g, b))
566 }
567 _ if had_hash => Err(ColorParseError::InvalidLength),
571 _ => Err(ColorParseError::Unknown),
572 }
573 }
574}
575
576fn named_color(s: &str) -> Option<Color> {
581 let lower = s.to_ascii_lowercase();
582 Some(match lower.as_str() {
583 "reset" | "default" => Color::Reset,
584 "black" => Color::Black,
585 "red" => Color::Red,
586 "green" => Color::Green,
587 "yellow" => Color::Yellow,
588 "blue" => Color::Blue,
589 "magenta" => Color::Magenta,
590 "cyan" => Color::Cyan,
591 "white" => Color::White,
592 "darkgray" | "darkgrey" | "gray" | "grey" => Color::DarkGray,
593 "lightred" => Color::LightRed,
594 "lightgreen" => Color::LightGreen,
595 "lightyellow" => Color::LightYellow,
596 "lightblue" => Color::LightBlue,
597 "lightmagenta" => Color::LightMagenta,
598 "lightcyan" => Color::LightCyan,
599 "lightwhite" => Color::LightWhite,
600 _ => return None,
601 })
602}
603
604fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
609 let h = wrap_hue(h);
610 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
611 let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
612 let m = l - c / 2.0;
613 let (r1, g1, b1) = hue_sextant(h, c, x);
614 (
615 round_channel(r1 + m),
616 round_channel(g1 + m),
617 round_channel(b1 + m),
618 )
619}
620
621fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (u8, u8, u8) {
626 let h = wrap_hue(h);
627 let c = v * s;
628 let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
629 let m = v - c;
630 let (r1, g1, b1) = hue_sextant(h, c, x);
631 (
632 round_channel(r1 + m),
633 round_channel(g1 + m),
634 round_channel(b1 + m),
635 )
636}
637
638fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
641 let rf = f64::from(r) / 255.0;
642 let gf = f64::from(g) / 255.0;
643 let bf = f64::from(b) / 255.0;
644 let max = rf.max(gf).max(bf);
645 let min = rf.min(gf).min(bf);
646 let delta = max - min;
647 let l = (max + min) / 2.0;
648
649 if delta <= f64::EPSILON {
650 return (0.0, 0.0, l);
652 }
653
654 let s = if l > 0.5 {
655 delta / (2.0 - max - min)
656 } else {
657 delta / (max + min)
658 };
659
660 let h = if max == rf {
661 let h = (gf - bf) / delta;
662 h % 6.0
663 } else if max == gf {
664 (bf - rf) / delta + 2.0
665 } else {
666 (rf - gf) / delta + 4.0
667 } * 60.0;
668
669 (wrap_hue(h), s, l)
670}
671
672#[inline]
675fn hue_sextant(h: f64, c: f64, x: f64) -> (f64, f64, f64) {
676 match h {
677 h if h < 60.0 => (c, x, 0.0),
678 h if h < 120.0 => (x, c, 0.0),
679 h if h < 180.0 => (0.0, c, x),
680 h if h < 240.0 => (0.0, x, c),
681 h if h < 300.0 => (x, 0.0, c),
682 _ => (c, 0.0, x),
683 }
684}
685
686#[inline]
688fn wrap_hue(h: f64) -> f64 {
689 let h = h % 360.0;
690 if h < 0.0 { h + 360.0 } else { h }
691}
692
693#[inline]
695fn round_channel(v: f64) -> u8 {
696 (v * 255.0).round().clamp(0.0, 255.0) as u8
697}
698
699#[cfg(feature = "serde")]
700impl Color {
701 fn named_token(self) -> Option<&'static str> {
703 Some(match self {
704 Color::Reset => "reset",
705 Color::Black => "black",
706 Color::Red => "red",
707 Color::Green => "green",
708 Color::Yellow => "yellow",
709 Color::Blue => "blue",
710 Color::Magenta => "magenta",
711 Color::Cyan => "cyan",
712 Color::White => "white",
713 Color::DarkGray => "darkgray",
714 Color::LightRed => "lightred",
715 Color::LightGreen => "lightgreen",
716 Color::LightYellow => "lightyellow",
717 Color::LightBlue => "lightblue",
718 Color::LightMagenta => "lightmagenta",
719 Color::LightCyan => "lightcyan",
720 Color::LightWhite => "lightwhite",
721 Color::Rgb(..) | Color::Indexed(_) => return None,
722 })
723 }
724
725 fn from_token(s: &str) -> Option<Color> {
731 if let Some(c) = Color::from_hex(s) {
732 return Some(c);
733 }
734 let lower = s.trim().to_ascii_lowercase();
735 if let Some(rest) = lower.strip_prefix("indexed:") {
736 return rest.trim().parse::<u8>().ok().map(Color::Indexed);
737 }
738 Some(match lower.as_str() {
739 "reset" | "default" => Color::Reset,
740 "black" => Color::Black,
741 "red" => Color::Red,
742 "green" => Color::Green,
743 "yellow" => Color::Yellow,
744 "blue" => Color::Blue,
745 "magenta" => Color::Magenta,
746 "cyan" => Color::Cyan,
747 "white" => Color::White,
748 "darkgray" | "darkgrey" | "gray" | "grey" => Color::DarkGray,
749 "lightred" => Color::LightRed,
750 "lightgreen" => Color::LightGreen,
751 "lightyellow" => Color::LightYellow,
752 "lightblue" => Color::LightBlue,
753 "lightmagenta" => Color::LightMagenta,
754 "lightcyan" => Color::LightCyan,
755 "lightwhite" => Color::LightWhite,
756 _ => return None,
757 })
758 }
759
760 fn to_token(self) -> String {
765 if let Some(name) = self.named_token() {
766 return name.to_string();
767 }
768 match self {
769 Color::Indexed(n) => format!("indexed:{n}"),
770 other => other.to_hex(),
772 }
773 }
774}
775
776#[cfg(feature = "serde")]
777impl serde::Serialize for Color {
778 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
781 where
782 S: serde::Serializer,
783 {
784 serializer.serialize_str(&self.to_token())
785 }
786}
787
788#[cfg(feature = "serde")]
789impl<'de> serde::Deserialize<'de> for Color {
790 fn deserialize<D>(deserializer: D) -> Result<Color, D::Error>
793 where
794 D: serde::Deserializer<'de>,
795 {
796 struct ColorVisitor;
797
798 impl serde::de::Visitor<'_> for ColorVisitor {
799 type Value = Color;
800
801 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802 f.write_str("a color token like \"#ff6b6b\", \"cyan\", or \"indexed:245\"")
803 }
804
805 fn visit_str<E>(self, value: &str) -> Result<Color, E>
806 where
807 E: serde::de::Error,
808 {
809 Color::from_token(value).ok_or_else(|| {
810 E::custom(format!(
811 "invalid color token {value:?}: expected #rgb/#rrggbb, a named color, or indexed:N"
812 ))
813 })
814 }
815 }
816
817 deserializer.deserialize_str(ColorVisitor)
818 }
819}
820
821#[non_exhaustive]
827#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
828#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
829pub enum ColorDepth {
830 TrueColor,
832 EightBit,
834 Basic,
836 NoColor,
841}
842
843#[cfg(test)]
844mod color_depth_tests {
845 use super::{Color, ColorDepth};
846
847 #[test]
848 fn no_color_downsamples_everything_to_reset() {
849 assert_eq!(Color::Red.downsampled(ColorDepth::NoColor), Color::Reset);
850 assert_eq!(
851 Color::Rgb(10, 20, 30).downsampled(ColorDepth::NoColor),
852 Color::Reset
853 );
854 assert_eq!(
855 Color::Indexed(44).downsampled(ColorDepth::NoColor),
856 Color::Reset
857 );
858 }
859}
860
861impl ColorDepth {
862 pub fn detect() -> Self {
870 if std::env::var("NO_COLOR")
872 .ok()
873 .is_some_and(|v| !v.is_empty())
874 {
875 return Self::NoColor;
876 }
877 if let Ok(ct) = std::env::var("COLORTERM") {
878 let ct = ct.to_lowercase();
879 if ct == "truecolor" || ct == "24bit" {
880 return Self::TrueColor;
881 }
882 }
883 if let Ok(term) = std::env::var("TERM")
884 && term.contains("256color")
885 {
886 return Self::EightBit;
887 }
888 Self::Basic
889 }
890}
891
892fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
893 if r == g && g == b {
894 if r < 8 {
895 return 16;
896 }
897 if r >= 248 {
898 return 231;
899 }
900 return 232 + (((r as u16 - 8) * 24 / 240) as u8);
901 }
902
903 let ri = if r < 48 {
904 0
905 } else {
906 ((r as u16 - 35) / 40) as u8
907 };
908 let gi = if g < 48 {
909 0
910 } else {
911 ((g as u16 - 35) / 40) as u8
912 };
913 let bi = if b < 48 {
914 0
915 } else {
916 ((b as u16 - 35) / 40) as u8
917 };
918 16 + 36 * ri.min(5) + 6 * gi.min(5) + bi.min(5)
919}
920
921fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> Color {
922 let lum = 0.2126 * to_linear(f64::from(r) / 255.0)
923 + 0.7152 * to_linear(f64::from(g) / 255.0)
924 + 0.0722 * to_linear(f64::from(b) / 255.0);
925
926 let max = r.max(g).max(b);
927 let min = r.min(g).min(b);
928 let saturation = if max == 0 {
929 0.0
930 } else {
931 (max - min) as f32 / max as f32
932 };
933
934 if saturation < 0.2 {
935 return match lum {
937 l if l < 0.05 => Color::Black,
938 l if l < 0.25 => Color::DarkGray,
939 l if l < 0.7 => Color::White,
940 _ => Color::White, };
942 }
943
944 let bright = max >= 200 && min >= 64;
952
953 let rf = r as f32;
954 let gf = g as f32;
955 let bf = b as f32;
956
957 if rf >= gf && rf >= bf {
958 if gf > bf * 1.5 {
959 if bright {
960 Color::LightYellow
961 } else {
962 Color::Yellow
963 }
964 } else if bf > gf * 1.5 {
965 if bright {
966 Color::LightMagenta
967 } else {
968 Color::Magenta
969 }
970 } else if bright {
971 Color::LightRed
972 } else {
973 Color::Red
974 }
975 } else if gf >= rf && gf >= bf {
976 if bf > rf * 1.5 {
977 if bright {
978 Color::LightCyan
979 } else {
980 Color::Cyan
981 }
982 } else if bright {
983 Color::LightGreen
984 } else {
985 Color::Green
986 }
987 } else if rf > gf * 1.5 {
988 if bright {
989 Color::LightMagenta
990 } else {
991 Color::Magenta
992 }
993 } else if gf > rf * 1.5 {
994 if bright {
995 Color::LightCyan
996 } else {
997 Color::Cyan
998 }
999 } else if bright {
1000 Color::LightBlue
1001 } else {
1002 Color::Blue
1003 }
1004}
1005
1006fn xterm256_to_rgb(idx: u8) -> (u8, u8, u8) {
1007 match idx {
1008 0 => (0, 0, 0),
1009 1 => (128, 0, 0),
1010 2 => (0, 128, 0),
1011 3 => (128, 128, 0),
1012 4 => (0, 0, 128),
1013 5 => (128, 0, 128),
1014 6 => (0, 128, 128),
1015 7 => (192, 192, 192),
1016 8 => (128, 128, 128),
1017 9 => (255, 0, 0),
1018 10 => (0, 255, 0),
1019 11 => (255, 255, 0),
1020 12 => (0, 0, 255),
1021 13 => (255, 0, 255),
1022 14 => (0, 255, 255),
1023 15 => (255, 255, 255),
1024 16..=231 => {
1025 let n = idx - 16;
1026 let b_idx = n % 6;
1027 let g_idx = (n / 6) % 6;
1028 let r_idx = n / 36;
1029 let to_val = |i: u8| if i == 0 { 0u8 } else { 55 + 40 * i };
1030 (to_val(r_idx), to_val(g_idx), to_val(b_idx))
1031 }
1032 232..=255 => {
1033 let v = 8 + 10 * (idx - 232);
1034 (v, v, v)
1035 }
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 #![allow(clippy::unwrap_used)]
1042 use super::*;
1043
1044 #[test]
1045 fn blend_halfway_rounds_to_128() {
1046 assert_eq!(
1047 Color::Rgb(255, 255, 255).blend_f64(Color::Rgb(0, 0, 0), 0.5),
1048 Color::Rgb(128, 128, 128)
1049 );
1050 }
1051
1052 #[test]
1053 fn contrast_ratio_white_on_black_is_high() {
1054 let ratio = Color::contrast_ratio_f64(Color::White, Color::Black);
1055 assert!(ratio > 15.0);
1056 }
1057
1058 #[test]
1059 fn contrast_ratio_same_color_is_one() {
1060 let ratio = Color::contrast_ratio_f64(Color::Rgb(100, 100, 100), Color::Rgb(100, 100, 100));
1061 assert!((ratio - 1.0).abs() < 0.01);
1062 }
1063
1064 #[test]
1065 fn lighten_and_darken_amount_endpoints_are_not_reversed() {
1066 let color = Color::Rgb(100, 120, 140);
1067 assert_eq!(color.lighten_f64(0.0), color);
1068 assert_eq!(color.lighten_f64(1.0), Color::Rgb(255, 255, 255));
1069 assert_eq!(
1070 color.lighten_f64(0.5),
1071 Color::Rgb(178, 188, 198),
1072 "half lightening blends halfway toward white"
1073 );
1074
1075 assert_eq!(color.darken_f64(0.0), color);
1076 assert_eq!(color.darken_f64(1.0), Color::Rgb(0, 0, 0));
1077 assert_eq!(
1078 color.darken_f64(0.5),
1079 Color::Rgb(50, 60, 70),
1080 "half darkening blends halfway toward black"
1081 );
1082 }
1083
1084 #[test]
1085 fn non_finite_color_amounts_use_the_zero_amount_policy() {
1086 let color = Color::Rgb(10, 20, 30);
1087 assert_eq!(color.blend_f64(Color::White, f64::NAN), Color::White);
1088 assert_eq!(color.lighten_f64(f64::NAN), color);
1089 assert_eq!(color.darken_f64(f64::INFINITY), color);
1090 }
1091
1092 #[test]
1093 fn meets_contrast_aa_white_on_black() {
1094 assert!(Color::meets_contrast_aa(Color::White, Color::Black));
1095 }
1096
1097 #[test]
1098 fn meets_contrast_aa_low_contrast_fails() {
1099 assert!(!Color::meets_contrast_aa(
1100 Color::Rgb(180, 180, 180),
1101 Color::Rgb(200, 200, 200)
1102 ));
1103 }
1104
1105 #[test]
1108 fn rgb_to_ansi256_no_overflow_full_range() {
1109 for r in 0u8..=255 {
1111 for g in 0u8..=255 {
1112 for b in 0u8..=255 {
1113 let _ = Color::Rgb(r, g, b).downsampled(ColorDepth::EightBit);
1114 }
1115 }
1116 }
1117 }
1118
1119 #[test]
1120 fn rgb_248_maps_to_231() {
1121 assert_eq!(
1122 Color::Rgb(248, 248, 248).downsampled(ColorDepth::EightBit),
1123 Color::Indexed(231)
1124 );
1125 }
1126
1127 #[test]
1130 fn luminance_dracula_purple_wcag() {
1131 let l = Color::Rgb(189, 147, 249).luminance_f64();
1132 assert!((l - 0.385).abs() < 0.01, "expected ~0.385, got {l}");
1133 }
1134
1135 #[test]
1136 fn contrast_aa_dracula_pair() {
1137 let p = Color::Rgb(189, 147, 249);
1138 let bg = Color::Rgb(40, 42, 54);
1139 assert!(Color::meets_contrast_aa(p, bg));
1140 let r = Color::contrast_ratio_f64(p, bg);
1141 assert!((r - 5.90).abs() < 0.1, "expected ~5.90, got {r}");
1142 }
1143
1144 #[test]
1145 fn contrast_white_on_black_is_21() {
1146 let r = Color::contrast_ratio_f64(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0));
1147 assert!((r - 21.0).abs() < 0.5, "expected ~21.0, got {r}");
1148 }
1149
1150 #[test]
1153 fn rgb_to_ansi16_bright_variants() {
1154 assert_eq!(
1156 Color::Rgb(255, 80, 80).downsampled(ColorDepth::Basic),
1157 Color::LightRed
1158 );
1159 assert_eq!(
1161 Color::Rgb(128, 20, 20).downsampled(ColorDepth::Basic),
1162 Color::Red
1163 );
1164 assert_eq!(
1166 Color::Rgb(200, 200, 200).downsampled(ColorDepth::Basic),
1167 Color::White
1168 );
1169 assert_eq!(
1171 Color::Rgb(80, 80, 80).downsampled(ColorDepth::Basic),
1172 Color::DarkGray
1173 );
1174 }
1175
1176 use std::str::FromStr;
1179
1180 #[test]
1181 fn from_tuple_and_array() {
1182 assert_eq!(Color::from((255, 107, 107)), Color::Rgb(255, 107, 107));
1183 assert_eq!(Color::from([1u8, 2, 3]), Color::Rgb(1, 2, 3));
1184 let c: Color = (10, 20, 30).into();
1186 assert_eq!(c, Color::Rgb(10, 20, 30));
1187 }
1188
1189 #[test]
1190 fn from_u32_packs_rrggbb() {
1191 assert_eq!(Color::from(0xff6b6b_u32), Color::Rgb(255, 107, 107));
1192 assert_eq!(Color::from(0x000000_u32), Color::Rgb(0, 0, 0));
1193 assert_eq!(Color::from(0xffffff_u32), Color::Rgb(255, 255, 255));
1194 assert_eq!(Color::from(0xff00ff00_u32), Color::Rgb(0, 255, 0));
1196 }
1197
1198 #[test]
1199 fn from_str_hex_round_trips() {
1200 assert_eq!(
1201 Color::from_str("#ff6b6b").unwrap(),
1202 Color::Rgb(255, 107, 107)
1203 );
1204 assert_eq!(
1206 Color::from_str("ff6b6b").unwrap(),
1207 Color::Rgb(255, 107, 107)
1208 );
1209 assert_eq!(Color::from_str("#abc").unwrap(), Color::Rgb(170, 187, 204));
1211 assert_eq!(Color::from_str("abc").unwrap(), Color::Rgb(170, 187, 204));
1212 assert_eq!(
1214 Color::from_str(" #ff6b6b ").unwrap(),
1215 Color::Rgb(255, 107, 107)
1216 );
1217 let c = Color::Rgb(18, 52, 86);
1219 assert_eq!(Color::from_str(&c.to_hex()).unwrap(), c);
1220 }
1221
1222 #[test]
1223 fn from_str_named_colors() {
1224 assert_eq!(Color::from_str("cyan").unwrap(), Color::Cyan);
1225 assert_eq!(Color::from_str("LightBlue").unwrap(), Color::LightBlue);
1226 assert_eq!(Color::from_str("DARKGRAY").unwrap(), Color::DarkGray);
1227 assert_eq!(Color::from_str("grey").unwrap(), Color::DarkGray);
1228 assert_eq!(Color::from_str("reset").unwrap(), Color::Reset);
1229 assert_eq!(Color::from_str("default").unwrap(), Color::Reset);
1230 }
1231
1232 #[test]
1233 fn from_str_error_cases() {
1234 assert_eq!(
1236 Color::from_str("#ff6b").unwrap_err(),
1237 ColorParseError::InvalidLength
1238 );
1239 assert_eq!(
1241 Color::from_str("#zz0011").unwrap_err(),
1242 ColorParseError::InvalidHexDigit
1243 );
1244 assert_eq!(
1246 Color::from_str("#xyz").unwrap_err(),
1247 ColorParseError::InvalidHexDigit
1248 );
1249 assert_eq!(
1251 Color::from_str("nope").unwrap_err(),
1252 ColorParseError::Unknown
1253 );
1254 assert_eq!(Color::from_str("").unwrap_err(), ColorParseError::Unknown);
1255 }
1256
1257 #[test]
1258 fn color_parse_error_display_and_error_trait() {
1259 let e = ColorParseError::InvalidLength;
1261 assert!(!e.to_string().is_empty());
1262 let _: &dyn std::error::Error = &e;
1263 }
1264
1265 #[test]
1266 fn from_hsl_primaries() {
1267 assert_eq!(Color::from_hsl_f64(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0));
1268 assert_eq!(Color::from_hsl_f64(120.0, 1.0, 0.5), Color::Rgb(0, 255, 0));
1269 assert_eq!(Color::from_hsl_f64(240.0, 1.0, 0.5), Color::Rgb(0, 0, 255));
1270 assert_eq!(Color::from_hsl_f64(0.0, 1.0, 0.0), Color::Rgb(0, 0, 0));
1272 assert_eq!(
1273 Color::from_hsl_f64(0.0, 1.0, 1.0),
1274 Color::Rgb(255, 255, 255)
1275 );
1276 assert_eq!(
1278 Color::from_hsl_f64(123.0, 0.0, 0.5),
1279 Color::Rgb(128, 128, 128)
1280 );
1281 }
1282
1283 #[test]
1284 fn from_hsl_wraps_and_clamps() {
1285 assert_eq!(Color::from_hsl_f64(360.0, 1.0, 0.5), Color::Rgb(255, 0, 0));
1287 assert_eq!(Color::from_hsl_f64(-120.0, 1.0, 0.5), Color::Rgb(0, 0, 255));
1289 assert_eq!(
1291 Color::from_hsl_f64(0.0, 5.0, 2.0),
1292 Color::Rgb(255, 255, 255)
1293 );
1294 }
1295
1296 #[test]
1297 fn from_hsv_primaries() {
1298 assert_eq!(Color::from_hsv_f64(0.0, 1.0, 1.0), Color::Rgb(255, 0, 0));
1299 assert_eq!(Color::from_hsv_f64(120.0, 1.0, 1.0), Color::Rgb(0, 255, 0));
1300 assert_eq!(Color::from_hsv_f64(240.0, 1.0, 1.0), Color::Rgb(0, 0, 255));
1301 assert_eq!(
1303 Color::from_hsv_f64(0.0, 0.0, 1.0),
1304 Color::Rgb(255, 255, 255)
1305 );
1306 assert_eq!(Color::from_hsv_f64(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0));
1307 }
1308
1309 #[test]
1310 fn rotate_hue_primary_round_trip() {
1311 assert_eq!(
1313 Color::Rgb(255, 0, 0).rotate_hue_f64(120.0),
1314 Color::Rgb(0, 255, 0)
1315 );
1316 assert_eq!(
1317 Color::Rgb(0, 255, 0).rotate_hue_f64(120.0),
1318 Color::Rgb(0, 0, 255)
1319 );
1320 assert_eq!(
1322 Color::Rgb(255, 0, 0).rotate_hue_f64(180.0),
1323 Color::Rgb(0, 255, 255)
1324 );
1325 assert_eq!(
1327 Color::Rgb(255, 0, 0).rotate_hue_f64(360.0),
1328 Color::Rgb(255, 0, 0)
1329 );
1330 }
1331
1332 #[test]
1333 fn rotate_hue_resolves_named_to_rgb() {
1334 let rotated = Color::Red.rotate_hue_f64(0.0);
1336 assert_eq!(rotated, Color::Rgb(205, 49, 49));
1337 let gray = Color::Rgb(120, 120, 120).rotate_hue_f64(90.0);
1338 assert_eq!(gray, Color::Rgb(120, 120, 120));
1340 }
1341}