Skip to main content

ez_tui/types/
theme.rs

1use crate::types::palette::EzPalette;
2use crate::{Attribute, Props};
3use derive_more::Constructor;
4use ratatui::prelude::Style;
5use ratatui::prelude::{Stylize, Text};
6use ratatui::style::palette::tailwind::{ORANGE, RED, WHITE};
7use ratatui::text::Line;
8use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph};
9use std::fmt::Debug;
10
11/// Holds some commonly used style across your application
12#[derive(Debug, Clone)]
13pub struct Theme {
14    /// The text color of the application
15    pub text_palette: EzPalette,
16    /// The main color of the application. This will be used mainly for blocks borders
17    pub main_palette: EzPalette,
18    /// An accent color mainly used for focused blocks.
19    pub accent_palette: EzPalette,
20
21    /// A common style applied everywhere we can
22    pub default_style: Style,
23    /// An alternate version of the default style. This is used for focused blocks.
24    pub alternate_style: Option<Style>,
25
26    /// A common style for all borders
27    pub default_border_theme: BorderTheme,
28    /// An alternate version of the default border theme. This is used for focused blocks.
29    pub alternate_border_theme: Option<BorderTheme>,
30}
31
32/// Wraps all the defining attributes of a border
33#[derive(Debug, PartialEq, Clone, Constructor)]
34pub struct BorderTheme {
35    /// The borders of the block
36    pub borders: Borders,
37    /// The type of border
38    pub border_type: BorderType,
39    /// The style of the border
40    pub border_style: Style,
41}
42
43impl From<(Borders, BorderType, Style)> for BorderTheme {
44    fn from((borders, border_type, border_style): (Borders, BorderType, Style)) -> Self {
45        Self {
46            borders,
47            border_type,
48            border_style,
49        }
50    }
51}
52impl From<BorderTheme> for (Borders, BorderType, Style) {
53    fn from(border_theme: BorderTheme) -> Self {
54        (
55            border_theme.borders,
56            border_theme.border_type,
57            border_theme.border_style,
58        )
59    }
60}
61
62impl Default for BorderTheme {
63    fn default() -> Self {
64        Self {
65            borders: Borders::ALL,
66            border_type: BorderType::Plain,
67            border_style: Style::default(),
68        }
69    }
70}
71impl Default for Theme {
72    fn default() -> Self {
73        Self::basic(WHITE.into(), &(RED.into()), &(ORANGE.into()))
74    }
75}
76
77// Constructors
78impl Theme {
79    /// Creates a new theme with the given main, accent and extra colors. Uses default values for all other fields.
80    #[must_use]
81    pub fn basic(text: EzPalette, main: &EzPalette, accent: &EzPalette) -> Self {
82        Self {
83            text_palette: text,
84            main_palette: main.clone(),
85            accent_palette: accent.clone(),
86
87            default_style: Style::new(),
88            alternate_style: None,
89            default_border_theme: (
90                Borders::ALL,
91                BorderType::Rounded,
92                Style::new().fg(main.c600),
93            )
94                .into(),
95            alternate_border_theme: Some(
96                (
97                    Borders::ALL,
98                    BorderType::Rounded,
99                    Style::new().fg(accent.c600),
100                )
101                    .into(),
102            ),
103        }
104    }
105}
106
107// Functions to build UI elements based on self and some potential overide_props
108impl Theme {
109    /// Create a new [`Paragraph`] with the given text and themed depending on this [`Theme`]
110    pub fn paragraph<'a, T>(&self, text: T, overide: Option<&Props>) -> Paragraph<'a>
111    where
112        T: Into<Text<'a>>,
113    {
114        Paragraph::new(text).style(self.get_style(overide))
115    }
116
117    /// Create a new [`List`] with the given items and themed depending on this [`Theme`]
118    pub fn list<'a, T>(&'a self, items: T, overide: Option<&Props>) -> List<'a>
119    where
120        T: IntoIterator,
121        T::Item: Into<ListItem<'a>>,
122    {
123        List::new(items)
124            .block(self.block(overide))
125            .style(self.get_style(overide))
126            .highlight_symbol(">")
127            .highlight_style(self.get_reversed_style(overide))
128            .repeat_highlight_symbol(true)
129    }
130
131    /// Create a new block themed depending on its focus
132    #[must_use]
133    pub fn block<'a>(&self, overide: Option<&Props>) -> Block<'a> {
134        let props_overide = overide.cloned().unwrap_or_else(Props::default);
135        let focused = props_overide.has_focus();
136        let style = self.get_style(overide);
137        let title = props_overide
138            .get(Attribute::Title)
139            .and_then(|v| v.as_title().cloned());
140
141        let mut block = Block::bordered();
142
143        if let Some((title_text, alignment)) = title {
144            let title_color = if focused {
145                self.accent_palette.c600
146            } else {
147                self.main_palette.c600
148            };
149            block = block.title(
150                Line::from(format!(" {title_text} "))
151                    .alignment(alignment)
152                    .fg(title_color),
153            );
154        }
155        let (borders, border_type, border_style) = props_overide
156            .get(Attribute::Borders)
157            .and_then(|v| v.as_borders().cloned())
158            .map_or_else(|| self.get_border(overide).into(), |bt| bt.clone().into());
159
160        block
161            .style(style)
162            .borders(borders)
163            .border_type(border_type)
164            .border_style(border_style)
165    }
166
167    /// Get the style for a given focus flag
168    #[must_use]
169    pub fn get_style(&self, overide: Option<&Props>) -> Style {
170        let props_overide = overide.cloned().unwrap_or_else(Props::default);
171        let focused = props_overide.has_focus();
172        if let Some(ov_style) = overide
173            .and_then(|p| p.get(Attribute::Style))
174            .and_then(|s| s.as_style())
175        {
176            ov_style
177        } else if focused && let Some(alt_style) = &self.alternate_style {
178            *alt_style
179        } else {
180            self.default_style
181        }
182    }
183
184    /// Get the style for a given focus flag
185    #[must_use]
186    pub fn get_reversed_style(&self, overide: Option<&Props>) -> Style {
187        let props_overide = overide.cloned().unwrap_or_else(Props::default);
188        let focused = props_overide.has_focus();
189        if let Some(ov_style) = overide
190            .and_then(|p| p.get(Attribute::Style))
191            .and_then(|s| s.as_style())
192        {
193            ov_style.reversed()
194        } else if !focused && let Some(alt_style) = &self.alternate_style {
195            *alt_style
196        } else {
197            self.default_style
198        }
199    }
200
201    /// Get the border theme for a given focus flag
202    #[must_use]
203    pub fn get_border(&self, overide: Option<&Props>) -> BorderTheme {
204        let props_overide = overide.cloned().unwrap_or_else(Props::default);
205        let focused = props_overide.has_focus();
206        if let Some(ov_borders) = overide
207            .and_then(|p| p.get(Attribute::Borders))
208            .and_then(|s| s.as_borders().cloned())
209        {
210            ov_borders
211        } else if focused && let Some(alt_borders) = &self.alternate_border_theme {
212            alt_borders.clone()
213        } else {
214            self.default_border_theme.clone()
215        }
216    }
217
218    /// Return the main or accent if the given param is respectively [`true`] or [`false`]
219    #[must_use]
220    pub fn main_or_accent(&self, condition: bool) -> EzPalette {
221        if condition {
222            self.main_palette.clone()
223        } else {
224            self.accent_palette.clone()
225        }
226    }
227
228    /// Return the main or text color if the given param is respectively [`true`] or [`false`]
229    #[must_use]
230    pub fn main_or_text(&self, condition: bool) -> EzPalette {
231        if condition {
232            self.main_palette.clone()
233        } else {
234            self.text_palette.clone()
235        }
236    }
237
238    /// Return the accent or text color if the given param is respectively [`true`] or [`false`]
239    #[must_use]
240    pub fn accent_or_text(&self, condition: bool) -> EzPalette {
241        if condition {
242            self.accent_palette.clone()
243        } else {
244            self.text_palette.clone()
245        }
246    }
247}