1use ratatui::style::Color;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum Theme {
22 ClassicGreen,
25 Amber,
27 Cyan,
29 Red,
31 Rainbow,
35 Custom(ColorRamp),
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct ColorRamp {
68 pub head: Color,
70 pub bright: Color,
73 pub mid: Color,
75 pub dim: Color,
77 pub fade: Color,
79}
80
81const CLASSIC_GREEN: ColorRamp = ColorRamp {
82 head: Color::Rgb(0xFF, 0xFF, 0xFF),
83 bright: Color::Rgb(0xCC, 0xFF, 0xCC),
84 mid: Color::Rgb(0x00, 0xFF, 0x00),
85 dim: Color::Rgb(0x00, 0x99, 0x00),
86 fade: Color::Rgb(0x00, 0x33, 0x00),
87};
88
89const AMBER: ColorRamp = ColorRamp {
90 head: Color::Rgb(0xFF, 0xFF, 0xFF),
91 bright: Color::Rgb(0xFF, 0xE5, 0xB4),
92 mid: Color::Rgb(0xFF, 0xAA, 0x00),
93 dim: Color::Rgb(0xB3, 0x6B, 0x00),
94 fade: Color::Rgb(0x4D, 0x2E, 0x00),
95};
96
97const CYAN: ColorRamp = ColorRamp {
98 head: Color::Rgb(0xFF, 0xFF, 0xFF),
99 bright: Color::Rgb(0xCC, 0xFF, 0xFF),
100 mid: Color::Rgb(0x00, 0xFF, 0xFF),
101 dim: Color::Rgb(0x00, 0x88, 0x99),
102 fade: Color::Rgb(0x00, 0x22, 0x33),
103};
104
105const RED: ColorRamp = ColorRamp {
106 head: Color::Rgb(0xFF, 0xFF, 0xFF),
107 bright: Color::Rgb(0xFF, 0xCC, 0xCC),
108 mid: Color::Rgb(0xFF, 0x33, 0x00),
109 dim: Color::Rgb(0x99, 0x11, 0x00),
110 fade: Color::Rgb(0x33, 0x00, 0x00),
111};
112
113const RAINBOW: ColorRamp = ColorRamp {
117 head: Color::Rgb(0xFF, 0xFF, 0xFF),
118 bright: Color::Rgb(0xFF, 0x00, 0x00),
119 mid: Color::Rgb(0xFF, 0xFF, 0x00),
120 dim: Color::Rgb(0x00, 0xFF, 0x00),
121 fade: Color::Rgb(0x00, 0x66, 0xFF),
122};
123
124impl Theme {
125 pub(crate) fn ramp(&self) -> ColorRamp {
126 match self {
127 Self::ClassicGreen => CLASSIC_GREEN,
128 Self::Amber => AMBER,
129 Self::Cyan => CYAN,
130 Self::Red => RED,
131 Self::Rainbow => RAINBOW,
132 Self::Custom(ramp) => *ramp,
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn assert_distinct_stops(theme: Theme) {
142 let r = theme.ramp();
143 let stops = [r.head, r.bright, r.mid, r.dim, r.fade];
144 for i in 0..stops.len() {
145 for j in (i + 1)..stops.len() {
146 assert_ne!(stops[i], stops[j], "{theme:?}: stops {i} and {j} collide");
147 }
148 }
149 }
150
151 fn assert_white_head(theme: Theme) {
152 assert_eq!(theme.ramp().head, Color::Rgb(0xFF, 0xFF, 0xFF));
153 }
154
155 #[test]
156 fn classic_green_ramp() {
157 assert_white_head(Theme::ClassicGreen);
158 assert_distinct_stops(Theme::ClassicGreen);
159 }
160
161 #[test]
162 fn amber_ramp() {
163 assert_white_head(Theme::Amber);
164 assert_distinct_stops(Theme::Amber);
165 }
166
167 #[test]
168 fn cyan_ramp() {
169 assert_white_head(Theme::Cyan);
170 assert_distinct_stops(Theme::Cyan);
171 }
172
173 #[test]
174 fn red_ramp() {
175 assert_white_head(Theme::Red);
176 assert_distinct_stops(Theme::Red);
177 }
178
179 #[test]
180 fn rainbow_ramp_has_diverse_hues() {
181 assert_white_head(Theme::Rainbow);
182 assert_distinct_stops(Theme::Rainbow);
183 let r = Theme::Rainbow.ramp();
185 let channels = |c: Color| match c {
186 Color::Rgb(r, g, b) => (r, g, b),
187 _ => panic!("expected Rgb"),
188 };
189 let (mr, mg, _) = channels(r.mid);
190 let (_, dg, _) = channels(r.dim);
191 assert!(mr >= 0x80 && mg >= 0x80, "mid should be warm");
192 assert!(dg >= 0x80, "dim should have strong green");
193 }
194
195 #[test]
196 fn custom_passthrough() {
197 let ramp = ColorRamp {
198 head: Color::Red,
199 bright: Color::LightRed,
200 mid: Color::Yellow,
201 dim: Color::DarkGray,
202 fade: Color::Black,
203 };
204 assert_eq!(Theme::Custom(ramp).ramp(), ramp);
205 }
206}