Skip to main content

tui_nuances/
provider.rs

1//! Traits that connect widget crates and application theme types to [`Theme`].
2//!
3//! * Widget crates implement [`Themed`] on their widgets:
4//!
5//!   ```
6//!   use tui_nuances::{Theme, Themed};
7//!   use ratatui::style::{Color, Style};
8//!
9//!   struct Gauge { style: Style }
10//!   impl Themed for Gauge {
11//!       fn apply_theme(mut self, theme: &Theme) -> Self {
12//!           self.style = Style::default().fg(theme.accent);
13//!           self
14//!       }
15//!   }
16//!   let g = Gauge { style: Style::default() }.apply_theme(&Theme::nord());
17//!   assert_eq!(g.style.fg, Some(Theme::nord().accent));
18//!   # let _ = Color::Red;
19//!   ```
20//!
21//! * Applications implement [`ThemeProvider`] on their own theme type, then
22//!   pass it anywhere via [`Themed::themed`]:
23//!
24//!   ```
25//!   use ratatui::style::Color;
26//!   use tui_nuances::{Theme, ThemeProvider};
27//!
28//!   struct AppTheme { bg: Color, text: Color, brand: Color }
29//!
30//!   impl ThemeProvider for AppTheme {
31//!       fn nuance_theme(&self) -> Theme {
32//!           Theme::from_base("app", Some(self.bg), self.text, self.brand, Color::Green, Color::Red)
33//!       }
34//!   }
35//!
36//!   let app = AppTheme { bg: Color::Rgb(16, 16, 24), text: Color::White, brand: Color::Magenta };
37//!   assert_eq!(app.nuance_theme().primary, Color::Magenta);
38//!   ```
39//!
40//! # `From<&T>` guidance
41//!
42//! Because of the orphan rule a *widget crate* cannot implement
43//! `From<&AppTheme> for Theme`, but an *application* (which owns `AppTheme`)
44//! can; it is a handy complement when widgets take `impl Into<Theme>`:
45//! `impl From<&AppTheme> for tui_nuances::Theme { … }`. Prefer
46//! [`ThemeProvider`] when you want one trait that works for every widget.
47
48use crate::theme::Theme;
49
50/// Implemented by widgets that can restyle themselves from a [`Theme`].
51pub trait Themed: Sized {
52    /// Return `self` styled from `theme`. Explicit per-item colors set on the
53    /// widget should keep winning over the theme.
54    #[must_use]
55    fn apply_theme(self, theme: &Theme) -> Self;
56
57    /// Apply the theme produced by any [`ThemeProvider`].
58    #[must_use]
59    fn themed(self, provider: &impl ThemeProvider) -> Self {
60        self.apply_theme(&provider.nuance_theme())
61    }
62}
63
64/// Implemented by application theme types to expose them as a [`Theme`].
65pub trait ThemeProvider {
66    /// Produce the shared [`Theme`] for this application theme.
67    fn nuance_theme(&self) -> Theme;
68}
69
70impl ThemeProvider for Theme {
71    fn nuance_theme(&self) -> Theme {
72        self.clone()
73    }
74}
75
76impl<T: ThemeProvider + ?Sized> ThemeProvider for &T {
77    fn nuance_theme(&self) -> Theme {
78        (**self).nuance_theme()
79    }
80}
81
82impl<T: ThemeProvider + ?Sized> ThemeProvider for Box<T> {
83    fn nuance_theme(&self) -> Theme {
84        (**self).nuance_theme()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use ratatui::style::Color;
92
93    struct W(Color);
94    impl Themed for W {
95        fn apply_theme(self, t: &Theme) -> Self {
96            W(t.accent)
97        }
98    }
99
100    struct App;
101    impl ThemeProvider for App {
102        fn nuance_theme(&self) -> Theme {
103            Theme::nord()
104        }
105    }
106
107    #[test]
108    fn theme_is_its_own_provider() {
109        let t = Theme::dracula();
110        assert_eq!(t.nuance_theme(), t);
111    }
112
113    #[test]
114    fn themed_uses_provider() {
115        assert_eq!(W(Color::Reset).themed(&App).0, Theme::nord().accent);
116        assert_eq!(W(Color::Reset).themed(&&App).0, Theme::nord().accent);
117    }
118
119    #[test]
120    fn boxed_dyn_provider() {
121        let b: Box<dyn ThemeProvider> = Box::new(App);
122        assert_eq!(b.nuance_theme().name, "nord");
123        assert_eq!(W(Color::Reset).themed(&b).0, Theme::nord().accent);
124    }
125}