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#[derive(Debug, Clone)]
13pub struct Theme {
14 pub text_palette: EzPalette,
16 pub main_palette: EzPalette,
18 pub accent_palette: EzPalette,
20
21 pub default_style: Style,
23 pub alternate_style: Option<Style>,
25
26 pub default_border_theme: BorderTheme,
28 pub alternate_border_theme: Option<BorderTheme>,
30}
31
32#[derive(Debug, PartialEq, Clone, Constructor)]
34pub struct BorderTheme {
35 pub borders: Borders,
37 pub border_type: BorderType,
39 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
77impl Theme {
79 #[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
107impl Theme {
109 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}