1use crate::color::{mix, readable_on, to_rgb};
4use ratatui::style::Color;
5
6pub const DEFAULT_PALETTE: [Color; 8] = [
8 Color::Cyan,
9 Color::Yellow,
10 Color::Magenta,
11 Color::Green,
12 Color::LightRed,
13 Color::LightBlue,
14 Color::LightGreen,
15 Color::White,
16];
17
18pub(crate) struct Spec {
20 pub bg: u32,
21 pub fg: u32,
22 pub muted: u32,
23 pub surface: u32,
24 pub accent: u32,
25 pub secondary: u32,
26 pub success: u32,
27 pub warning: u32,
28 pub error: u32,
29 pub info: u32,
30 pub palette: &'static [u32],
31}
32
33pub(crate) const fn rgb(hex: u32) -> Color {
35 Color::Rgb((hex >> 16) as u8, (hex >> 8) as u8, hex as u8)
36}
37
38#[derive(Debug, Clone, PartialEq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50#[cfg_attr(feature = "serde", serde(default))]
51pub struct Theme {
52 pub name: String,
54 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
56 pub background: Option<Color>,
57 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
59 pub surface: Option<Color>,
60 pub foreground: Color,
62 pub muted: Color,
64 pub disabled: Color,
66 pub border: Color,
68 pub border_focused: Color,
70 pub title: Color,
72 pub primary: Color,
74 pub secondary: Color,
76 pub accent: Color,
78 pub success: Color,
80 pub warning: Color,
82 pub error: Color,
84 pub info: Color,
86 pub selection_bg: Color,
88 pub selection_fg: Color,
90 pub highlight: Color,
92 pub palette: Vec<Color>,
94 pub bull: Color,
96 pub bear: Color,
98 pub neutral: Color,
100 pub axis: Color,
102 pub grid: Color,
104 pub crosshair: Color,
106 pub tooltip_fg: Color,
108 pub tooltip_bg: Color,
110}
111
112impl Default for Theme {
113 fn default() -> Self {
115 Self {
116 name: "default".into(),
117 background: None,
118 surface: None,
119 foreground: Color::White,
120 muted: Color::Gray,
121 disabled: Color::DarkGray,
122 border: Color::Gray,
123 border_focused: Color::Cyan,
124 title: Color::White,
125 primary: Color::Cyan,
126 secondary: Color::Magenta,
127 accent: Color::Cyan,
128 success: Color::Green,
129 warning: Color::Yellow,
130 error: Color::Red,
131 info: Color::Blue,
132 selection_bg: Color::Blue,
133 selection_fg: Color::White,
134 highlight: Color::Yellow,
135 palette: DEFAULT_PALETTE.to_vec(),
136 bull: Color::Green,
137 bear: Color::Red,
138 neutral: Color::Blue,
139 axis: Color::DarkGray,
140 grid: Color::DarkGray,
141 crosshair: Color::Gray,
142 tooltip_fg: Color::White,
143 tooltip_bg: Color::DarkGray,
144 }
145 }
146}
147
148impl Theme {
149 #[must_use]
174 pub fn from_base(
175 name: &str,
176 background: Option<Color>,
177 foreground: Color,
178 accent: Color,
179 bull: Color,
180 bear: Color,
181 ) -> Self {
182 let base = background.unwrap_or(Color::Rgb(0, 0, 0));
183 let blend = |t: f64, fallback: Color| {
184 if to_rgb(base).is_some() && to_rgb(foreground).is_some() {
185 mix(base, foreground, t)
186 } else {
187 fallback
188 }
189 };
190 let muted = blend(0.6, Color::Gray);
191 let grid = blend(0.25, Color::DarkGray);
192 let disabled = blend(0.4, Color::DarkGray);
193 let surface = background.map(|_| blend(0.08, base));
194 let selection_bg = if to_rgb(base).is_some() && to_rgb(accent).is_some() {
195 mix(base, accent, 0.35)
196 } else {
197 accent
198 };
199 let mut palette = vec![accent, bull, bear];
200 palette.extend(
201 DEFAULT_PALETTE
202 .iter()
203 .copied()
204 .filter(|c| ![accent, bull, bear].contains(c)),
205 );
206 Self {
207 name: name.into(),
208 background,
209 surface,
210 foreground,
211 muted,
212 disabled,
213 border: blend(0.35, Color::Gray),
214 border_focused: accent,
215 title: foreground,
216 primary: accent,
217 secondary: blend(0.5, Color::Magenta),
218 accent,
219 success: bull,
220 warning: Color::Yellow,
221 error: bear,
222 info: accent,
223 selection_bg,
224 selection_fg: readable_on(selection_bg),
225 highlight: Color::Yellow,
226 palette,
227 bull,
228 bear,
229 neutral: accent,
230 axis: grid,
231 grid,
232 crosshair: muted,
233 tooltip_fg: foreground,
234 tooltip_bg: surface.or(background).unwrap_or(grid),
235 }
236 }
237
238 #[must_use]
252 pub fn from_palette(name: &str, colors: &[Color]) -> Self {
253 let src: &[Color] = if colors.is_empty() {
254 &DEFAULT_PALETTE
255 } else {
256 colors
257 };
258 let at = |i: usize| src[i % src.len()];
259 let mut t = Self::from_base(name, None, Color::White, at(0), at(1), at(2));
260 let n = src.len().max(8);
261 t.palette = (0..n).map(at).collect();
262 t
263 }
264
265 pub(crate) fn from_spec(name: &str, s: Spec) -> Self {
267 let (bg, fg, accent) = (rgb(s.bg), rgb(s.fg), rgb(s.accent));
268 let surface = rgb(s.surface);
269 let selection_bg = mix(bg, accent, 0.35);
270 let mut t = Self {
271 name: name.into(),
272 background: Some(bg),
273 surface: Some(surface),
274 foreground: fg,
275 muted: rgb(s.muted),
276 disabled: mix(bg, fg, 0.38),
277 border: mix(bg, fg, 0.3),
278 border_focused: accent,
279 title: fg,
280 primary: accent,
281 secondary: rgb(s.secondary),
282 accent,
283 success: rgb(s.success),
284 warning: rgb(s.warning),
285 error: rgb(s.error),
286 info: rgb(s.info),
287 selection_bg,
288 selection_fg: readable_on(selection_bg),
289 highlight: rgb(s.warning),
290 palette: s.palette.iter().map(|&c| rgb(c)).collect(),
291 bull: rgb(s.success),
292 bear: rgb(s.error),
293 neutral: rgb(s.muted),
294 axis: rgb(s.muted),
295 grid: mix(bg, fg, 0.2),
296 crosshair: rgb(s.muted),
297 tooltip_fg: fg,
298 tooltip_bg: surface,
299 };
300 crate::roles::apply(&mut t);
301 t
302 }
303
304 pub(crate) fn map_colors(&mut self, f: impl Fn(Color) -> Color) {
306 self.background = self.background.map(&f);
307 self.surface = self.surface.map(&f);
308 for c in [
309 &mut self.foreground,
310 &mut self.muted,
311 &mut self.disabled,
312 &mut self.border,
313 &mut self.border_focused,
314 &mut self.title,
315 &mut self.primary,
316 &mut self.secondary,
317 &mut self.accent,
318 &mut self.success,
319 &mut self.warning,
320 &mut self.error,
321 &mut self.info,
322 &mut self.selection_bg,
323 &mut self.selection_fg,
324 &mut self.highlight,
325 &mut self.bull,
326 &mut self.bear,
327 &mut self.neutral,
328 &mut self.axis,
329 &mut self.grid,
330 &mut self.crosshair,
331 &mut self.tooltip_fg,
332 &mut self.tooltip_bg,
333 ] {
334 *c = f(*c);
335 }
336 for c in &mut self.palette {
337 *c = f(*c);
338 }
339 }
340
341 #[must_use]
344 pub fn colors(&self) -> Vec<Color> {
345 let mut t = self.clone();
346 let out = std::cell::RefCell::new(Vec::new());
347 t.map_colors(|c| {
348 out.borrow_mut().push(c);
349 c
350 });
351 out.into_inner()
352 }
353
354 #[must_use]
356 pub fn name(mut self, name: impl Into<String>) -> Self {
357 self.name = name.into();
358 self
359 }
360
361 #[must_use]
363 pub fn palette(mut self, palette: Vec<Color>) -> Self {
364 if !palette.is_empty() {
365 self.palette = palette;
366 }
367 self
368 }
369
370 #[must_use]
372 pub fn background(mut self, background: Option<Color>) -> Self {
373 self.background = background;
374 self
375 }
376
377 #[must_use]
379 pub fn surface(mut self, surface: Option<Color>) -> Self {
380 self.surface = surface;
381 self
382 }
383
384 color_setters!(
385 foreground,
386 muted,
387 disabled,
388 border,
389 border_focused,
390 title,
391 primary,
392 secondary,
393 accent,
394 success,
395 warning,
396 error,
397 info,
398 selection_bg,
399 selection_fg,
400 highlight,
401 bull,
402 bear,
403 neutral,
404 axis,
405 grid,
406 crosshair,
407 tooltip_fg,
408 tooltip_bg,
409 );
410
411 #[must_use]
413 pub fn tooltip(mut self, fg: Color, bg: Color) -> Self {
414 self.tooltip_fg = fg;
415 self.tooltip_bg = bg;
416 self
417 }
418
419 #[must_use]
421 pub fn selection(mut self, fg: Color, bg: Color) -> Self {
422 self.selection_fg = fg;
423 self.selection_bg = bg;
424 self
425 }
426
427 #[must_use]
429 pub fn color(&self, i: usize) -> Color {
430 if self.palette.is_empty() {
431 self.accent
432 } else {
433 self.palette[i % self.palette.len()]
434 }
435 }
436}
437
438impl From<&Theme> for Theme {
439 fn from(t: &Theme) -> Self {
440 t.clone()
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 fn base() -> Theme {
449 Theme::from_base(
450 "t",
451 Some(Color::Rgb(0, 0, 0)),
452 Color::Rgb(200, 200, 200),
453 Color::Rgb(10, 20, 30),
454 Color::Green,
455 Color::Red,
456 )
457 }
458
459 #[test]
460 fn from_base_keeps_inputs() {
461 let t = base();
462 assert_eq!(t.name, "t");
463 assert_eq!(t.background, Some(Color::Rgb(0, 0, 0)));
464 assert_eq!(t.foreground, Color::Rgb(200, 200, 200));
465 assert_eq!(
466 (t.accent, t.primary),
467 (Color::Rgb(10, 20, 30), Color::Rgb(10, 20, 30))
468 );
469 assert_eq!((t.bull, t.success), (Color::Green, Color::Green));
470 assert_eq!((t.bear, t.error), (Color::Red, Color::Red));
471 }
472
473 #[test]
474 fn from_base_blends_muted_and_grid() {
475 let t = base();
476 assert_eq!(t.muted, Color::Rgb(120, 120, 120));
477 assert_eq!(t.grid, Color::Rgb(50, 50, 50));
478 assert_eq!(t.axis, t.grid);
479 assert_eq!(t.crosshair, t.muted);
480 }
481
482 #[test]
483 fn from_base_palette_starts_with_base_colors() {
484 let t = base();
485 assert_eq!(&t.palette[..3], &[t.accent, Color::Green, Color::Red]);
486 assert!(t.palette.len() >= 8);
487 assert_eq!(t.palette.iter().filter(|c| **c == Color::Green).count(), 1);
488 }
489
490 #[test]
491 fn from_base_without_background() {
492 let t = Theme::from_base(
493 "n",
494 None,
495 Color::Rgb(200, 200, 200),
496 Color::Cyan,
497 Color::Green,
498 Color::Red,
499 );
500 assert_eq!(t.background, None);
501 assert_eq!(t.surface, None);
502 assert_eq!(t.muted, Color::Rgb(120, 120, 120));
503 }
504
505 #[test]
506 fn from_base_unresolvable_falls_back() {
507 let t = Theme::from_base(
508 "r",
509 None,
510 Color::Reset,
511 Color::Cyan,
512 Color::Green,
513 Color::Red,
514 );
515 assert_eq!(t.muted, Color::Gray);
516 assert_eq!(t.grid, Color::DarkGray);
517 }
518
519 #[test]
520 fn from_base_selection_is_readable() {
521 let t = base();
522 assert!(crate::color::contrast_ratio(t.selection_fg, t.selection_bg) >= 4.5);
523 }
524
525 #[test]
526 fn from_palette_cycles_to_eight() {
527 let t = Theme::from_palette("p", &[Color::Red, Color::Blue, Color::Green]);
528 assert_eq!(t.palette.len(), 8);
529 assert_eq!(t.palette[3], Color::Red);
530 assert_eq!(
531 (t.accent, t.bull, t.bear),
532 (Color::Red, Color::Blue, Color::Green)
533 );
534 }
535
536 #[test]
537 fn from_palette_empty_uses_default() {
538 let t = Theme::from_palette("e", &[]);
539 assert_eq!(t.palette, DEFAULT_PALETTE.to_vec());
540 }
541
542 #[test]
543 fn from_palette_keeps_long_palettes() {
544 let long: Vec<Color> = (0..12).map(Color::Indexed).collect();
545 assert_eq!(Theme::from_palette("l", &long).palette, long);
546 }
547
548 #[test]
549 fn setters_override_fields() {
550 let t = Theme::default()
551 .name("x")
552 .background(Some(Color::Black))
553 .surface(Some(Color::Blue))
554 .foreground(Color::Red)
555 .primary(Color::Green)
556 .tooltip(Color::Black, Color::White)
557 .selection(Color::Cyan, Color::Magenta);
558 assert_eq!(t.name, "x");
559 assert_eq!(t.background, Some(Color::Black));
560 assert_eq!(t.surface, Some(Color::Blue));
561 assert_eq!(t.foreground, Color::Red);
562 assert_eq!(t.primary, Color::Green);
563 assert_eq!((t.tooltip_fg, t.tooltip_bg), (Color::Black, Color::White));
564 assert_eq!(
565 (t.selection_fg, t.selection_bg),
566 (Color::Cyan, Color::Magenta)
567 );
568 }
569
570 #[test]
571 fn empty_palette_setter_ignored() {
572 let before = Theme::default();
573 assert_eq!(before.clone().palette(vec![]).palette, before.palette);
574 assert_eq!(before.palette(vec![Color::Red]).palette, vec![Color::Red]);
575 }
576
577 #[test]
578 fn color_cycles_and_handles_empty() {
579 let t = Theme::default().palette(vec![Color::Red, Color::Blue]);
580 assert_eq!(t.color(0), Color::Red);
581 assert_eq!(t.color(3), Color::Blue);
582 let mut e = Theme::default();
583 e.palette.clear();
584 assert_eq!(e.color(5), e.accent);
585 }
586
587 #[test]
588 fn colors_lists_everything() {
589 let t = base();
590 assert!(t.colors().len() >= 24 + 8);
591 }
592
593 #[test]
594 fn from_ref_clones() {
595 let t = base();
596 assert_eq!(Theme::from(&t), t);
597 }
598}