1use std::collections::BTreeMap;
15
16use crate::metrics::{ComponentSize, ThemeMetrics};
17use crate::palette::Palette;
18use crate::shader::{ShaderHandle, StockShader, UniformBlock, UniformValue};
19use crate::tokens;
20use crate::tree::{Color, FontFamily, SurfaceRole};
21use crate::vector::IconMaterial;
22
23#[derive(Clone, Debug)]
25pub struct Theme {
26 palette: Palette,
27 metrics: ThemeMetrics,
28 surface: SurfaceTheme,
29 roles: BTreeMap<SurfaceRole, SurfaceTheme>,
30 icon_material: IconMaterial,
31 font_family: FontFamily,
32 mono_font_family: FontFamily,
33}
34
35impl Theme {
36 pub fn aetna_dark() -> Self {
40 Self::default().with_palette(Palette::aetna_dark())
41 }
42
43 pub fn aetna_light() -> Self {
48 Self::default().with_palette(Palette::aetna_light())
49 }
50
51 pub fn radix_slate_blue_dark() -> Self {
54 Self::default().with_palette(Palette::radix_slate_blue_dark())
55 }
56
57 pub fn radix_slate_blue_light() -> Self {
60 Self::default().with_palette(Palette::radix_slate_blue_light())
61 }
62
63 pub fn radix_sand_amber_dark() -> Self {
66 Self::default().with_palette(Palette::radix_sand_amber_dark())
67 }
68
69 pub fn radix_sand_amber_light() -> Self {
72 Self::default().with_palette(Palette::radix_sand_amber_light())
73 }
74
75 pub fn radix_mauve_violet_dark() -> Self {
78 Self::default().with_palette(Palette::radix_mauve_violet_dark())
79 }
80
81 pub fn radix_mauve_violet_light() -> Self {
84 Self::default().with_palette(Palette::radix_mauve_violet_light())
85 }
86
87 pub fn with_palette(mut self, palette: Palette) -> Self {
91 self.palette = palette;
92 self
93 }
94
95 pub fn palette(&self) -> &Palette {
97 &self.palette
98 }
99
100 pub fn metrics(&self) -> &ThemeMetrics {
102 &self.metrics
103 }
104
105 pub fn font_family(&self) -> FontFamily {
108 self.font_family
109 }
110
111 pub fn with_font_family(mut self, family: FontFamily) -> Self {
113 self.font_family = family;
114 self
115 }
116
117 pub fn mono_font_family(&self) -> FontFamily {
123 self.mono_font_family
124 }
125
126 pub fn with_mono_font_family(mut self, family: FontFamily) -> Self {
128 self.mono_font_family = family;
129 self
130 }
131
132 pub fn with_metrics(mut self, metrics: ThemeMetrics) -> Self {
134 self.metrics = metrics;
135 self
136 }
137
138 pub fn with_default_component_size(mut self, size: ComponentSize) -> Self {
140 self.metrics = self.metrics.with_default_component_size(size);
141 self
142 }
143
144 pub fn with_button_size(mut self, size: ComponentSize) -> Self {
145 self.metrics = self.metrics.with_button_size(size);
146 self
147 }
148
149 pub fn with_input_size(mut self, size: ComponentSize) -> Self {
150 self.metrics = self.metrics.with_input_size(size);
151 self
152 }
153
154 pub fn with_badge_size(mut self, size: ComponentSize) -> Self {
155 self.metrics = self.metrics.with_badge_size(size);
156 self
157 }
158
159 pub fn with_tab_size(mut self, size: ComponentSize) -> Self {
160 self.metrics = self.metrics.with_tab_size(size);
161 self
162 }
163
164 pub fn with_choice_size(mut self, size: ComponentSize) -> Self {
165 self.metrics = self.metrics.with_choice_size(size);
166 self
167 }
168
169 pub fn with_slider_size(mut self, size: ComponentSize) -> Self {
170 self.metrics = self.metrics.with_slider_size(size);
171 self
172 }
173
174 pub fn with_progress_size(mut self, size: ComponentSize) -> Self {
175 self.metrics = self.metrics.with_progress_size(size);
176 self
177 }
178
179 pub(crate) fn apply_metrics(&self, root: &mut crate::El) {
180 self.metrics.apply_to_tree(root);
181 apply_font_family(root, self.font_family);
182 apply_mono_font_family(root, self.mono_font_family);
183 }
184
185 pub fn resolve(&self, c: Color) -> Color {
191 self.palette.resolve(c)
192 }
193
194 pub fn with_surface_shader(mut self, shader: &'static str) -> Self {
202 self.surface.handle = ShaderHandle::Custom(shader);
203 self.surface.rounded_rect_slots = true;
204 self
205 }
206
207 pub fn with_surface_uniform(mut self, key: &'static str, value: UniformValue) -> Self {
211 self.surface.uniforms.insert(key, value);
212 self
213 }
214
215 pub fn with_role_shader(mut self, role: SurfaceRole, shader: &'static str) -> Self {
218 self.role_mut(role).handle = ShaderHandle::Custom(shader);
219 self.role_mut(role).rounded_rect_slots = true;
220 self
221 }
222
223 pub fn with_role_uniform(
225 mut self,
226 role: SurfaceRole,
227 key: &'static str,
228 value: UniformValue,
229 ) -> Self {
230 self.role_mut(role).uniforms.insert(key, value);
231 self
232 }
233
234 pub fn with_icon_material(mut self, material: IconMaterial) -> Self {
238 self.icon_material = material;
239 self
240 }
241
242 pub fn icon_material(&self) -> IconMaterial {
243 self.icon_material
244 }
245
246 pub(crate) fn surface_handle(&self, role: SurfaceRole) -> ShaderHandle {
247 self.role_theme(role).handle
248 }
249
250 pub(crate) fn apply_surface_uniforms(&self, role: SurfaceRole, uniforms: &mut UniformBlock) {
251 let surface = self.role_theme(role);
252 uniforms
253 .entry("surface_role")
254 .or_insert(UniformValue::F32(role.uniform_id()));
255 apply_role_material(role, uniforms, &self.palette);
256 if surface.rounded_rect_slots {
257 add_rounded_rect_slots(uniforms);
258 }
259 for (key, value) in &surface.uniforms {
260 uniforms.entry(*key).or_insert(*value);
261 }
262 }
263
264 fn role_mut(&mut self, role: SurfaceRole) -> &mut SurfaceTheme {
265 self.roles
266 .entry(role)
267 .or_insert_with(|| self.surface.clone())
268 }
269
270 fn role_theme(&self, role: SurfaceRole) -> &SurfaceTheme {
271 self.roles.get(&role).unwrap_or(&self.surface)
272 }
273}
274
275impl Default for Theme {
276 fn default() -> Self {
277 Self {
278 palette: Palette::default(),
279 metrics: ThemeMetrics::default(),
280 surface: SurfaceTheme {
281 handle: ShaderHandle::Stock(StockShader::RoundedRect),
282 uniforms: UniformBlock::new(),
283 rounded_rect_slots: false,
284 },
285 roles: BTreeMap::new(),
286 icon_material: IconMaterial::Flat,
287 font_family: FontFamily::default(),
288 mono_font_family: FontFamily::JetBrainsMono,
289 }
290 }
291}
292
293#[derive(Clone, Debug)]
294struct SurfaceTheme {
295 handle: ShaderHandle,
296 uniforms: UniformBlock,
297 rounded_rect_slots: bool,
298}
299
300fn add_rounded_rect_slots(uniforms: &mut UniformBlock) {
301 if let Some(fill) = uniforms.get("fill").copied() {
302 uniforms.entry("vec_a").or_insert(fill);
303 }
304 if let Some(stroke) = uniforms.get("stroke").copied() {
305 uniforms.entry("vec_b").or_insert(stroke);
306 }
307
308 let stroke_width = as_f32(uniforms.get("stroke_width")).unwrap_or(0.0);
309 let radius = as_f32(uniforms.get("radius")).unwrap_or(0.0);
310 let shadow = as_f32(uniforms.get("shadow")).unwrap_or(0.0);
311 let focus_width = as_f32(uniforms.get("focus_width")).unwrap_or(0.0);
312 uniforms.entry("vec_c").or_insert(UniformValue::Vec4([
313 stroke_width,
314 radius,
315 shadow,
316 focus_width,
317 ]));
318
319 if let Some(focus_color) = uniforms.get("focus_color").copied() {
320 uniforms.entry("vec_d").or_insert(focus_color);
321 }
322}
323
324fn apply_role_material(role: SurfaceRole, uniforms: &mut UniformBlock, palette: &Palette) {
325 match role {
331 SurfaceRole::None => {}
332 SurfaceRole::Panel => {
333 set_color(uniforms, "stroke", tokens::BORDER.with_alpha(210));
334 set_f32(uniforms, "stroke_width", 1.0);
335 set_f32(uniforms, "shadow", tokens::SHADOW_SM);
336 }
337 SurfaceRole::Raised => {
338 default_color(uniforms, "stroke", tokens::BORDER);
339 default_f32(uniforms, "stroke_width", 1.0);
340 default_f32(uniforms, "shadow", tokens::SHADOW_SM * 0.5);
341 }
342 SurfaceRole::Sunken | SurfaceRole::Input => {
343 set_color(
344 uniforms,
345 "fill",
346 palette.resolve(tokens::MUTED).darken(0.08),
347 );
348 set_color(uniforms, "stroke", tokens::INPUT.with_alpha(190));
349 set_f32(uniforms, "stroke_width", 1.0);
350 set_f32(uniforms, "shadow", 0.0);
351 }
352 SurfaceRole::Popover => {
353 set_color(uniforms, "stroke", tokens::INPUT);
354 set_f32(uniforms, "stroke_width", 1.0);
355 set_f32(uniforms, "shadow", tokens::SHADOW_LG);
356 }
357 SurfaceRole::Selected => {
358 default_color(uniforms, "fill", tokens::PRIMARY.with_alpha(28));
359 set_color(uniforms, "stroke", tokens::PRIMARY.with_alpha(110));
360 set_f32(uniforms, "stroke_width", 1.0);
361 set_f32(uniforms, "shadow", 0.0);
362 }
363 SurfaceRole::Current => {
364 default_color(uniforms, "fill", tokens::ACCENT);
365 set_color(uniforms, "stroke", tokens::BORDER.with_alpha(180));
366 set_f32(uniforms, "stroke_width", 1.0);
367 set_f32(uniforms, "shadow", 0.0);
368 }
369 SurfaceRole::Danger => {
370 set_color(uniforms, "stroke", tokens::DESTRUCTIVE);
371 set_f32(uniforms, "stroke_width", 1.0);
372 set_f32(uniforms, "shadow", 0.0);
373 }
374 }
375}
376
377fn apply_font_family(node: &mut crate::El, family: FontFamily) {
378 if !node.explicit_font_family {
379 node.font_family = family;
380 }
381 for child in &mut node.children {
382 apply_font_family(child, family);
383 }
384}
385
386fn apply_mono_font_family(node: &mut crate::El, family: FontFamily) {
387 if !node.explicit_mono_font_family {
388 node.mono_font_family = family;
389 }
390 for child in &mut node.children {
391 apply_mono_font_family(child, family);
392 }
393}
394
395fn default_color(uniforms: &mut UniformBlock, key: &'static str, color: Color) {
396 uniforms.entry(key).or_insert(UniformValue::Color(color));
397}
398
399fn set_color(uniforms: &mut UniformBlock, key: &'static str, color: Color) {
400 uniforms.insert(key, UniformValue::Color(color));
401}
402
403fn default_f32(uniforms: &mut UniformBlock, key: &'static str, value: f32) {
404 uniforms.entry(key).or_insert(UniformValue::F32(value));
405}
406
407fn set_f32(uniforms: &mut UniformBlock, key: &'static str, value: f32) {
408 uniforms.insert(key, UniformValue::F32(value));
409}
410
411fn as_f32(value: Option<&UniformValue>) -> Option<f32> {
412 match value {
413 Some(UniformValue::F32(v)) => Some(*v),
414 _ => None,
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::tree::column;
422 use crate::widgets::text::text;
423
424 #[test]
425 fn theme_can_route_icon_material() {
426 let theme = Theme::default().with_icon_material(IconMaterial::Relief);
427 assert_eq!(theme.icon_material(), IconMaterial::Relief);
428 }
429
430 #[test]
431 fn theme_font_family_applies_to_unset_text_nodes() {
432 let mut root = column([text("Themed")]);
433 Theme::default()
434 .with_font_family(FontFamily::Inter)
435 .apply_metrics(&mut root);
436
437 assert_eq!(root.children[0].font_family, FontFamily::Inter);
438 }
439
440 #[test]
441 fn explicit_font_family_survives_theme_default() {
442 let mut root = column([text("Pinned").roboto()]);
443 Theme::default()
444 .with_font_family(FontFamily::Inter)
445 .apply_metrics(&mut root);
446
447 assert_eq!(root.children[0].font_family, FontFamily::Roboto);
448 }
449
450 #[test]
451 fn theme_mono_font_family_applies_to_unset_text_nodes() {
452 let mut root = column([text("code()").code()]);
453 Theme::default().apply_metrics(&mut root);
454
455 assert_eq!(root.children[0].mono_font_family, FontFamily::JetBrainsMono);
459 }
460
461 #[test]
462 fn theme_mono_font_family_swap_is_independent_from_proportional() {
463 let mut root = column([text("body"), text("code()").code()]);
464 Theme::default()
465 .with_font_family(FontFamily::Inter)
466 .with_mono_font_family(FontFamily::Roboto)
467 .apply_metrics(&mut root);
468
469 assert_eq!(root.children[0].font_family, FontFamily::Inter);
470 assert_eq!(root.children[1].mono_font_family, FontFamily::Roboto);
471 assert_eq!(root.children[1].font_family, FontFamily::Inter);
473 }
474
475 #[test]
476 fn explicit_mono_font_family_survives_theme_default() {
477 let mut root = column([text("Pinned").code().jetbrains_mono()]);
478 Theme::default()
479 .with_mono_font_family(FontFamily::Roboto)
480 .apply_metrics(&mut root);
481
482 assert_eq!(root.children[0].mono_font_family, FontFamily::JetBrainsMono);
483 }
484}