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}