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}