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}