valence_text/
color.rs

1//! [`Color`] and related data structures.
2
3use std::fmt;
4use std::hash::Hash;
5
6use serde::de::Visitor;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use thiserror::Error;
9
10/// Text color
11#[derive(Default, Debug, PartialOrd, Eq, Ord, Clone, Copy)]
12pub enum Color {
13    /// The default color for the text will be used, which varies by context
14    /// (in some cases, it's white; in others, it's black; in still others, it
15    /// is a shade of gray that isn't normally used on text).
16    #[default]
17    Reset,
18    /// RGB Color
19    Rgb(RgbColor),
20    /// One of the 16 named Minecraft colors
21    Named(NamedColor),
22}
23
24/// RGB Color
25#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
26pub struct RgbColor {
27    /// Red channel
28    pub r: u8,
29    /// Green channel
30    pub g: u8,
31    /// Blue channel
32    pub b: u8,
33}
34
35/// Named Minecraft color
36#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
37pub enum NamedColor {
38    /// Hex digit: `0`, name: `black`
39    Black = 0,
40    /// Hex digit: `1`, name: `dark_blue`
41    DarkBlue,
42    /// Hex digit: `2`, name: `dark_green`
43    DarkGreen,
44    /// Hex digit: `3`, name: `dark_aqua`
45    DarkAqua,
46    /// Hex digit: `4`, name: `dark_red`
47    DarkRed,
48    /// Hex digit: `5`, name: `dark_purple`
49    DarkPurple,
50    /// Hex digit: `6`, name: `gold`
51    Gold,
52    /// Hex digit: `7`, name: `gray`
53    Gray,
54    /// Hex digit: `8`, name: `dark_gray`
55    DarkGray,
56    /// Hex digit: `9`, name: `blue`
57    Blue,
58    /// Hex digit: `a`, name: `green`
59    Green,
60    /// Hex digit: `b`, name: `aqua`
61    Aqua,
62    /// Hex digit: `c`, name: `red`
63    Red,
64    /// Hex digit: `d`, name: `light_purple`
65    LightPurple,
66    /// Hex digit: `e`, name: `yellow`
67    Yellow,
68    /// Hex digit: `f`, name: `white`
69    White,
70}
71
72/// Color parsing error
73#[derive(Debug, Error, PartialEq, PartialOrd, Clone, Copy, Hash, Eq, Ord)]
74#[error("invalid color name or hex code")]
75pub struct ColorError;
76
77impl Color {
78    pub const RESET: Self = Self::Reset;
79    pub const AQUA: Self = Self::Named(NamedColor::Aqua);
80    pub const BLACK: Self = Self::Named(NamedColor::Black);
81    pub const BLUE: Self = Self::Named(NamedColor::Blue);
82    pub const DARK_AQUA: Self = Self::Named(NamedColor::DarkAqua);
83    pub const DARK_BLUE: Self = Self::Named(NamedColor::DarkBlue);
84    pub const DARK_GRAY: Self = Self::Named(NamedColor::DarkGray);
85    pub const DARK_GREEN: Self = Self::Named(NamedColor::DarkGreen);
86    pub const DARK_PURPLE: Self = Self::Named(NamedColor::DarkPurple);
87    pub const DARK_RED: Self = Self::Named(NamedColor::DarkRed);
88    pub const GOLD: Self = Self::Named(NamedColor::Gold);
89    pub const GRAY: Self = Self::Named(NamedColor::Gray);
90    pub const GREEN: Self = Self::Named(NamedColor::Green);
91    pub const LIGHT_PURPLE: Self = Self::Named(NamedColor::LightPurple);
92    pub const RED: Self = Self::Named(NamedColor::Red);
93    pub const WHITE: Self = Self::Named(NamedColor::White);
94    pub const YELLOW: Self = Self::Named(NamedColor::Yellow);
95
96    /// Constructs a new RGB color
97    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
98        Self::Rgb(RgbColor::new(r, g, b))
99    }
100}
101
102impl RgbColor {
103    /// Constructs a new color from red, green, and blue components.
104    pub const fn new(r: u8, g: u8, b: u8) -> Self {
105        Self { r, g, b }
106    }
107    /// Converts the RGB color to the closest [`NamedColor`] equivalent (lossy).
108    pub fn to_named_lossy(self) -> NamedColor {
109        // calculates the squared distance between 2 colors
110        fn squared_distance(c1: RgbColor, c2: RgbColor) -> i32 {
111            (i32::from(c1.r) - i32::from(c2.r)).pow(2)
112                + (i32::from(c1.g) - i32::from(c2.g)).pow(2)
113                + (i32::from(c1.b) - i32::from(c2.b)).pow(2)
114        }
115
116        [
117            NamedColor::Aqua,
118            NamedColor::Black,
119            NamedColor::Blue,
120            NamedColor::DarkAqua,
121            NamedColor::DarkBlue,
122            NamedColor::DarkGray,
123            NamedColor::DarkGreen,
124            NamedColor::DarkPurple,
125            NamedColor::DarkRed,
126            NamedColor::Gold,
127            NamedColor::Gray,
128            NamedColor::Green,
129            NamedColor::LightPurple,
130            NamedColor::Red,
131            NamedColor::White,
132            NamedColor::Yellow,
133        ]
134        .into_iter()
135        .min_by_key(|&named| squared_distance(named.into(), self))
136        .unwrap()
137    }
138}
139
140impl NamedColor {
141    /// Returns the corresponding hex digit of the color.
142    pub const fn hex_digit(self) -> char {
143        b"0123456789abcdef"[self as usize] as char
144    }
145    /// Returns the identifier of the color.
146    pub const fn name(self) -> &'static str {
147        [
148            "black",
149            "dark_blue",
150            "dark_green",
151            "dark_aqua",
152            "dark_red",
153            "dark_purple",
154            "gold",
155            "gray",
156            "dark_gray",
157            "blue",
158            "green",
159            "aqua",
160            "red",
161            "light_purple",
162            "yellow",
163            "white",
164        ][self as usize]
165    }
166}
167
168impl PartialEq for Color {
169    fn eq(&self, other: &Self) -> bool {
170        match (*self, *other) {
171            (Self::Reset, Self::Reset) => true,
172            (Self::Rgb(rgb1), Self::Rgb(rgb2)) => rgb1 == rgb2,
173            (Self::Named(normal1), Self::Named(normal2)) => normal1 == normal2,
174            (Self::Rgb(rgb), Self::Named(normal)) | (Self::Named(normal), Self::Rgb(rgb)) => {
175                rgb == RgbColor::from(normal)
176            }
177            (Self::Reset, _) | (_, Self::Reset) => false,
178        }
179    }
180}
181
182impl Hash for Color {
183    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
184        match self {
185            Self::Reset => state.write_u8(0),
186            Self::Rgb(rgb) => {
187                state.write_u8(1);
188                rgb.hash(state);
189            }
190            Self::Named(normal) => {
191                state.write_u8(1);
192                RgbColor::from(*normal).hash(state);
193            }
194        }
195    }
196}
197
198impl From<NamedColor> for RgbColor {
199    fn from(value: NamedColor) -> Self {
200        match value {
201            NamedColor::Aqua => Self::new(85, 255, 255),
202            NamedColor::Black => Self::new(0, 0, 0),
203            NamedColor::Blue => Self::new(85, 85, 255),
204            NamedColor::DarkAqua => Self::new(0, 170, 170),
205            NamedColor::DarkBlue => Self::new(0, 0, 170),
206            NamedColor::DarkGray => Self::new(85, 85, 85),
207            NamedColor::DarkGreen => Self::new(0, 170, 0),
208            NamedColor::DarkPurple => Self::new(170, 0, 170),
209            NamedColor::DarkRed => Self::new(170, 0, 0),
210            NamedColor::Gold => Self::new(255, 170, 0),
211            NamedColor::Gray => Self::new(170, 170, 170),
212            NamedColor::Green => Self::new(85, 255, 85),
213            NamedColor::LightPurple => Self::new(255, 85, 255),
214            NamedColor::Red => Self::new(255, 85, 85),
215            NamedColor::White => Self::new(255, 255, 255),
216            NamedColor::Yellow => Self::new(255, 255, 85),
217        }
218    }
219}
220
221impl From<RgbColor> for Color {
222    fn from(value: RgbColor) -> Self {
223        Self::Rgb(value)
224    }
225}
226
227impl From<NamedColor> for Color {
228    fn from(value: NamedColor) -> Self {
229        Self::Named(value)
230    }
231}
232
233impl TryFrom<&str> for Color {
234    type Error = ColorError;
235
236    fn try_from(value: &str) -> Result<Self, Self::Error> {
237        if value.starts_with('#') {
238            return Ok(Self::Rgb(RgbColor::try_from(value)?));
239        }
240
241        if value == "reset" {
242            return Ok(Self::Reset);
243        }
244
245        Ok(Self::Named(NamedColor::try_from(value)?))
246    }
247}
248
249impl TryFrom<&str> for NamedColor {
250    type Error = ColorError;
251
252    fn try_from(value: &str) -> Result<Self, Self::Error> {
253        match value {
254            "black" => Ok(NamedColor::Black),
255            "dark_blue" => Ok(NamedColor::DarkBlue),
256            "dark_green" => Ok(NamedColor::DarkGreen),
257            "dark_aqua" => Ok(NamedColor::DarkAqua),
258            "dark_red" => Ok(NamedColor::DarkRed),
259            "dark_purple" => Ok(NamedColor::DarkPurple),
260            "gold" => Ok(NamedColor::Gold),
261            "gray" => Ok(NamedColor::Gray),
262            "dark_gray" => Ok(NamedColor::DarkGray),
263            "blue" => Ok(NamedColor::Blue),
264            "green" => Ok(NamedColor::Green),
265            "aqua" => Ok(NamedColor::Aqua),
266            "red" => Ok(NamedColor::Red),
267            "light_purple" => Ok(NamedColor::LightPurple),
268            "yellow" => Ok(NamedColor::Yellow),
269            "white" => Ok(NamedColor::White),
270            _ => Err(ColorError),
271        }
272    }
273}
274
275impl TryFrom<&str> for RgbColor {
276    type Error = ColorError;
277
278    fn try_from(value: &str) -> Result<Self, Self::Error> {
279        let to_num = |d| match d {
280            b'0'..=b'9' => Ok(d - b'0'),
281            b'a'..=b'f' => Ok(d - b'a' + 0xa),
282            b'A'..=b'F' => Ok(d - b'A' + 0xa),
283            _ => Err(ColorError),
284        };
285
286        if let &[b'#', r0, r1, g0, g1, b0, b1] = value.as_bytes() {
287            Ok(RgbColor {
288                r: (to_num(r0)? << 4) | to_num(r1)?,
289                g: (to_num(g0)? << 4) | to_num(g1)?,
290                b: (to_num(b0)? << 4) | to_num(b1)?,
291            })
292        } else {
293            Err(ColorError)
294        }
295    }
296}
297
298impl Serialize for Color {
299    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
300        format!("{self}").serialize(serializer)
301    }
302}
303
304impl<'de> Deserialize<'de> for Color {
305    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
306        deserializer.deserialize_str(ColorVisitor)
307    }
308}
309
310struct ColorVisitor;
311
312impl Visitor<'_> for ColorVisitor {
313    type Value = Color;
314
315    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
316        write!(f, "a hex color (#rrggbb), a normal color or 'reset'")
317    }
318
319    fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
320        Color::try_from(s).map_err(|_| E::custom("invalid color"))
321    }
322}
323
324impl fmt::Display for Color {
325    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
326        match self {
327            Color::Reset => write!(f, "reset"),
328            Color::Rgb(rgb) => rgb.fmt(f),
329            Color::Named(normal) => normal.fmt(f),
330        }
331    }
332}
333
334impl fmt::Display for RgbColor {
335    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
336        write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
337    }
338}
339
340impl fmt::Display for NamedColor {
341    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
342        write!(f, "{}", self.name())
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn colors() {
352        assert_eq!(
353            Color::try_from("#aBcDeF"),
354            Ok(RgbColor::new(0xab, 0xcd, 0xef).into())
355        );
356        assert_eq!(
357            Color::try_from("#fFfFfF"),
358            Ok(RgbColor::new(255, 255, 255).into())
359        );
360        assert_eq!(Color::try_from("#000000"), Ok(NamedColor::Black.into()));
361        assert_eq!(Color::try_from("red"), Ok(NamedColor::Red.into()));
362        assert_eq!(Color::try_from("blue"), Ok(NamedColor::Blue.into()));
363        assert!(Color::try_from("#ffTf00").is_err());
364        assert!(Color::try_from("#ffš00").is_err());
365        assert!(Color::try_from("#00000000").is_err());
366        assert!(Color::try_from("#").is_err());
367    }
368}