rat_theme4/theme.rs
1//!
2//! SalsaTheme is the main structure for themes.
3//!
4//! It holds one [Palette] that has the color-table
5//! and a list of aliases for colors in the color-table.
6//! These aliases allow the palette to give a bit more
7//! semantics to it plain color-array.
8//!
9//! SalsaTheme is on the other end and has a hashmap
10//! of style-names that map to
11//! * a [Style]
12//! * a Fn closure that creates a widget-specific xxxStyle struct.
13//! this closure can use the palette or any previously defined
14//! styles to create the xxxStyle struct.
15//!
16//! In between is a create-fn that links all of this together.
17//!
18//! In your application you can use one of the defined Themes/Palettes
19//! and modify/extend before you hand it off to your UI code.
20//!
21//! __Rationale__
22//!
23//! - Colors are separated from styles. There is an editor `pal-edit`
24//! to create a palette + aliases. It can generate rust code
25//! that can be used as `statÃc` data.
26//! - There is a `.pal` file-format for this. **todo: add a function
27//! to load a .pal**
28//!
29//! - Themes and xxxStyle structs can contain other things than
30//! colors. `Block` is used often. Alignment and related flags
31//! are available. And there are some flags that modify the
32//! behaviour of widgets.
33//! - xxxStyle combine everything in one package, and can be
34//! set with one function call when rendering. You don't need
35//! 20 lines of styling functions for each widget.
36//!
37use crate::is_log_style_define;
38use crate::palette::Palette;
39use crate::themes::create_fallback;
40use log::info;
41use ratatui::style::Style;
42use std::any::{Any, type_name};
43use std::collections::{HashMap, hash_map};
44use std::fmt::{Debug, Formatter};
45
46/// Categorization of themes.
47/// Helpful when extending an existing theme.
48#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum Category {
51 #[default]
52 Other,
53 /// Dark theme.
54 Dark,
55 /// Light theme.
56 Light,
57 /// Shell theme. Themes of this category rely on background colors sparingly
58 /// and use any default the terminal itself provides.
59 Shell,
60}
61
62trait StyleValue: Any + Debug {}
63impl<T> StyleValue for T where T: Any + Debug {}
64
65type Entry = Box<dyn Fn(&SalsaTheme) -> Box<dyn StyleValue> + 'static>;
66type Modify = Box<dyn Fn(Box<dyn Any>, &SalsaTheme) -> Box<dyn StyleValue> + 'static>;
67
68///
69/// SalsaTheme holds any predefined styles for the UI.
70///
71/// The foremost usage is as a store of named [Style](ratatui::style::Style)s.
72/// It can also hold the structured styles used by rat-widget's.
73/// Or really any value that can be produced by a closure.
74///
75/// It uses a flat naming scheme and doesn't cascade upwards at all.
76pub struct SalsaTheme {
77 pub name: String,
78 pub cat: Category,
79 pub p: Palette,
80 styles: HashMap<&'static str, Entry>,
81 modify: HashMap<&'static str, Modify>,
82}
83
84impl Default for SalsaTheme {
85 fn default() -> Self {
86 create_fallback("Fallback", Palette::default())
87 }
88}
89
90impl Debug for SalsaTheme {
91 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("Theme")
93 .field("name", &self.name)
94 .field("cat", &self.cat)
95 .field("palette", &self.p)
96 .field("styles", &self.styles.keys().collect::<Vec<_>>())
97 .field("modify", &self.modify.keys().collect::<Vec<_>>())
98 .finish()
99 }
100}
101
102impl SalsaTheme {
103 /// Create an empty theme with a given color palette.
104 pub fn new(name: impl Into<String>, cat: Category, p: Palette) -> Self {
105 Self {
106 name: name.into(),
107 cat,
108 p,
109 styles: Default::default(),
110 modify: Default::default(),
111 }
112 }
113
114 /// Some display name.
115 pub fn name(&self) -> &str {
116 &self.name
117 }
118
119 /// Define a style as a plain [Style].
120 pub fn define_style(&mut self, name: &'static str, style: Style) {
121 let boxed = Box::new(move |_: &SalsaTheme| -> Box<dyn StyleValue> { Box::new(style) });
122 self.define(name, boxed);
123 }
124
125 /// Define a style a struct that will be cloned for every query.
126 pub fn define_clone(&mut self, name: &'static str, sample: impl Clone + Any + Debug + 'static) {
127 let boxed = Box::new(move |_th: &SalsaTheme| -> Box<dyn StyleValue> {
128 Box::new(sample.clone()) //
129 });
130 self.define(name, boxed);
131 }
132
133 /// Define a style as a call to a constructor fn.
134 ///
135 /// The constructor gets access to all previously defined styles.
136 pub fn define_fn<O: Any + Debug>(
137 &mut self,
138 name: &'static str,
139 create: impl Fn(&SalsaTheme) -> O + 'static,
140 ) {
141 let boxed = Box::new(move |th: &SalsaTheme| -> Box<dyn StyleValue> {
142 Box::new(create(th)) //
143 });
144 self.define(name, boxed);
145 }
146
147 /// Define a style as a call to a constructor fn.
148 ///
149 /// This one takes no arguments, this is nice to set Widget::default
150 /// as the style-fn.
151 pub fn define_fn0<O: Any + Debug>(
152 &mut self,
153 name: &'static str,
154 create: impl Fn() -> O + 'static,
155 ) {
156 let boxed = Box::new(move |_th: &SalsaTheme| -> Box<dyn StyleValue> {
157 Box::new(create()) //
158 });
159 self.define(name, boxed);
160 }
161
162 fn define(&mut self, name: &'static str, boxed: Entry) {
163 if is_log_style_define() {
164 info!("salsa-style: {:?}->{:?}", name, (*boxed)(self));
165 }
166 match self.styles.insert(name, boxed) {
167 None => {}
168 Some(v) => {
169 if is_log_style_define() {
170 info!("salsa-style: OVERWRITE {:?}. Was {:?}", name, v(self));
171 }
172 }
173 };
174 }
175
176 /// Add a modification of a defined style.
177 ///
178 /// This function is applied to the original style every time the style is queried.
179 ///
180 /// Currently only a single modification is possible. If you set a second one
181 /// it will overwrite the previous.
182 ///
183 /// __Panic__
184 ///
185 /// * When debug_assertions are enabled the modifier will panic if
186 /// it gets a type other than `O`.
187 /// * Otherwise it will fall back to the default value of `O`.
188 ///
189 pub fn modify<O: Any + Default + Debug + Sized + 'static>(
190 &mut self,
191 name: &'static str,
192 modify: impl Fn(O, &SalsaTheme) -> O + 'static,
193 ) {
194 let boxed = Box::new(
195 move |v: Box<dyn Any>, th: &SalsaTheme| -> Box<dyn StyleValue> {
196 if cfg!(debug_assertions) {
197 let v = match v.downcast::<O>() {
198 Ok(v) => *v,
199 Err(e) => {
200 panic!(
201 "downcast fails for '{}' to {}. Is {:?}",
202 name,
203 type_name::<O>(),
204 e
205 );
206 }
207 };
208
209 let v = modify(v, th);
210
211 Box::new(v)
212 } else {
213 let v = match v.downcast::<O>() {
214 Ok(v) => *v,
215 Err(_) => O::default(),
216 };
217
218 let v = modify(v, th);
219
220 Box::new(v)
221 }
222 },
223 );
224
225 match self.modify.entry(name) {
226 hash_map::Entry::Occupied(mut entry) => {
227 if is_log_style_define() {
228 info!("salsa-style: overwrite modifier for {:?}", name);
229 }
230 _ = entry.insert(boxed);
231 }
232 hash_map::Entry::Vacant(entry) => {
233 if is_log_style_define() {
234 info!("salsa-style: set modifier for {:?}", name);
235 }
236 entry.insert(boxed);
237 }
238 };
239 }
240
241 /// Get one of the defined ratatui-Styles.
242 ///
243 /// This is the same as the single [style] function, it just
244 /// fixes the return-type to [Style]. This is useful if the
245 /// receiver is defined as `impl Into<Style>`.
246 ///
247 /// This may fail:
248 ///
249 /// __Panic__
250 ///
251 /// * When debug_assertions are enabled it will panic when
252 /// called with an unknown style name, or if the downcast
253 /// to the out type fails.
254 /// * Otherwise, it will return the default value of the out type.
255 pub fn style_style(&self, name: &str) -> Style
256 where
257 Self: Sized,
258 {
259 self.style::<Style>(name)
260 }
261
262 /// Get any of the defined styles.
263 ///
264 /// It downcasts the stored value to the required out type.
265 ///
266 /// This may fail:
267 ///
268 /// __Panic__
269 ///
270 /// * When debug_assertions are enabled it will panic when
271 /// called with an unknown style name, or if the downcast
272 /// to the out type fails.
273 /// * Otherwise, it will return the default value of the out type.
274 pub fn style<O: Default + Sized + 'static>(&self, name: &str) -> O
275 where
276 Self: Sized,
277 {
278 if cfg!(debug_assertions) {
279 let style = match self.dyn_style(name) {
280 Some(v) => v,
281 None => {
282 panic!("unknown widget {:?}", name)
283 }
284 };
285 let any_style = style as Box<dyn Any>;
286 let style = match any_style.downcast::<O>() {
287 Ok(v) => v,
288 Err(_) => {
289 let style = self.dyn_style(name).expect("style");
290 panic!(
291 "downcast fails for '{}' to {}: {:?}",
292 name,
293 type_name::<O>(),
294 style
295 );
296 }
297 };
298 *style
299 } else {
300 let Some(style) = self.dyn_style(name) else {
301 return O::default();
302 };
303 let any_style = style as Box<dyn Any>;
304 let Ok(style) = any_style.downcast::<O>() else {
305 return O::default();
306 };
307 *style
308 }
309 }
310
311 /// Get a style struct or the modified variant of it.
312 #[allow(clippy::collapsible_else_if)]
313 fn dyn_style(&self, name: &str) -> Option<Box<dyn StyleValue>> {
314 if let Some(entry_fn) = self.styles.get(name) {
315 let mut style = entry_fn(self);
316 if let Some(modify) = self.modify.get(name) {
317 style = modify(style, self);
318 }
319 Some(style)
320 } else {
321 if cfg!(debug_assertions) {
322 panic!("unknown style {:?}", name)
323 } else {
324 None
325 }
326 }
327 }
328}