1use ratatui::style::{Color, Modifier, Style};
4
5pub struct Theme {
6 pub bg: Color,
7 pub bg_sel: Color,
8 pub bg_modal: Color,
9 pub fg: Color,
10 pub fg_bright: Color,
11 pub border: Color,
12 pub title: Color,
13 pub header: Color,
14 pub gutter_active: Color,
15 pub gutter_marked: Color,
16 pub hot: Color,
17 pub warm: Color,
18 pub ok: Color,
19 pub dim: Color,
20}
21
22impl Theme {
23 pub const GRUVBOX: Theme = Theme {
24 bg: Color::Rgb(0x28, 0x28, 0x28),
25 bg_sel: Color::Rgb(0x3c, 0x38, 0x36), bg_modal: Color::Rgb(0x1d, 0x20, 0x21),
27 fg: Color::Rgb(0xeb, 0xdb, 0xb2),
28 fg_bright: Color::Rgb(0xfb, 0xf1, 0xc7),
29 border: Color::Rgb(0x92, 0x83, 0x74),
30 title: Color::Rgb(0x8e, 0xc0, 0x7c),
31 header: Color::Rgb(0x83, 0xa5, 0x98),
32 gutter_active: Color::Rgb(0xfa, 0xbd, 0x2f),
33 gutter_marked: Color::Rgb(0xfe, 0x80, 0x19),
34 hot: Color::Rgb(0xfb, 0x49, 0x34),
35 warm: Color::Rgb(0xfe, 0x80, 0x19),
36 ok: Color::Rgb(0xb8, 0xbb, 0x26),
37 dim: Color::Rgb(0x92, 0x83, 0x74),
38 };
39
40 pub fn block_style(&self) -> Style {
41 Style::default().fg(self.border).bg(self.bg)
42 }
43 pub fn title_style(&self) -> Style {
44 Style::default().fg(self.title).add_modifier(Modifier::BOLD)
45 }
46 pub fn header_style(&self) -> Style {
47 Style::default()
48 .fg(self.header)
49 .add_modifier(Modifier::BOLD)
50 }
51 pub fn gutter_active_style(&self) -> Style {
52 Style::default().fg(self.gutter_active)
53 }
54 pub fn gutter_marked_style(&self) -> Style {
55 Style::default().fg(self.gutter_marked)
56 }
57 pub fn body_style(&self) -> Style {
58 Style::default().fg(self.fg).bg(self.bg)
59 }
60 pub fn modal_block_style(&self) -> Style {
61 Style::default().fg(self.gutter_active).bg(self.bg_modal)
62 }
63 pub fn modal_body_style(&self) -> Style {
64 Style::default().fg(self.fg_bright).bg(self.bg_modal)
65 }
66 pub fn dim_style(&self) -> Style {
67 Style::default().fg(self.dim)
68 }
69
70 pub fn score_color(&self, t: f64) -> Color {
75 let t = t.clamp(0.0, 1.0);
76 if t <= 0.5 {
77 lerp_rgb(self.ok, self.warm, t * 2.0)
78 } else {
79 lerp_rgb(self.warm, self.hot, (t - 0.5) * 2.0)
80 }
81 }
82}
83
84fn lerp_rgb(a: Color, b: Color, t: f64) -> Color {
85 let (ar, ag, ab) = rgb(a);
86 let (br, bg, bb) = rgb(b);
87 let mix = |x: u8, y: u8| ((x as f64) + ((y as f64) - (x as f64)) * t).round() as u8;
88 Color::Rgb(mix(ar, br), mix(ag, bg), mix(ab, bb))
89}
90
91fn rgb(c: Color) -> (u8, u8, u8) {
92 if let Color::Rgb(r, g, b) = c {
93 (r, g, b)
94 } else {
95 (0x80, 0x80, 0x80)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn score_color_endpoints_match_palette() {
107 let t = Theme::GRUVBOX;
108 assert_eq!(t.score_color(0.0), t.ok);
109 assert_eq!(t.score_color(0.5), t.warm);
110 assert_eq!(t.score_color(1.0), t.hot);
111 }
112
113 #[test]
114 fn score_color_clamps_out_of_range() {
115 let t = Theme::GRUVBOX;
116 assert_eq!(t.score_color(-1.0), t.ok);
117 assert_eq!(t.score_color(2.0), t.hot);
118 }
119
120 #[test]
121 fn score_color_blends_between_anchors() {
122 let t = Theme::GRUVBOX;
123 let q = t.score_color(0.25);
125 let (qr, qg, qb) = rgb(q);
126 let (ok_r, ok_g, ok_b) = rgb(t.ok);
127 let (wa_r, wa_g, wa_b) = rgb(t.warm);
128 let mid = |a: u8, b: u8| ((a as u16 + b as u16) / 2) as u8;
129 assert!((qr as i16 - mid(ok_r, wa_r) as i16).abs() <= 1);
131 assert!((qg as i16 - mid(ok_g, wa_g) as i16).abs() <= 1);
132 assert!((qb as i16 - mid(ok_b, wa_b) as i16).abs() <= 1);
133 }
134}