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}