Skip to main content

retroglyph/
color.rs

1//! Styling types for character cells.
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
4/// Standard 16-color ANSI palette.
5///
6/// Prefer `Ansi` colors when you want your game to respect the user's
7/// terminal color theme (e.g., Solarized, Nord, or custom themes).
8/// Use `Rgb` for fixed colors that must appear identical regardless of
9/// the user's terminal configuration.
10pub enum AnsiColor {
11    #[default]
12    /// Black.
13    Black = 0,
14    /// Red.
15    Red,
16    /// Green.
17    Green,
18    /// Yellow.
19    Yellow,
20    /// Blue.
21    Blue,
22    /// Magenta.
23    Magenta,
24    /// Cyan.
25    Cyan,
26    /// White.
27    White,
28    /// Bright Black.
29    BrightBlack,
30    /// Bright Red.
31    BrightRed,
32    /// Bright Green.
33    BrightGreen,
34    /// Bright Yellow.
35    BrightYellow,
36    /// Bright Blue.
37    BrightBlue,
38    /// Bright Magenta.
39    BrightMagenta,
40    /// Bright Cyan.
41    BrightCyan,
42    /// Bright White.
43    BrightWhite,
44}
45
46impl AnsiColor {
47    /// Returns the ANSI color code as a `u8` index.
48    #[must_use]
49    pub const fn to_index(self) -> u8 {
50        self as u8
51    }
52}
53
54/// Error returned when a `u8` value has no corresponding [`AnsiColor`].
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct InvalidAnsiIndex(pub u8);
57
58impl core::fmt::Display for InvalidAnsiIndex {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        write!(f, "invalid ANSI color index: {}", self.0)
61    }
62}
63
64impl TryFrom<u8> for AnsiColor {
65    type Error = InvalidAnsiIndex;
66
67    fn try_from(v: u8) -> Result<Self, Self::Error> {
68        match v {
69            0 => Ok(Self::Black),
70            1 => Ok(Self::Red),
71            2 => Ok(Self::Green),
72            3 => Ok(Self::Yellow),
73            4 => Ok(Self::Blue),
74            5 => Ok(Self::Magenta),
75            6 => Ok(Self::Cyan),
76            7 => Ok(Self::White),
77            8 => Ok(Self::BrightBlack),
78            9 => Ok(Self::BrightRed),
79            10 => Ok(Self::BrightGreen),
80            11 => Ok(Self::BrightYellow),
81            12 => Ok(Self::BrightBlue),
82            13 => Ok(Self::BrightMagenta),
83            14 => Ok(Self::BrightCyan),
84            15 => Ok(Self::BrightWhite),
85            _ => Err(InvalidAnsiIndex(v)),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
91/// Represents a color in the terminal grid.
92pub enum Color {
93    #[default]
94    /// Backend's default foreground/background color.
95    ///
96    /// This tells the rendering backend to use the terminal's configured
97    /// default colors (e.g., the user's background color preference).
98    Default,
99    /// One of the 16 standard ANSI colors.
100    ///
101    /// Use these to respect the user's terminal theme.
102    Ansi(AnsiColor),
103    /// 256-color palette index.
104    Indexed(u8),
105    /// 24-bit RGB color.
106    ///
107    /// Use this for exact color matching regardless of terminal settings.
108    Rgb {
109        /// Red channel.
110        r: u8,
111        /// Green channel.
112        g: u8,
113        /// Blue channel.
114        b: u8,
115    },
116}
117
118impl Color {
119    /// Standard Black (ANSI).
120    pub const BLACK: Self = Self::Ansi(AnsiColor::Black);
121    /// Standard Red (ANSI).
122    pub const RED: Self = Self::Ansi(AnsiColor::Red);
123    /// Standard Green (ANSI).
124    pub const GREEN: Self = Self::Ansi(AnsiColor::Green);
125    /// Standard Yellow (ANSI).
126    pub const YELLOW: Self = Self::Ansi(AnsiColor::Yellow);
127    /// Standard Blue (ANSI).
128    pub const BLUE: Self = Self::Ansi(AnsiColor::Blue);
129    /// Standard Magenta (ANSI).
130    pub const MAGENTA: Self = Self::Ansi(AnsiColor::Magenta);
131    /// Standard Cyan (ANSI).
132    pub const CYAN: Self = Self::Ansi(AnsiColor::Cyan);
133    /// Standard White (ANSI).
134    pub const WHITE: Self = Self::Ansi(AnsiColor::White);
135    /// Bright Black / dark grey (ANSI).
136    pub const BRIGHT_BLACK: Self = Self::Ansi(AnsiColor::BrightBlack);
137    /// Bright Red (ANSI).
138    pub const BRIGHT_RED: Self = Self::Ansi(AnsiColor::BrightRed);
139    /// Bright Green (ANSI).
140    pub const BRIGHT_GREEN: Self = Self::Ansi(AnsiColor::BrightGreen);
141    /// Bright Yellow (ANSI).
142    pub const BRIGHT_YELLOW: Self = Self::Ansi(AnsiColor::BrightYellow);
143    /// Bright Blue (ANSI).
144    pub const BRIGHT_BLUE: Self = Self::Ansi(AnsiColor::BrightBlue);
145    /// Bright Magenta (ANSI).
146    pub const BRIGHT_MAGENTA: Self = Self::Ansi(AnsiColor::BrightMagenta);
147    /// Bright Cyan (ANSI).
148    pub const BRIGHT_CYAN: Self = Self::Ansi(AnsiColor::BrightCyan);
149    /// Bright White (ANSI).
150    pub const BRIGHT_WHITE: Self = Self::Ansi(AnsiColor::BrightWhite);
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_color_defaults() {
159        assert_eq!(Color::default(), Color::Default);
160    }
161
162    #[test]
163    fn test_ansi_values() {
164        assert_eq!(AnsiColor::Red as u8, 1);
165        assert_eq!(AnsiColor::BrightWhite as u8, 15);
166    }
167
168    #[test]
169    fn test_ansi_try_from_roundtrip() {
170        for i in 0u8..16 {
171            let color = AnsiColor::try_from(i).expect("should be valid");
172            assert_eq!(color.to_index(), i);
173        }
174    }
175
176    #[test]
177    fn test_ansi_try_from_invalid() {
178        assert_eq!(AnsiColor::try_from(16), Err(InvalidAnsiIndex(16)));
179        assert_eq!(AnsiColor::try_from(255), Err(InvalidAnsiIndex(255)));
180    }
181}