ratatui_core/style/
color.rs

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