ratatui/style/color.rs
1#![allow(clippy::unreadable_literal)]
2
3use std::{fmt, str::FromStr};
4
5use crate::style::stylize::{ColorDebug, ColorDebugKind};
6
7/// ANSI Color
8///
9/// All colors from the [ANSI color table] are supported (though some names are not exactly the
10/// same).
11///
12/// | Color Name | Color | Foreground | Background |
13/// |----------------|-------------------------|------------|------------|
14/// | `black` | [`Color::Black`] | 30 | 40 |
15/// | `red` | [`Color::Red`] | 31 | 41 |
16/// | `green` | [`Color::Green`] | 32 | 42 |
17/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
18/// | `blue` | [`Color::Blue`] | 34 | 44 |
19/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
20/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
21/// | `gray`* | [`Color::Gray`] | 37 | 47 |
22/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
23/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
24/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
25/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
26/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
27/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
28/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
29/// | `white`* | [`Color::White`] | 97 | 107 |
30///
31/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
32/// - `gray` is sometimes called `silver` - this is supported
33/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
34/// - `white` is sometimes called `light white` or `bright white` (both are supported)
35/// - we support `bright` and `light` prefixes for all colors
36/// - we support `-` and `_` and ` ` as separators for all colors
37/// - we support both `gray` and `grey` spellings
38///
39/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
40/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
41///
42/// # Example
43///
44/// ```
45/// use std::str::FromStr;
46///
47/// use ratatui::style::Color;
48///
49/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
50/// assert_eq!("red".parse(), Ok(Color::Red));
51/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
52/// assert_eq!("light red".parse(), Ok(Color::LightRed));
53/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
54/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
55/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
56/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
57/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
58/// assert_eq!("silver".parse(), Ok(Color::Gray));
59/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
60/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
61/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
62/// assert_eq!("white".parse(), Ok(Color::White));
63/// assert_eq!("bright white".parse(), Ok(Color::White));
64/// ```
65///
66/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
67#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
68pub enum Color {
69 /// Resets the foreground or background color
70 #[default]
71 Reset,
72 /// ANSI Color: Black. Foreground: 30, Background: 40
73 Black,
74 /// ANSI Color: Red. Foreground: 31, Background: 41
75 Red,
76 /// ANSI Color: Green. Foreground: 32, Background: 42
77 Green,
78 /// ANSI Color: Yellow. Foreground: 33, Background: 43
79 Yellow,
80 /// ANSI Color: Blue. Foreground: 34, Background: 44
81 Blue,
82 /// ANSI Color: Magenta. Foreground: 35, Background: 45
83 Magenta,
84 /// ANSI Color: Cyan. Foreground: 36, Background: 46
85 Cyan,
86 /// ANSI Color: White. Foreground: 37, Background: 47
87 ///
88 /// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
89 Gray,
90 /// ANSI Color: Bright Black. Foreground: 90, Background: 100
91 ///
92 /// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
93 DarkGray,
94 /// ANSI Color: Bright Red. Foreground: 91, Background: 101
95 LightRed,
96 /// ANSI Color: Bright Green. Foreground: 92, Background: 102
97 LightGreen,
98 /// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
99 LightYellow,
100 /// ANSI Color: Bright Blue. Foreground: 94, Background: 104
101 LightBlue,
102 /// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
103 LightMagenta,
104 /// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
105 LightCyan,
106 /// ANSI Color: Bright White. Foreground: 97, Background: 107
107 /// Sometimes called `bright white` or `light white` in some terminals
108 White,
109 /// An RGB color.
110 ///
111 /// Note that only terminals that support 24-bit true color will display this correctly.
112 /// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
113 /// support this.
114 ///
115 /// If the terminal does not support true color, code using the [`TermwizBackend`] will
116 /// fallback to the default text color. Crossterm and Termion do not have this capability and
117 /// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
118 /// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
119 ///
120 /// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
121 ///
122 /// [`TermwizBackend`]: crate::backend::TermwizBackend
123 Rgb(u8, u8, u8),
124 /// An 8-bit 256 color.
125 ///
126 /// See also <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
127 Indexed(u8),
128}
129
130impl Color {
131 /// Convert a u32 to a Color
132 ///
133 /// The u32 should be in the format 0x00RRGGBB.
134 pub const fn from_u32(u: u32) -> Self {
135 let r = (u >> 16) as u8;
136 let g = (u >> 8) as u8;
137 let b = u as u8;
138 Self::Rgb(r, g, b)
139 }
140}
141
142#[cfg(feature = "serde")]
143impl serde::Serialize for Color {
144 /// This utilises the [`fmt::Display`] implementation for serialization.
145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
146 where
147 S: serde::Serializer,
148 {
149 serializer.serialize_str(&self.to_string())
150 }
151}
152
153#[cfg(feature = "serde")]
154impl<'de> serde::Deserialize<'de> for Color {
155 /// This is used to deserialize a value into Color via serde.
156 ///
157 /// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
158 /// and indexed values are able to be deserialized. In addition, values that were produced by
159 /// the the older serialization implementation of Color are also able to be deserialized.
160 ///
161 /// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
162 /// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
163 /// serialized using the string representation of the index and the RGB hex value, for example
164 /// in json it would now be `"10"` and `"#FF00FF"` respectively.
165 ///
166 /// See the [`Color`] documentation for more information on color names.
167 ///
168 /// # Examples
169 ///
170 /// ```
171 /// use std::str::FromStr;
172 ///
173 /// use ratatui::style::Color;
174 ///
175 /// #[derive(Debug, serde::Deserialize)]
176 /// struct Theme {
177 /// color: Color,
178 /// }
179 ///
180 /// # fn get_theme() -> Result<(), serde_json::Error> {
181 /// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
182 /// assert_eq!(theme.color, Color::White);
183 ///
184 /// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
185 /// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
186 ///
187 /// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
188 /// assert_eq!(theme.color, Color::Indexed(42));
189 ///
190 /// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
191 /// assert!(err.is_data());
192 /// assert_eq!(
193 /// err.to_string(),
194 /// "Failed to parse Colors at line 1 column 20"
195 /// );
196 ///
197 /// // Deserializing from the previous serialization implementation
198 /// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
199 /// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
200 ///
201 /// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
202 /// assert_eq!(theme.color, Color::Indexed(10));
203 /// # Ok(())
204 /// # }
205 /// ```
206 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
207 where
208 D: serde::Deserializer<'de>,
209 {
210 /// Colors are currently serialized with the `Display` implementation, so
211 /// RGB values are serialized via hex, for example "#FFFFFF".
212 ///
213 /// Previously they were serialized using serde derive, which encoded
214 /// RGB values as a map, for example { "rgb": [255, 255, 255] }.
215 ///
216 /// The deserialization implementation utilises a `Helper` struct
217 /// to be able to support both formats for backwards compatibility.
218 #[derive(serde::Deserialize)]
219 enum ColorWrapper {
220 Rgb(u8, u8, u8),
221 Indexed(u8),
222 }
223
224 #[derive(serde::Deserialize)]
225 #[serde(untagged)]
226 enum ColorFormat {
227 V2(String),
228 V1(ColorWrapper),
229 }
230
231 let multi_type = ColorFormat::deserialize(deserializer)
232 .map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
233 match multi_type {
234 ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
235 ColorFormat::V1(color_wrapper) => match color_wrapper {
236 ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
237 ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
238 },
239 }
240 }
241}
242
243/// Error type indicating a failure to parse a color string.
244#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
245pub struct ParseColorError;
246
247impl fmt::Display for ParseColorError {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 write!(f, "Failed to parse Colors")
250 }
251}
252
253impl std::error::Error for ParseColorError {}
254
255/// Converts a string representation to a `Color` instance.
256///
257/// The `from_str` function attempts to parse the given string and convert it to the corresponding
258/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
259/// be parsed, a `ParseColorError` is returned.
260///
261/// See the [`Color`] documentation for more information on the supported color names.
262///
263/// # Examples
264///
265/// ```
266/// use std::str::FromStr;
267///
268/// use ratatui::style::Color;
269///
270/// let color: Color = Color::from_str("blue").unwrap();
271/// assert_eq!(color, Color::Blue);
272///
273/// let color: Color = Color::from_str("#FF0000").unwrap();
274/// assert_eq!(color, Color::Rgb(255, 0, 0));
275///
276/// let color: Color = Color::from_str("10").unwrap();
277/// assert_eq!(color, Color::Indexed(10));
278///
279/// let color: Result<Color, _> = Color::from_str("invalid_color");
280/// assert!(color.is_err());
281/// ```
282impl FromStr for Color {
283 type Err = ParseColorError;
284
285 fn from_str(s: &str) -> Result<Self, Self::Err> {
286 Ok(
287 // There is a mix of different color names and formats in the wild.
288 // This is an attempt to support as many as possible.
289 match s
290 .to_lowercase()
291 .replace([' ', '-', '_'], "")
292 .replace("bright", "light")
293 .replace("grey", "gray")
294 .replace("silver", "gray")
295 .replace("lightblack", "darkgray")
296 .replace("lightwhite", "white")
297 .replace("lightgray", "white")
298 .as_ref()
299 {
300 "reset" => Self::Reset,
301 "black" => Self::Black,
302 "red" => Self::Red,
303 "green" => Self::Green,
304 "yellow" => Self::Yellow,
305 "blue" => Self::Blue,
306 "magenta" => Self::Magenta,
307 "cyan" => Self::Cyan,
308 "gray" => Self::Gray,
309 "darkgray" => Self::DarkGray,
310 "lightred" => Self::LightRed,
311 "lightgreen" => Self::LightGreen,
312 "lightyellow" => Self::LightYellow,
313 "lightblue" => Self::LightBlue,
314 "lightmagenta" => Self::LightMagenta,
315 "lightcyan" => Self::LightCyan,
316 "white" => Self::White,
317 _ => {
318 if let Ok(index) = s.parse::<u8>() {
319 Self::Indexed(index)
320 } else if let Some((r, g, b)) = parse_hex_color(s) {
321 Self::Rgb(r, g, b)
322 } else {
323 return Err(ParseColorError);
324 }
325 }
326 },
327 )
328 }
329}
330
331fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
332 if !input.starts_with('#') || input.len() != 7 {
333 return None;
334 }
335 let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
336 let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
337 let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
338 Some((r, g, b))
339}
340
341impl fmt::Display for Color {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 match self {
344 Self::Reset => write!(f, "Reset"),
345 Self::Black => write!(f, "Black"),
346 Self::Red => write!(f, "Red"),
347 Self::Green => write!(f, "Green"),
348 Self::Yellow => write!(f, "Yellow"),
349 Self::Blue => write!(f, "Blue"),
350 Self::Magenta => write!(f, "Magenta"),
351 Self::Cyan => write!(f, "Cyan"),
352 Self::Gray => write!(f, "Gray"),
353 Self::DarkGray => write!(f, "DarkGray"),
354 Self::LightRed => write!(f, "LightRed"),
355 Self::LightGreen => write!(f, "LightGreen"),
356 Self::LightYellow => write!(f, "LightYellow"),
357 Self::LightBlue => write!(f, "LightBlue"),
358 Self::LightMagenta => write!(f, "LightMagenta"),
359 Self::LightCyan => write!(f, "LightCyan"),
360 Self::White => write!(f, "White"),
361 Self::Rgb(r, g, b) => write!(f, "#{r:02X}{g:02X}{b:02X}"),
362 Self::Indexed(i) => write!(f, "{i}"),
363 }
364 }
365}
366
367impl Color {
368 pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
369 ColorDebug { kind, color: self }
370 }
371
372 /// Converts a HSL representation to a `Color::Rgb` instance.
373 ///
374 /// The `from_hsl` function converts the Hue, Saturation and Lightness values to a corresponding
375 /// `Color` RGB equivalent.
376 ///
377 /// Hue values should be in the range [-180..180]. Values outside this range are normalized by
378 /// wrapping.
379 ///
380 /// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are
381 /// clamped.
382 ///
383 /// Clamping to valid ranges happens before conversion to RGB.
384 ///
385 /// # Examples
386 ///
387 /// ```
388 /// use ratatui::{palette::Hsl, style::Color};
389 ///
390 /// // Minimum Lightness is black
391 /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
392 /// assert_eq!(color, Color::Rgb(0, 0, 0));
393 ///
394 /// // Maximum Lightness is white
395 /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.0));
396 /// assert_eq!(color, Color::Rgb(255, 255, 255));
397 ///
398 /// // Minimum Saturation is fully desaturated red = gray
399 /// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5));
400 /// assert_eq!(color, Color::Rgb(128, 128, 128));
401 ///
402 /// // Bright red
403 /// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5));
404 /// assert_eq!(color, Color::Rgb(255, 0, 0));
405 ///
406 /// // Bright blue
407 /// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5));
408 /// assert_eq!(color, Color::Rgb(0, 0, 255));
409 /// ```
410 #[cfg(feature = "palette")]
411 pub fn from_hsl(hsl: palette::Hsl) -> Self {
412 use palette::{Clamp, FromColor, Srgb};
413 let hsl = hsl.clamp();
414 let Srgb {
415 red,
416 green,
417 blue,
418 standard: _,
419 }: Srgb<u8> = Srgb::from_color(hsl).into();
420
421 Self::Rgb(red, green, blue)
422 }
423
424 /// Converts a `HSLuv` representation to a `Color::Rgb` instance.
425 ///
426 /// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
427 /// corresponding `Color` RGB equivalent.
428 ///
429 /// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized
430 /// by wrapping.
431 ///
432 /// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are
433 /// clamped.
434 ///
435 /// Clamping to valid ranges happens before conversion to RGB.
436 ///
437 /// # Examples
438 ///
439 /// ```
440 /// use ratatui::{palette::Hsluv, style::Color};
441 ///
442 /// // Minimum Lightness is black
443 /// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));
444 /// assert_eq!(color, Color::Rgb(0, 0, 0));
445 ///
446 /// // Maximum Lightness is white
447 /// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0));
448 /// assert_eq!(color, Color::Rgb(255, 255, 255));
449 ///
450 /// // Minimum Saturation is fully desaturated red = gray
451 /// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0));
452 /// assert_eq!(color, Color::Rgb(119, 119, 119));
453 ///
454 /// // Bright Red
455 /// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2));
456 /// assert_eq!(color, Color::Rgb(255, 0, 0));
457 ///
458 /// // Bright Blue
459 /// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3));
460 /// assert_eq!(color, Color::Rgb(0, 0, 255));
461 /// ```
462 #[cfg(feature = "palette")]
463 pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
464 use palette::{Clamp, FromColor, Srgb};
465 let hsluv = hsluv.clamp();
466 let Srgb {
467 red,
468 green,
469 blue,
470 standard: _,
471 }: Srgb<u8> = Srgb::from_color(hsluv).into();
472
473 Self::Rgb(red, green, blue)
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use std::error::Error;
480
481 #[cfg(feature = "palette")]
482 use palette::{Hsl, Hsluv};
483 use rstest::rstest;
484 #[cfg(feature = "serde")]
485 use serde::de::{Deserialize, IntoDeserializer};
486
487 use super::*;
488
489 #[cfg(feature = "palette")]
490 #[rstest]
491 #[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
492 #[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))]
493 #[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
494 #[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
495 #[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
496 #[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))]
497 #[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))]
498 #[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))]
499 #[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))]
500 #[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
501 #[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
502 #[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))]
503 #[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))]
504 #[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))]
505 #[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))]
506 #[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))]
507 #[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))]
508 fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) {
509 assert_eq!(Color::from_hsl(hsl), expected);
510 }
511
512 #[cfg(feature = "palette")]
513 #[rstest]
514 #[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
515 #[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))]
516 #[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
517 #[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))]
518 #[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))]
519 #[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))]
520 #[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))]
521 #[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))]
522 #[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))]
523 #[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
524 #[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
525 #[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))]
526 #[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))]
527 #[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))]
528 #[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))]
529 #[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))]
530 #[case::over_saturation_lightness_clamps(
531 Hsluv::new(0.0, 150.0, 150.0),
532 Color::Rgb(255, 255, 255)
533 )]
534 fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) {
535 assert_eq!(Color::from_hsluv(hsluv), expected);
536 }
537
538 #[test]
539 fn from_u32() {
540 assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));
541 assert_eq!(Color::from_u32(0xFF0000), Color::Rgb(255, 0, 0));
542 assert_eq!(Color::from_u32(0x00FF00), Color::Rgb(0, 255, 0));
543 assert_eq!(Color::from_u32(0x0000FF), Color::Rgb(0, 0, 255));
544 assert_eq!(Color::from_u32(0xFFFFFF), Color::Rgb(255, 255, 255));
545 }
546
547 #[test]
548 fn from_rgb_color() {
549 let color: Color = Color::from_str("#FF0000").unwrap();
550 assert_eq!(color, Color::Rgb(255, 0, 0));
551 }
552
553 #[test]
554 fn from_indexed_color() {
555 let color: Color = Color::from_str("10").unwrap();
556 assert_eq!(color, Color::Indexed(10));
557 }
558
559 #[test]
560 fn from_ansi_color() -> Result<(), Box<dyn Error>> {
561 assert_eq!(Color::from_str("reset")?, Color::Reset);
562 assert_eq!(Color::from_str("black")?, Color::Black);
563 assert_eq!(Color::from_str("red")?, Color::Red);
564 assert_eq!(Color::from_str("green")?, Color::Green);
565 assert_eq!(Color::from_str("yellow")?, Color::Yellow);
566 assert_eq!(Color::from_str("blue")?, Color::Blue);
567 assert_eq!(Color::from_str("magenta")?, Color::Magenta);
568 assert_eq!(Color::from_str("cyan")?, Color::Cyan);
569 assert_eq!(Color::from_str("gray")?, Color::Gray);
570 assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
571 assert_eq!(Color::from_str("lightred")?, Color::LightRed);
572 assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
573 assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
574 assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
575 assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
576 assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
577 assert_eq!(Color::from_str("white")?, Color::White);
578
579 // aliases
580 assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
581 assert_eq!(Color::from_str("lightwhite")?, Color::White);
582 assert_eq!(Color::from_str("lightgray")?, Color::White);
583
584 // silver = grey = gray
585 assert_eq!(Color::from_str("grey")?, Color::Gray);
586 assert_eq!(Color::from_str("silver")?, Color::Gray);
587
588 // spaces are ignored
589 assert_eq!(Color::from_str("light black")?, Color::DarkGray);
590 assert_eq!(Color::from_str("light white")?, Color::White);
591 assert_eq!(Color::from_str("light gray")?, Color::White);
592
593 // dashes are ignored
594 assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
595 assert_eq!(Color::from_str("light-white")?, Color::White);
596 assert_eq!(Color::from_str("light-gray")?, Color::White);
597
598 // underscores are ignored
599 assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
600 assert_eq!(Color::from_str("light_white")?, Color::White);
601 assert_eq!(Color::from_str("light_gray")?, Color::White);
602
603 // bright = light
604 assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
605 assert_eq!(Color::from_str("bright-white")?, Color::White);
606
607 // bright = light
608 assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
609 assert_eq!(Color::from_str("brightwhite")?, Color::White);
610
611 Ok(())
612 }
613
614 #[test]
615 fn from_invalid_colors() {
616 let bad_colors = [
617 "invalid_color", // not a color string
618 "abcdef0", // 7 chars is not a color
619 " bcdefa", // doesn't start with a '#'
620 "#abcdef00", // too many chars
621 "#1🦀2", // len 7 but on char boundaries shouldnt panic
622 "resett", // typo
623 "lightblackk", // typo
624 ];
625
626 for bad_color in bad_colors {
627 assert!(
628 Color::from_str(bad_color).is_err(),
629 "bad color: '{bad_color}'"
630 );
631 }
632 }
633
634 #[test]
635 fn display() {
636 assert_eq!(format!("{}", Color::Black), "Black");
637 assert_eq!(format!("{}", Color::Red), "Red");
638 assert_eq!(format!("{}", Color::Green), "Green");
639 assert_eq!(format!("{}", Color::Yellow), "Yellow");
640 assert_eq!(format!("{}", Color::Blue), "Blue");
641 assert_eq!(format!("{}", Color::Magenta), "Magenta");
642 assert_eq!(format!("{}", Color::Cyan), "Cyan");
643 assert_eq!(format!("{}", Color::Gray), "Gray");
644 assert_eq!(format!("{}", Color::DarkGray), "DarkGray");
645 assert_eq!(format!("{}", Color::LightRed), "LightRed");
646 assert_eq!(format!("{}", Color::LightGreen), "LightGreen");
647 assert_eq!(format!("{}", Color::LightYellow), "LightYellow");
648 assert_eq!(format!("{}", Color::LightBlue), "LightBlue");
649 assert_eq!(format!("{}", Color::LightMagenta), "LightMagenta");
650 assert_eq!(format!("{}", Color::LightCyan), "LightCyan");
651 assert_eq!(format!("{}", Color::White), "White");
652 assert_eq!(format!("{}", Color::Indexed(10)), "10");
653 assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
654 assert_eq!(format!("{}", Color::Reset), "Reset");
655 }
656
657 #[cfg(feature = "serde")]
658 #[test]
659 fn deserialize() -> Result<(), serde::de::value::Error> {
660 assert_eq!(
661 Color::Black,
662 Color::deserialize("Black".into_deserializer())?
663 );
664 assert_eq!(
665 Color::Magenta,
666 Color::deserialize("magenta".into_deserializer())?
667 );
668 assert_eq!(
669 Color::LightGreen,
670 Color::deserialize("LightGreen".into_deserializer())?
671 );
672 assert_eq!(
673 Color::White,
674 Color::deserialize("bright-white".into_deserializer())?
675 );
676 assert_eq!(
677 Color::Indexed(42),
678 Color::deserialize("42".into_deserializer())?
679 );
680 assert_eq!(
681 Color::Rgb(0, 255, 0),
682 Color::deserialize("#00ff00".into_deserializer())?
683 );
684 Ok(())
685 }
686
687 #[cfg(feature = "serde")]
688 #[test]
689 fn deserialize_error() {
690 let color: Result<_, serde::de::value::Error> =
691 Color::deserialize("invalid".into_deserializer());
692 assert!(color.is_err());
693
694 let color: Result<_, serde::de::value::Error> =
695 Color::deserialize("#00000000".into_deserializer());
696 assert!(color.is_err());
697 }
698
699 #[cfg(feature = "serde")]
700 #[test]
701 fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
702 let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
703 assert_eq!(json_rgb, r##""#FF00FF""##);
704 assert_eq!(
705 serde_json::from_str::<Color>(&json_rgb)?,
706 Color::Rgb(255, 0, 255)
707 );
708
709 let json_white = serde_json::to_string(&Color::White)?;
710 assert_eq!(json_white, r#""White""#);
711
712 let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
713 assert_eq!(json_indexed, r#""10""#);
714 assert_eq!(
715 serde_json::from_str::<Color>(&json_indexed)?,
716 Color::Indexed(10)
717 );
718
719 Ok(())
720 }
721
722 #[cfg(feature = "serde")]
723 #[test]
724 fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
725 assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
726 assert_eq!(
727 Color::Rgb(255, 0, 255),
728 serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
729 );
730 assert_eq!(
731 Color::Indexed(10),
732 serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
733 );
734 Ok(())
735 }
736}