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
54const 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 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}