rat_theme4/
salsa_theme.rs

1use crate::{Palette, is_log_style_define};
2use log::info;
3use ratatui::style::Style;
4use std::any::{Any, type_name};
5use std::collections::{HashMap, hash_map};
6use std::fmt::{Debug, Formatter};
7
8/// Categorization of themes.
9/// Helpful when extending an existing theme.
10#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum Category {
13    #[default]
14    Other,
15    /// Dark theme.
16    Dark,
17    /// Light theme.
18    Light,
19    /// Shell theme. Themes of this category rely on background colors sparingly
20    /// and use any default the terminal itself provides.
21    Shell,
22}
23
24trait StyleValue: Any + Debug {}
25impl<T> StyleValue for T where T: Any + Debug {}
26
27type Entry = Box<dyn Fn(&SalsaTheme) -> Box<dyn StyleValue> + 'static>;
28type Modify = Box<dyn Fn(Box<dyn Any>, &SalsaTheme) -> Box<dyn StyleValue> + 'static>;
29
30///
31/// SalsaTheme holds any predefined styles for the UI.  
32///
33/// The foremost usage is as a store of named [Style](ratatui::style::Style)s.
34/// It can also hold the structured styles used by rat-widget's.
35/// Or really any value that can be produced by a closure.
36///
37/// It uses a flat naming scheme and doesn't cascade upwards at all.
38#[derive(Default)]
39pub struct SalsaTheme {
40    pub name: String,
41    pub cat: Category,
42    pub p: Palette,
43    styles: HashMap<&'static str, Entry>,
44    modify: HashMap<&'static str, Modify>,
45}
46
47impl Debug for SalsaTheme {
48    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("SalsaTheme")
50            .field("name", &self.name)
51            .field("cat", &self.cat)
52            .field("palette", &self.p)
53            .field("styles", &self.styles.keys().collect::<Vec<_>>())
54            .field("modify", &self.modify.keys().collect::<Vec<_>>())
55            .finish()
56    }
57}
58
59impl SalsaTheme {
60    /// Create an empty theme with a given color palette.
61    pub fn new(name: impl Into<String>, cat: Category, p: Palette) -> Self {
62        Self {
63            name: name.into(),
64            cat,
65            p,
66            styles: Default::default(),
67            modify: Default::default(),
68        }
69    }
70
71    /// Some display name.
72    pub fn name(&self) -> &str {
73        &self.name
74    }
75
76    /// Define a style as a plain [Style].
77    pub fn define(&mut self, name: &'static str, style: Style) {
78        let boxed = Box::new(move |_: &SalsaTheme| -> Box<dyn StyleValue> { Box::new(style) });
79        if is_log_style_define() {
80            info!("salsa-style: {:?}->{:?}", name, style);
81        }
82        self.styles.insert(name, boxed);
83    }
84
85    /// Define a style a struct that will be cloned for every query.
86    pub fn define_clone(&mut self, name: &'static str, sample: impl Clone + Any + Debug + 'static) {
87        let boxed = Box::new(move |_th: &SalsaTheme| -> Box<dyn StyleValue> {
88            Box::new(sample.clone()) //
89        });
90        if is_log_style_define() {
91            info!("salsa-style: {:?}->{:?}", name, boxed(self));
92        }
93        self.styles.insert(name, boxed);
94    }
95
96    /// Define a style as a call to a constructor fn.
97    ///
98    /// The constructor gets access to all previously initialized styles.
99    pub fn define_fn<O: Any + Debug>(
100        &mut self,
101        name: &'static str,
102        create: impl Fn(&SalsaTheme) -> O + 'static,
103    ) {
104        let boxed = Box::new(move |th: &SalsaTheme| -> Box<dyn StyleValue> {
105            Box::new(create(th)) //
106        });
107        if is_log_style_define() {
108            info!("salsa-style: {:?}->{:?}", name, boxed(self));
109        }
110        self.styles.insert(name, boxed);
111    }
112
113    /// Define a style as a call to a constructor fn.
114    ///
115    /// This one takes no arguments, this is nice to set Widget::default
116    /// as the style-fn.
117    pub fn define_fn0<O: Any + Debug>(
118        &mut self,
119        name: &'static str,
120        create: impl Fn() -> O + 'static,
121    ) {
122        let boxed = Box::new(move |_th: &SalsaTheme| -> Box<dyn StyleValue> {
123            Box::new(create()) //
124        });
125        if is_log_style_define() {
126            info!("salsa-style: {:?}->{:?}", name, boxed(self));
127        }
128        self.styles.insert(name, boxed);
129    }
130
131    /// Add a modification of a defined style.
132    pub fn modify<O: Any + Debug>(
133        &mut self,
134        name: &'static str,
135        modify: impl Fn(Box<dyn Any>, &SalsaTheme) -> Box<O> + 'static,
136    ) {
137        let boxed = Box::new(
138            move |v: Box<dyn Any>, th: &SalsaTheme| -> Box<dyn StyleValue> { modify(v, th) },
139        );
140        match self.modify.entry(name) {
141            hash_map::Entry::Occupied(_) => {
142                panic!("salsa-theme: only a single modification is possible");
143            }
144            hash_map::Entry::Vacant(entry) => {
145                entry.insert(boxed);
146            }
147        };
148    }
149
150    /// Get one of the defined ratatui-Styles.
151    ///
152    /// This is the same as the single [style] function, it just
153    /// fixes the return-type to style. This is useful if the
154    /// receiver is declared as `impl Into<Style>`.
155    ///
156    /// It downcasts the stored value to the required out type.
157    /// This may fail.
158    ///
159    /// * When debug_assertions are enabled it will panic when
160    ///   called with an unknown style name, or if the downcast
161    ///   to the out type fails.
162    /// * Otherwise, it will return the default value of the out type.
163    pub fn style_style(&self, name: &str) -> Style
164    where
165        Self: Sized,
166    {
167        self.style::<Style>(name)
168    }
169
170    /// Get any of the defined styles.
171    ///
172    /// It downcasts the stored value to the required out type.
173    /// This may fail.
174    ///
175    /// * When debug_assertions are enabled it will panic when
176    ///   called with an unknown style name, or if the downcast
177    ///   to the out type fails.
178    /// * Otherwise, it will return the default value of the out type.
179    pub fn style<O: Default + Sized + 'static>(&self, name: &str) -> O
180    where
181        Self: Sized,
182    {
183        if cfg!(debug_assertions) {
184            let style = match self.dyn_style(name) {
185                Some(v) => v,
186                None => {
187                    panic!("unknown widget {:?}", name)
188                }
189            };
190            let any_style = style as Box<dyn Any>;
191            let style = match any_style.downcast::<O>() {
192                Ok(v) => v,
193                Err(_) => {
194                    let style = self.dyn_style(name).expect("style");
195                    panic!(
196                        "downcast fails for '{}' to {}: {:?}",
197                        name,
198                        type_name::<O>(),
199                        style
200                    );
201                }
202            };
203            *style
204        } else {
205            let Some(style) = self.dyn_style(name) else {
206                return O::default();
207            };
208            let any_style = style as Box<dyn Any>;
209            let Ok(style) = any_style.downcast::<O>() else {
210                return O::default();
211            };
212            *style
213        }
214    }
215
216    #[allow(clippy::collapsible_else_if)]
217    fn dyn_style(&self, name: &str) -> Option<Box<dyn StyleValue>> {
218        if let Some(entry_fn) = self.styles.get(name) {
219            let mut style = entry_fn(self);
220            if let Some(modify) = self.modify.get(name) {
221                style = modify(style, self);
222            }
223            Some(style)
224        } else {
225            if cfg!(debug_assertions) {
226                panic!("unknown style {:?}", name)
227            } else {
228                None
229            }
230        }
231    }
232}