Skip to main content

matrix_rain/
theme.rs

1use ratatui::style::Color;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub enum Theme {
5    ClassicGreen,
6    Amber,
7    Cyan,
8    Red,
9    Rainbow,
10    Custom(ColorRamp),
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct ColorRamp {
15    pub head: Color,
16    pub bright: Color,
17    pub mid: Color,
18    pub dim: Color,
19    pub fade: Color,
20}
21
22const CLASSIC_GREEN: ColorRamp = ColorRamp {
23    head: Color::Rgb(0xFF, 0xFF, 0xFF),
24    bright: Color::Rgb(0xCC, 0xFF, 0xCC),
25    mid: Color::Rgb(0x00, 0xFF, 0x00),
26    dim: Color::Rgb(0x00, 0x99, 0x00),
27    fade: Color::Rgb(0x00, 0x33, 0x00),
28};
29
30const AMBER: ColorRamp = ColorRamp {
31    head: Color::Rgb(0xFF, 0xFF, 0xFF),
32    bright: Color::Rgb(0xFF, 0xE5, 0xB4),
33    mid: Color::Rgb(0xFF, 0xAA, 0x00),
34    dim: Color::Rgb(0xB3, 0x6B, 0x00),
35    fade: Color::Rgb(0x4D, 0x2E, 0x00),
36};
37
38const CYAN: ColorRamp = ColorRamp {
39    head: Color::Rgb(0xFF, 0xFF, 0xFF),
40    bright: Color::Rgb(0xCC, 0xFF, 0xFF),
41    mid: Color::Rgb(0x00, 0xFF, 0xFF),
42    dim: Color::Rgb(0x00, 0x88, 0x99),
43    fade: Color::Rgb(0x00, 0x22, 0x33),
44};
45
46const RED: ColorRamp = ColorRamp {
47    head: Color::Rgb(0xFF, 0xFF, 0xFF),
48    bright: Color::Rgb(0xFF, 0xCC, 0xCC),
49    mid: Color::Rgb(0xFF, 0x33, 0x00),
50    dim: Color::Rgb(0x99, 0x11, 0x00),
51    fade: Color::Rgb(0x33, 0x00, 0x00),
52};
53
54// Rainbow: 4 distinct hues across the trail with a white head. The smooth-interpolation
55// path will lerp between adjacent stops, producing a vertical hue gradient inside each
56// drop. On 256-color / 16-color terminals the 5-stop quantisation still reads as colorful.
57const RAINBOW: ColorRamp = ColorRamp {
58    head: Color::Rgb(0xFF, 0xFF, 0xFF),
59    bright: Color::Rgb(0xFF, 0x00, 0x00),
60    mid: Color::Rgb(0xFF, 0xFF, 0x00),
61    dim: Color::Rgb(0x00, 0xFF, 0x00),
62    fade: Color::Rgb(0x00, 0x66, 0xFF),
63};
64
65impl Theme {
66    pub(crate) fn ramp(&self) -> ColorRamp {
67        match self {
68            Self::ClassicGreen => CLASSIC_GREEN,
69            Self::Amber => AMBER,
70            Self::Cyan => CYAN,
71            Self::Red => RED,
72            Self::Rainbow => RAINBOW,
73            Self::Custom(ramp) => *ramp,
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn assert_distinct_stops(theme: Theme) {
83        let r = theme.ramp();
84        let stops = [r.head, r.bright, r.mid, r.dim, r.fade];
85        for i in 0..stops.len() {
86            for j in (i + 1)..stops.len() {
87                assert_ne!(stops[i], stops[j], "{theme:?}: stops {i} and {j} collide");
88            }
89        }
90    }
91
92    fn assert_white_head(theme: Theme) {
93        assert_eq!(theme.ramp().head, Color::Rgb(0xFF, 0xFF, 0xFF));
94    }
95
96    #[test]
97    fn classic_green_ramp() {
98        assert_white_head(Theme::ClassicGreen);
99        assert_distinct_stops(Theme::ClassicGreen);
100    }
101
102    #[test]
103    fn amber_ramp() {
104        assert_white_head(Theme::Amber);
105        assert_distinct_stops(Theme::Amber);
106    }
107
108    #[test]
109    fn cyan_ramp() {
110        assert_white_head(Theme::Cyan);
111        assert_distinct_stops(Theme::Cyan);
112    }
113
114    #[test]
115    fn red_ramp() {
116        assert_white_head(Theme::Red);
117        assert_distinct_stops(Theme::Red);
118    }
119
120    #[test]
121    fn rainbow_ramp_has_diverse_hues() {
122        assert_white_head(Theme::Rainbow);
123        assert_distinct_stops(Theme::Rainbow);
124        // Sanity: rainbow's mid/dim/fade should span the hue wheel — no two share dominant channel.
125        let r = Theme::Rainbow.ramp();
126        let channels = |c: Color| match c {
127            Color::Rgb(r, g, b) => (r, g, b),
128            _ => panic!("expected Rgb"),
129        };
130        let (mr, mg, _) = channels(r.mid);
131        let (_, dg, _) = channels(r.dim);
132        assert!(mr >= 0x80 && mg >= 0x80, "mid should be warm");
133        assert!(dg >= 0x80, "dim should have strong green");
134    }
135
136    #[test]
137    fn custom_passthrough() {
138        let ramp = ColorRamp {
139            head: Color::Red,
140            bright: Color::LightRed,
141            mid: Color::Yellow,
142            dim: Color::DarkGray,
143            fade: Color::Black,
144        };
145        assert_eq!(Theme::Custom(ramp).ramp(), ramp);
146    }
147}