1use ratatui::style::Color;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
38#[cfg_attr(feature = "theme-serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct ColorPalette {
40 pub primary: Color,
43 pub secondary: Color,
45
46 pub text: Color,
49 pub text_dim: Color,
51 pub text_disabled: Color,
53 pub text_placeholder: Color,
55 pub text_muted: Color,
57
58 pub bg: Color,
61 pub surface: Color,
63 pub surface_raised: Color,
65
66 pub border_focused: Color,
69 pub border: Color,
71 pub border_disabled: Color,
73 pub border_accent: Color,
75 pub separator: Color,
77
78 pub highlight_fg: Color,
81 pub highlight_bg: Color,
83 pub menu_highlight_fg: Color,
85 pub menu_highlight_bg: Color,
87 pub pressed_fg: Color,
89 pub pressed_bg: Color,
91
92 pub success: Color,
95 pub warning: Color,
97 pub error: Color,
99 pub info: Color,
101
102 pub diff_add_fg: Color,
105 pub diff_add_bg: Color,
107 pub diff_del_fg: Color,
109 pub diff_del_bg: Color,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115#[cfg_attr(feature = "theme-serde", derive(serde::Serialize, serde::Deserialize))]
116pub struct Theme {
117 pub name: String,
119 pub palette: ColorPalette,
121}
122
123impl Default for Theme {
124 fn default() -> Self {
125 Self::dark()
126 }
127}
128
129impl Theme {
130 pub fn dark() -> Self {
132 Self {
133 name: "Dark".to_string(),
134 palette: ColorPalette {
135 primary: Color::Yellow,
136 secondary: Color::Cyan,
137
138 text: Color::White,
139 text_dim: Color::Gray,
140 text_disabled: Color::DarkGray,
141 text_placeholder: Color::DarkGray,
142 text_muted: Color::Rgb(140, 140, 140),
143
144 bg: Color::Reset,
145 surface: Color::Rgb(40, 40, 40),
146 surface_raised: Color::Rgb(50, 50, 50),
147
148 border_focused: Color::Yellow,
149 border: Color::Gray,
150 border_disabled: Color::DarkGray,
151 border_accent: Color::Cyan,
152 separator: Color::Rgb(80, 80, 80),
153
154 highlight_fg: Color::Black,
155 highlight_bg: Color::Yellow,
156 menu_highlight_fg: Color::White,
157 menu_highlight_bg: Color::Rgb(60, 100, 180),
158 pressed_fg: Color::Black,
159 pressed_bg: Color::White,
160
161 success: Color::Green,
162 warning: Color::Yellow,
163 error: Color::Red,
164 info: Color::Cyan,
165
166 diff_add_fg: Color::Green,
167 diff_add_bg: Color::Rgb(0, 40, 0),
168 diff_del_fg: Color::Red,
169 diff_del_bg: Color::Rgb(40, 0, 0),
170 },
171 }
172 }
173
174 pub fn light() -> Self {
176 Self {
177 name: "Light".to_string(),
178 palette: ColorPalette {
179 primary: Color::Blue,
180 secondary: Color::Rgb(0, 128, 128),
181
182 text: Color::Rgb(30, 30, 30),
183 text_dim: Color::Rgb(100, 100, 100),
184 text_disabled: Color::Rgb(160, 160, 160),
185 text_placeholder: Color::Rgb(160, 160, 160),
186 text_muted: Color::Rgb(100, 100, 100),
187
188 bg: Color::Reset,
189 surface: Color::Rgb(250, 250, 250),
190 surface_raised: Color::Rgb(240, 240, 240),
191
192 border_focused: Color::Blue,
193 border: Color::Rgb(180, 180, 180),
194 border_disabled: Color::Rgb(200, 200, 200),
195 border_accent: Color::Rgb(0, 128, 128),
196 separator: Color::Rgb(200, 200, 200),
197
198 highlight_fg: Color::White,
199 highlight_bg: Color::Blue,
200 menu_highlight_fg: Color::White,
201 menu_highlight_bg: Color::Rgb(0, 120, 215),
202 pressed_fg: Color::White,
203 pressed_bg: Color::Rgb(30, 30, 30),
204
205 success: Color::Rgb(0, 128, 0),
206 warning: Color::Rgb(200, 150, 0),
207 error: Color::Rgb(200, 0, 0),
208 info: Color::Rgb(0, 128, 128),
209
210 diff_add_fg: Color::Rgb(0, 128, 0),
211 diff_add_bg: Color::Rgb(220, 255, 220),
212 diff_del_fg: Color::Rgb(200, 0, 0),
213 diff_del_bg: Color::Rgb(255, 220, 220),
214 },
215 }
216 }
217
218 pub fn style<S: for<'a> From<&'a Theme>>(&self) -> S {
230 S::from(self)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::components::{ButtonStyle, CheckBoxStyle, InputStyle};
238
239 #[test]
240 fn test_dark_theme_matches_button_default() {
241 let theme = Theme::dark();
242 let themed: ButtonStyle = theme.style();
243 let default = ButtonStyle::default();
244
245 assert_eq!(themed.focused_fg, default.focused_fg);
246 assert_eq!(themed.focused_bg, default.focused_bg);
247 assert_eq!(themed.unfocused_fg, default.unfocused_fg);
248 assert_eq!(themed.unfocused_bg, default.unfocused_bg);
249 assert_eq!(themed.disabled_fg, default.disabled_fg);
250 assert_eq!(themed.pressed_fg, default.pressed_fg);
251 assert_eq!(themed.pressed_bg, default.pressed_bg);
252 assert_eq!(themed.toggled_fg, default.toggled_fg);
253 assert_eq!(themed.toggled_bg, default.toggled_bg);
254 }
255
256 #[test]
257 fn test_dark_theme_matches_input_default() {
258 let theme = Theme::dark();
259 let themed: InputStyle = theme.style();
260 let default = InputStyle::default();
261
262 assert_eq!(themed.focused_border, default.focused_border);
263 assert_eq!(themed.unfocused_border, default.unfocused_border);
264 assert_eq!(themed.disabled_border, default.disabled_border);
265 assert_eq!(themed.text_fg, default.text_fg);
266 assert_eq!(themed.cursor_fg, default.cursor_fg);
267 assert_eq!(themed.placeholder_fg, default.placeholder_fg);
268 }
269
270 #[test]
271 fn test_dark_theme_matches_checkbox_default() {
272 let theme = Theme::dark();
273 let themed: CheckBoxStyle = theme.style();
274 let default = CheckBoxStyle::default();
275
276 assert_eq!(themed.focused_fg, default.focused_fg);
277 assert_eq!(themed.unfocused_fg, default.unfocused_fg);
278 assert_eq!(themed.disabled_fg, default.disabled_fg);
279 assert_eq!(themed.checked_fg, default.checked_fg);
280 }
281
282 #[test]
283 fn test_light_theme_differs_from_dark() {
284 let dark = Theme::dark();
285 let light = Theme::light();
286
287 assert_ne!(dark.palette.text, light.palette.text);
288 assert_ne!(dark.palette.primary, light.palette.primary);
289 assert_ne!(dark.palette.surface, light.palette.surface);
290 }
291
292 #[test]
293 fn test_theme_default_is_dark() {
294 let default = Theme::default();
295 let dark = Theme::dark();
296 assert_eq!(default.palette, dark.palette);
297 }
298
299 #[test]
300 fn test_theme_clone_and_eq() {
301 let theme = Theme::dark();
302 let cloned = theme.clone();
303 assert_eq!(theme, cloned);
304 }
305
306 #[test]
307 fn test_color_palette_clone_and_eq() {
308 let palette = Theme::dark().palette;
309 let cloned = palette.clone();
310 assert_eq!(palette, cloned);
311 }
312
313 #[test]
314 fn test_style_generic_method() {
315 let theme = Theme::dark();
316 let _: ButtonStyle = theme.style();
317 let _: InputStyle = theme.style();
318 let _: CheckBoxStyle = theme.style();
319 }
320
321 #[test]
322 fn test_light_theme_produces_valid_styles() {
323 let theme = Theme::light();
324 let btn: ButtonStyle = theme.style();
325 let input: InputStyle = theme.style();
326 let cb: CheckBoxStyle = theme.style();
327
328 let default_btn = ButtonStyle::default();
330 assert_ne!(btn.focused_bg, default_btn.focused_bg);
331
332 assert_ne!(input.text_fg, Color::Reset);
334 assert_ne!(cb.focused_fg, Color::Reset);
335 }
336}