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