Skip to main content

unicode_plot/
color.rs

1use std::ops::BitOr;
2
3/// A 3-bit canvas-level color index used for pixel compositing.
4///
5/// Values range from 0 (`NORMAL`) to 7 (`WHITE`). Colors compose additively
6/// via [`BitOr`]: for example, `BLUE | RED` yields `MAGENTA`.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[repr(transparent)]
9pub struct CanvasColor(u8);
10
11impl CanvasColor {
12    /// No color (default foreground).
13    pub const NORMAL: Self = Self(0);
14    /// Blue (bit 0).
15    pub const BLUE: Self = Self(1);
16    /// Red (bit 1).
17    pub const RED: Self = Self(2);
18    /// Magenta (blue | red).
19    pub const MAGENTA: Self = Self(3);
20    /// Green (bit 2).
21    pub const GREEN: Self = Self(4);
22    /// Cyan (blue | green).
23    pub const CYAN: Self = Self(5);
24    /// Yellow (red | green).
25    pub const YELLOW: Self = Self(6);
26    /// White (all bits set).
27    pub const WHITE: Self = Self(7);
28
29    /// Creates a canvas color from a raw index. Returns `None` if `value > 7`.
30    #[must_use]
31    pub const fn new(value: u8) -> Option<Self> {
32        if value <= Self::WHITE.0 {
33            Some(Self(value))
34        } else {
35            None
36        }
37    }
38
39    /// Returns the underlying `u8` index.
40    #[must_use]
41    pub const fn as_u8(self) -> u8 {
42        self.0
43    }
44}
45
46impl Default for CanvasColor {
47    fn default() -> Self {
48        Self::NORMAL
49    }
50}
51
52impl BitOr for CanvasColor {
53    type Output = Self;
54
55    fn bitor(self, rhs: Self) -> Self::Output {
56        Self((self.0 | rhs.0) & Self::WHITE.0)
57    }
58}
59
60/// Terminal color specification for plot labels, annotations, and series.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum TermColor {
64    /// One of the standard 16 named ANSI colors.
65    Named(NamedColor),
66    /// An extended 256-color palette index.
67    Ansi256(u8),
68    /// A 24-bit true-color RGB value.
69    Rgb(u8, u8, u8),
70}
71
72/// Standard named terminal colors, including the eight bright/light variants.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum NamedColor {
76    Black,
77    Red,
78    Green,
79    Yellow,
80    Blue,
81    Magenta,
82    Cyan,
83    White,
84    /// Bright black (ANSI 90). Alias: `Gray`.
85    LightBlack,
86    /// Same as `LightBlack` (ANSI 90).
87    Gray,
88    LightRed,
89    LightGreen,
90    LightYellow,
91    LightBlue,
92    LightMagenta,
93    LightCyan,
94}
95
96/// Controls whether ANSI color escapes are emitted during rendering.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
98#[non_exhaustive]
99pub enum ColorMode {
100    /// Emit color only when the output writer is a terminal.
101    #[default]
102    Auto,
103    /// Always emit ANSI color escapes.
104    Always,
105    /// Never emit ANSI color escapes; produce plain text.
106    Never,
107}
108
109/// The default series color cycle used by [`Plot::next_color`](crate::Plot::next_color).
110///
111/// Matches the Ruby `UnicodePlot` auto-color sequence: green, blue, red,
112/// magenta, yellow, cyan.
113/// Maps a [`TermColor`] to the closest [`CanvasColor`] for pixel compositing.
114///
115/// Only the seven primary named colors have direct canvas equivalents. All
116/// other terminal colors (bright variants, 256-color, RGB) map to
117/// [`CanvasColor::NORMAL`].
118#[must_use]
119pub(crate) fn canvas_color_from_term(color: TermColor) -> CanvasColor {
120    match color {
121        TermColor::Named(NamedColor::Blue) => CanvasColor::BLUE,
122        TermColor::Named(NamedColor::Red) => CanvasColor::RED,
123        TermColor::Named(NamedColor::Magenta) => CanvasColor::MAGENTA,
124        TermColor::Named(NamedColor::Green) => CanvasColor::GREEN,
125        TermColor::Named(NamedColor::Cyan) => CanvasColor::CYAN,
126        TermColor::Named(NamedColor::Yellow) => CanvasColor::YELLOW,
127        TermColor::Named(NamedColor::White) => CanvasColor::WHITE,
128        _ => CanvasColor::NORMAL,
129    }
130}
131
132/// The default series color cycle used by [`Plot::next_color`](crate::Plot::next_color).
133///
134/// Matches the Ruby `UnicodePlot` auto-color sequence: green, blue, red,
135/// magenta, yellow, cyan.
136pub const AUTO_SERIES_COLORS: [NamedColor; 6] = [
137    NamedColor::Green,
138    NamedColor::Blue,
139    NamedColor::Red,
140    NamedColor::Magenta,
141    NamedColor::Yellow,
142    NamedColor::Cyan,
143];
144
145#[cfg(test)]
146mod tests {
147    use super::{AUTO_SERIES_COLORS, CanvasColor, NamedColor};
148
149    #[test]
150    fn canvas_color_additive_blending_matches_bitwise_or() {
151        for lhs in 0_u8..=7 {
152            for rhs in 0_u8..=7 {
153                let left = CanvasColor::new(lhs);
154                let right = CanvasColor::new(rhs);
155
156                assert!(left.is_some() && right.is_some(), "lhs={lhs} rhs={rhs}");
157
158                if let (Some(left), Some(right)) = (left, right) {
159                    let blended = left | right;
160                    assert_eq!(blended.as_u8(), lhs | rhs, "lhs={lhs} rhs={rhs}");
161                }
162            }
163        }
164    }
165
166    #[test]
167    fn canvas_color_constants_match_reference_indices() {
168        assert_eq!(CanvasColor::NORMAL.as_u8(), 0);
169        assert_eq!(CanvasColor::BLUE.as_u8(), 1);
170        assert_eq!(CanvasColor::RED.as_u8(), 2);
171        assert_eq!(CanvasColor::MAGENTA.as_u8(), 3);
172        assert_eq!(CanvasColor::GREEN.as_u8(), 4);
173        assert_eq!(CanvasColor::CYAN.as_u8(), 5);
174        assert_eq!(CanvasColor::YELLOW.as_u8(), 6);
175        assert_eq!(CanvasColor::WHITE.as_u8(), 7);
176    }
177
178    #[test]
179    fn canvas_color_new_rejects_out_of_range_values() {
180        assert_eq!(CanvasColor::new(8), None);
181        assert_eq!(CanvasColor::new(u8::MAX), None);
182    }
183
184    #[test]
185    fn canvas_color_semantic_blending_examples() {
186        assert_eq!(CanvasColor::BLUE | CanvasColor::RED, CanvasColor::MAGENTA);
187        assert_eq!(CanvasColor::NORMAL | CanvasColor::GREEN, CanvasColor::GREEN);
188    }
189
190    #[test]
191    fn auto_series_colors_match_reference_cycle() {
192        assert_eq!(
193            AUTO_SERIES_COLORS,
194            [
195                NamedColor::Green,
196                NamedColor::Blue,
197                NamedColor::Red,
198                NamedColor::Magenta,
199                NamedColor::Yellow,
200                NamedColor::Cyan,
201            ]
202        );
203    }
204}