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}