parse_style/
rgbcolor.rs

1use super::ParseColorError;
2use crate::color::Color;
3use crate::style::Style;
4use crate::util::strip_nocase_prefix;
5use std::fmt;
6
7/// A 24-bit color composed of red, green, and blue components
8///
9/// An `RgbColor` value can be [parsed][std::str::FromStr] from a string
10/// consisting of `'#'` followed by six hexadecimal digits or from a string
11/// of the form `"rgb({red},{blue},{green})"` where the individual components
12/// are decimal integers from 0 through 255.
13///
14/// `RgbColor` values are [displayed][std::fmt::Display] as strings consisting
15/// of `'#'` followed by six lowercase hexadecimal digits.
16///
17/// # Examples
18///
19/// ```
20/// use parse_style::RgbColor;
21///
22/// assert_eq!("#e99695".parse::<RgbColor>().unwrap(), RgbColor(233, 150, 149));
23/// assert_eq!("rgb(233,150,149)".parse::<RgbColor>().unwrap(), RgbColor(233, 150, 149));
24///
25/// assert_eq!(RgbColor(233, 150, 149).to_string(), "#e99695");
26/// ```
27#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
28pub struct RgbColor(
29    /// Red
30    pub u8,
31    /// Green
32    pub u8,
33    /// Blue
34    pub u8,
35);
36
37impl RgbColor {
38    /// Return the red component
39    pub fn red(self) -> u8 {
40        self.0
41    }
42
43    /// Return the green component
44    pub fn green(self) -> u8 {
45        self.1
46    }
47
48    /// Return the blue component
49    pub fn blue(self) -> u8 {
50        self.2
51    }
52
53    /// Return a new [`Style`] that uses this color as the foreground color
54    pub fn as_foreground(self) -> Style {
55        Style::new().foreground(Some(self.into()))
56    }
57
58    /// Return a new [`Style`] that uses this color as the background color
59    pub fn as_background(self) -> Style {
60        Style::new().background(Some(self.into()))
61    }
62
63    /// Return a new [`Style`] that uses this color as the foreground color and
64    /// `bg` as the background color
65    pub fn on<C: Into<Color>>(self, bg: C) -> Style {
66        Style::new()
67            .foreground(Some(self.into()))
68            .background(Some(bg.into()))
69    }
70}
71
72impl From<(u8, u8, u8)> for RgbColor {
73    fn from(value: (u8, u8, u8)) -> RgbColor {
74        RgbColor(value.0, value.1, value.2)
75    }
76}
77
78impl From<RgbColor> for (u8, u8, u8) {
79    fn from(value: RgbColor) -> (u8, u8, u8) {
80        (value.0, value.1, value.2)
81    }
82}
83
84#[cfg(feature = "anstyle")]
85#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
86impl From<RgbColor> for anstyle::RgbColor {
87    /// Convert an `RgbColor` to an [`anstyle::RgbColor`]
88    fn from(value: RgbColor) -> anstyle::RgbColor {
89        anstyle::RgbColor(value.0, value.1, value.2)
90    }
91}
92
93#[cfg(feature = "anstyle")]
94#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
95impl From<anstyle::RgbColor> for RgbColor {
96    /// Convert an [`anstyle::RgbColor`] to a `RgbColor`
97    fn from(value: anstyle::RgbColor) -> RgbColor {
98        RgbColor(value.0, value.1, value.2)
99    }
100}
101
102#[cfg(feature = "crossterm")]
103#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
104impl From<RgbColor> for crossterm::style::Color {
105    /// Convert an `RgbColor` to a [`crossterm::style::Color`]
106    fn from(value: RgbColor) -> crossterm::style::Color {
107        crossterm::style::Color::Rgb {
108            r: value.0,
109            g: value.1,
110            b: value.2,
111        }
112    }
113}
114
115#[cfg(feature = "ratatui")]
116#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
117impl From<RgbColor> for ratatui::style::Color {
118    /// Convert an `RgbColor` to a [`ratatui::style::Color`]
119    fn from(value: RgbColor) -> ratatui::style::Color {
120        ratatui::style::Color::Rgb(value.0, value.1, value.2)
121    }
122}
123
124impl fmt::Display for RgbColor {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
127    }
128}
129
130impl std::str::FromStr for RgbColor {
131    type Err = ParseColorError;
132
133    fn from_str(s: &str) -> Result<RgbColor, ParseColorError> {
134        if let Some(hex) = s
135            .strip_prefix('#')
136            .filter(|s| s.chars().all(|c| c.is_ascii_hexdigit()) && s.len() == 6)
137        {
138            let red = u8::from_str_radix(&hex[..2], 16).expect("should be valid hex string");
139            let green = u8::from_str_radix(&hex[2..4], 16).expect("should be valid hex string");
140            let blue = u8::from_str_radix(&hex[4..], 16).expect("should be valid hex string");
141            Ok(RgbColor(red, green, blue))
142        } else if let Some(dec) = strip_nocase_prefix(s, "rgb(").and_then(|s| s.strip_suffix(')')) {
143            let mut rgb = dec.split(',').map(str::parse::<u8>);
144            let red = rgb.next();
145            let green = rgb.next();
146            let blue = rgb.next();
147            let rest = rgb.next();
148            if let (Some(Ok(red)), Some(Ok(green)), Some(Ok(blue)), None) = (red, green, blue, rest)
149            {
150                Ok(RgbColor(red, green, blue))
151            } else {
152                Err(ParseColorError(s.to_owned()))
153            }
154        } else {
155            Err(ParseColorError(s.to_owned()))
156        }
157    }
158}
159
160#[cfg(feature = "serde")]
161#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
162impl serde::Serialize for RgbColor {
163    fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
164        serializer.collect_str(self)
165    }
166}
167
168#[cfg(feature = "serde")]
169#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
170impl<'de> serde::Deserialize<'de> for RgbColor {
171    fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
172        struct Visitor;
173
174        impl serde::de::Visitor<'_> for Visitor {
175            type Value = RgbColor;
176
177            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178                f.write_str(r##"a string of the form "rgb(INT,INT,INT)" or "#xxxxxx""##)
179            }
180
181            fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
182            where
183                E: serde::de::Error,
184            {
185                input
186                    .parse::<RgbColor>()
187                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(input), &self))
188            }
189        }
190
191        deserializer.deserialize_str(Visitor)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use rstest::rstest;
199
200    #[test]
201    fn test_display() {
202        assert_eq!(RgbColor(0x7F, 0xFF, 0x00).to_string(), "#7fff00");
203    }
204
205    #[rstest]
206    #[case("#7fff00", RgbColor(0x7F, 0xFF, 0x00))]
207    #[case("#7FFF00", RgbColor(0x7F, 0xFF, 0x00))]
208    #[case("rgb(78,126,70)", RgbColor(78, 126, 70))]
209    #[case("RGB(78,126,70)", RgbColor(78, 126, 70))]
210    fn test_parse(#[case] s: &str, #[case] color: RgbColor) {
211        assert_eq!(s.parse::<RgbColor>().unwrap(), color);
212    }
213
214    #[rstest]
215    #[case("7fff00")]
216    #[case("# 7fff00")]
217    #[case("#000")]
218    #[case("rgb(78, 126, 70)")]
219    #[case("rgb(78,126)")]
220    #[case("rgb(78,126,70,0)")]
221    #[case("rgb(0x7f,0xff,0x00)")]
222    fn test_parse_err(#[case] s: &str) {
223        assert!(s.parse::<RgbColor>().is_err());
224    }
225}