chunkedge_text/
color.rs

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