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}