Skip to main content

tui_nuances/
style.rs

1//! Semantic [`Style`] helpers on [`Theme`]: ask for *meaning*, not colors.
2
3use crate::theme::Theme;
4use ratatui::{
5    buffer::Buffer,
6    layout::Rect,
7    style::{Color, Modifier, Style},
8};
9
10/// Status severity, each mapped to a theme color by [`Theme::status_color`].
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Status {
13    /// Operation succeeded / healthy.
14    Success,
15    /// Needs attention.
16    Warning,
17    /// Failed / unhealthy.
18    Error,
19    /// Neutral information.
20    Info,
21}
22
23impl Status {
24    /// All statuses, in severity-agnostic display order.
25    pub const ALL: [Status; 4] = [
26        Status::Success,
27        Status::Warning,
28        Status::Error,
29        Status::Info,
30    ];
31
32    /// Lower-case label (`"success"`, `"warning"`, `"error"`, `"info"`).
33    #[must_use]
34    pub fn label(self) -> &'static str {
35        match self {
36            Status::Success => "success",
37            Status::Warning => "warning",
38            Status::Error => "error",
39            Status::Info => "info",
40        }
41    }
42}
43
44impl Theme {
45    /// Base style: foreground (+ background when set).
46    #[must_use]
47    pub fn style(&self) -> Style {
48        let s = Style::default().fg(self.foreground);
49        match self.background {
50            Some(bg) => s.bg(bg),
51            None => s,
52        }
53    }
54
55    /// Alias of [`Theme::style`] (the name `tui-chart` used).
56    #[must_use]
57    pub fn base_style(&self) -> Style {
58        self.style()
59    }
60
61    /// Style for panels / cards: foreground on `surface` (falls back to the background).
62    #[must_use]
63    pub fn surface_style(&self) -> Style {
64        let s = Style::default().fg(self.foreground);
65        match self.surface.or(self.background) {
66            Some(bg) => s.bg(bg),
67            None => s,
68        }
69    }
70
71    /// De-emphasised text style.
72    #[must_use]
73    pub fn muted_style(&self) -> Style {
74        Style::default().fg(self.muted)
75    }
76
77    /// Border style; the focused variant uses `border_focused` and bold.
78    #[must_use]
79    pub fn border_style(&self, focused: bool) -> Style {
80        if focused {
81            Style::default()
82                .fg(self.border_focused)
83                .add_modifier(Modifier::BOLD)
84        } else {
85            Style::default().fg(self.border)
86        }
87    }
88
89    /// Title style (bold).
90    #[must_use]
91    pub fn title_style(&self) -> Style {
92        Style::default().fg(self.title).add_modifier(Modifier::BOLD)
93    }
94
95    /// Style of the selected item: `selection_fg` on `selection_bg`.
96    #[must_use]
97    pub fn selected_style(&self) -> Style {
98        Style::default().fg(self.selection_fg).bg(self.selection_bg)
99    }
100
101    /// Style of disabled elements.
102    #[must_use]
103    pub fn disabled_style(&self) -> Style {
104        Style::default().fg(self.disabled)
105    }
106
107    /// Style of search matches / markers: `highlight` color, bold.
108    #[must_use]
109    pub fn highlight_style(&self) -> Style {
110        Style::default()
111            .fg(self.highlight)
112            .add_modifier(Modifier::BOLD)
113    }
114
115    /// Color for a [`Status`].
116    #[must_use]
117    pub fn status_color(&self, status: Status) -> Color {
118        match status {
119            Status::Success => self.success,
120            Status::Warning => self.warning,
121            Status::Error => self.error,
122            Status::Info => self.info,
123        }
124    }
125
126    /// Style for a [`Status`] (its color as foreground).
127    #[must_use]
128    pub fn status_style(&self, status: Status) -> Style {
129        Style::default().fg(self.status_color(status))
130    }
131
132    /// Axis line style.
133    #[must_use]
134    pub fn axis_style(&self) -> Style {
135        Style::default().fg(self.axis)
136    }
137
138    /// Tick / legend label style (same color as `muted`).
139    #[must_use]
140    pub fn label_style(&self) -> Style {
141        self.muted_style()
142    }
143
144    /// Grid line style.
145    #[must_use]
146    pub fn grid_style(&self) -> Style {
147        Style::default().fg(self.grid)
148    }
149
150    /// Tooltip style: `tooltip_fg` on `tooltip_bg`.
151    #[must_use]
152    pub fn tooltip_style(&self) -> Style {
153        Style::default().fg(self.tooltip_fg).bg(self.tooltip_bg)
154    }
155
156    /// Fill `area` with the theme background (no-op when there is none).
157    pub fn fill(&self, area: Rect, buf: &mut Buffer) {
158        if let Some(bg) = self.background {
159            buf.set_style(area.intersection(buf.area), Style::default().bg(bg));
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn style_has_fg_and_bg() {
170        let t = Theme::dracula();
171        let s = t.style();
172        assert_eq!(s.fg, Some(t.foreground));
173        assert_eq!(s.bg, t.background);
174        assert_eq!(Theme::default().style().bg, None);
175        assert_eq!(t.base_style(), s);
176    }
177
178    #[test]
179    fn surface_style_falls_back() {
180        let t = Theme::dracula();
181        assert_eq!(t.surface_style().bg, t.surface);
182        let t = t.surface(None);
183        assert_eq!(t.surface_style().bg, t.background);
184    }
185
186    #[test]
187    fn border_focus_differs() {
188        let t = Theme::nord();
189        assert_eq!(t.border_style(false).fg, Some(t.border));
190        assert_eq!(t.border_style(true).fg, Some(t.border_focused));
191        assert!(t.border_style(true).add_modifier.contains(Modifier::BOLD));
192    }
193
194    #[test]
195    fn selected_uses_selection_colors() {
196        let t = Theme::gruvbox();
197        let s = t.selected_style();
198        assert_eq!((s.fg, s.bg), (Some(t.selection_fg), Some(t.selection_bg)));
199    }
200
201    #[test]
202    fn status_maps_each_variant() {
203        let t = Theme::one_dark();
204        assert_eq!(t.status_color(Status::Success), t.success);
205        assert_eq!(t.status_color(Status::Warning), t.warning);
206        assert_eq!(t.status_color(Status::Error), t.error);
207        assert_eq!(t.status_color(Status::Info), t.info);
208        assert_eq!(t.status_style(Status::Error).fg, Some(t.error));
209    }
210
211    #[test]
212    fn status_labels_unique() {
213        let mut l: Vec<_> = Status::ALL.iter().map(|s| s.label()).collect();
214        l.sort_unstable();
215        l.dedup();
216        assert_eq!(l.len(), 4);
217    }
218
219    #[test]
220    fn misc_styles() {
221        let t = Theme::monokai();
222        assert_eq!(t.muted_style().fg, Some(t.muted));
223        assert_eq!(t.label_style(), t.muted_style());
224        assert_eq!(t.disabled_style().fg, Some(t.disabled));
225        assert_eq!(t.axis_style().fg, Some(t.axis));
226        assert_eq!(t.grid_style().fg, Some(t.grid));
227        assert_eq!(t.highlight_style().fg, Some(t.highlight));
228        assert_eq!(t.tooltip_style().bg, Some(t.tooltip_bg));
229        assert!(t.title_style().add_modifier.contains(Modifier::BOLD));
230    }
231
232    #[test]
233    fn fill_paints_background_within_buffer() {
234        let t = Theme::dracula();
235        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 2));
236        t.fill(Rect::new(0, 0, 10, 10), &mut buf);
237        assert_eq!(buf[(3, 1)].bg, t.background.unwrap());
238        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 2));
239        Theme::default().fill(buf.area, &mut buf);
240        assert_eq!(buf[(0, 0)].bg, Color::Reset);
241    }
242}