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