1#![warn(missing_docs)]
22
23pub mod palette;
24pub mod style;
25pub mod tokens;
26
27use std::collections::BTreeMap;
28
29use crate::metrics::{ComponentSize, ThemeMetrics};
30use crate::shader::{ShaderHandle, StockShader, UniformBlock, UniformValue};
31use crate::tree::{Color, FontFamily, SurfaceRole};
32use crate::vector::IconMaterial;
33use palette::Palette;
34
35#[derive(Clone, Debug)]
37pub struct Theme {
38 palette: Palette,
39 metrics: ThemeMetrics,
40 surface: SurfaceTheme,
41 roles: BTreeMap<SurfaceRole, SurfaceTheme>,
42 icon_material: IconMaterial,
43 font_family: FontFamily,
44 mono_font_family: FontFamily,
45}
46
47impl Theme {
48 pub fn damascene_dark() -> Self {
52 Self::default().with_palette(Palette::damascene_dark())
53 }
54
55 pub fn damascene_light() -> Self {
60 Self::default().with_palette(Palette::damascene_light())
61 }
62
63 pub fn radix_slate_blue_dark() -> Self {
66 Self::default().with_palette(Palette::radix_slate_blue_dark())
67 }
68
69 pub fn radix_slate_blue_light() -> Self {
72 Self::default().with_palette(Palette::radix_slate_blue_light())
73 }
74
75 pub fn radix_sand_amber_dark() -> Self {
78 Self::default().with_palette(Palette::radix_sand_amber_dark())
79 }
80
81 pub fn radix_sand_amber_light() -> Self {
84 Self::default().with_palette(Palette::radix_sand_amber_light())
85 }
86
87 pub fn radix_mauve_violet_dark() -> Self {
90 Self::default().with_palette(Palette::radix_mauve_violet_dark())
91 }
92
93 pub fn radix_mauve_violet_light() -> Self {
96 Self::default().with_palette(Palette::radix_mauve_violet_light())
97 }
98
99 pub fn with_palette(mut self, palette: Palette) -> Self {
103 self.palette = palette;
104 self
105 }
106
107 pub fn palette(&self) -> &Palette {
109 &self.palette
110 }
111
112 pub fn metrics(&self) -> &ThemeMetrics {
114 &self.metrics
115 }
116
117 pub fn font_family(&self) -> FontFamily {
120 self.font_family
121 }
122
123 pub fn with_font_family(mut self, family: FontFamily) -> Self {
125 self.font_family = family;
126 self
127 }
128
129 pub fn mono_font_family(&self) -> FontFamily {
135 self.mono_font_family
136 }
137
138 pub fn with_mono_font_family(mut self, family: FontFamily) -> Self {
140 self.mono_font_family = family;
141 self
142 }
143
144 pub fn with_metrics(mut self, metrics: ThemeMetrics) -> Self {
146 self.metrics = metrics;
147 self
148 }
149
150 pub fn with_default_component_size(mut self, size: ComponentSize) -> Self {
152 self.metrics = self.metrics.with_default_component_size(size);
153 self
154 }
155
156 pub fn with_button_size(mut self, size: ComponentSize) -> Self {
159 self.metrics = self.metrics.with_button_size(size);
160 self
161 }
162
163 pub fn with_input_size(mut self, size: ComponentSize) -> Self {
166 self.metrics = self.metrics.with_input_size(size);
167 self
168 }
169
170 pub fn with_badge_size(mut self, size: ComponentSize) -> Self {
172 self.metrics = self.metrics.with_badge_size(size);
173 self
174 }
175
176 pub fn with_tab_size(mut self, size: ComponentSize) -> Self {
179 self.metrics = self.metrics.with_tab_size(size);
180 self
181 }
182
183 pub fn with_choice_size(mut self, size: ComponentSize) -> Self {
186 self.metrics = self.metrics.with_choice_size(size);
187 self
188 }
189
190 pub fn with_slider_size(mut self, size: ComponentSize) -> Self {
193 self.metrics = self.metrics.with_slider_size(size);
194 self
195 }
196
197 pub fn with_progress_size(mut self, size: ComponentSize) -> Self {
200 self.metrics = self.metrics.with_progress_size(size);
201 self
202 }
203
204 pub(crate) fn apply_metrics(&self, root: &mut crate::El) {
205 self.metrics.apply_node(root);
210 if !root.explicit_font_family {
211 root.font_family = self.font_family;
212 }
213 if !root.explicit_mono_font_family {
214 root.mono_font_family = self.mono_font_family;
215 }
216 for child in &mut root.children {
217 self.apply_metrics(child);
218 }
219 }
220
221 pub fn resolve(&self, c: Color) -> Color {
227 self.palette.resolve(c)
228 }
229
230 pub fn with_surface_shader(mut self, shader: &'static str) -> Self {
238 self.surface.handle = ShaderHandle::Custom(shader);
239 self.surface.rounded_rect_slots = true;
240 self
241 }
242
243 pub fn with_surface_uniform(mut self, key: &'static str, value: UniformValue) -> Self {
247 self.surface.uniforms.insert(key, value);
248 self
249 }
250
251 pub fn with_role_shader(mut self, role: SurfaceRole, shader: &'static str) -> Self {
254 self.role_mut(role).handle = ShaderHandle::Custom(shader);
255 self.role_mut(role).rounded_rect_slots = true;
256 self
257 }
258
259 pub fn with_role_uniform(
261 mut self,
262 role: SurfaceRole,
263 key: &'static str,
264 value: UniformValue,
265 ) -> Self {
266 self.role_mut(role).uniforms.insert(key, value);
267 self
268 }
269
270 pub fn with_icon_material(mut self, material: IconMaterial) -> Self {
274 self.icon_material = material;
275 self
276 }
277
278 pub fn icon_material(&self) -> IconMaterial {
280 self.icon_material
281 }
282
283 pub(crate) fn surface_handle(&self, role: SurfaceRole) -> ShaderHandle {
284 self.role_theme(role).handle
285 }
286
287 pub(crate) fn apply_surface_uniforms(&self, role: SurfaceRole, uniforms: &mut UniformBlock) {
288 let surface = self.role_theme(role);
289 uniforms
290 .entry("surface_role")
291 .or_insert(UniformValue::F32(role.uniform_id()));
292 apply_role_material(role, uniforms, &self.palette);
293 if surface.rounded_rect_slots {
294 add_rounded_rect_slots(uniforms);
295 }
296 for (key, value) in &surface.uniforms {
297 uniforms.entry(*key).or_insert(*value);
298 }
299 }
300
301 fn role_mut(&mut self, role: SurfaceRole) -> &mut SurfaceTheme {
302 self.roles
303 .entry(role)
304 .or_insert_with(|| self.surface.clone())
305 }
306
307 fn role_theme(&self, role: SurfaceRole) -> &SurfaceTheme {
308 self.roles.get(&role).unwrap_or(&self.surface)
309 }
310}
311
312impl Default for Theme {
313 fn default() -> Self {
314 Self {
315 palette: Palette::default(),
316 metrics: ThemeMetrics::default(),
317 surface: SurfaceTheme {
318 handle: ShaderHandle::Stock(StockShader::RoundedRect),
319 uniforms: UniformBlock::new(),
320 rounded_rect_slots: false,
321 },
322 roles: BTreeMap::new(),
323 icon_material: IconMaterial::Flat,
324 font_family: FontFamily::default(),
325 mono_font_family: FontFamily::JetBrainsMono,
326 }
327 }
328}
329
330#[derive(Clone, Debug)]
331struct SurfaceTheme {
332 handle: ShaderHandle,
333 uniforms: UniformBlock,
334 rounded_rect_slots: bool,
335}
336
337fn add_rounded_rect_slots(uniforms: &mut UniformBlock) {
338 if let Some(fill) = uniforms.get("fill").copied() {
339 uniforms.entry("vec_a").or_insert(fill);
340 }
341 if let Some(stroke) = uniforms.get("stroke").copied() {
342 uniforms.entry("vec_b").or_insert(stroke);
343 }
344
345 let stroke_width = as_f32(uniforms.get("stroke_width")).unwrap_or(0.0);
346 let radius = as_f32(uniforms.get("radius")).unwrap_or(0.0);
347 let shadow = as_f32(uniforms.get("shadow")).unwrap_or(0.0);
348 let focus_width = as_f32(uniforms.get("focus_width")).unwrap_or(0.0);
349 uniforms.entry("vec_c").or_insert(UniformValue::Vec4([
350 stroke_width,
351 radius,
352 shadow,
353 focus_width,
354 ]));
355
356 if let Some(focus_color) = uniforms.get("focus_color").copied() {
357 uniforms.entry("vec_d").or_insert(focus_color);
358 }
359}
360
361fn apply_role_material(role: SurfaceRole, uniforms: &mut UniformBlock, palette: &Palette) {
362 match role {
368 SurfaceRole::None => {}
369 SurfaceRole::Panel => {
370 set_color(uniforms, "stroke", tokens::BORDER.with_alpha_u8(210));
371 set_f32(uniforms, "stroke_width", 1.0);
372 set_f32(uniforms, "shadow", tokens::SHADOW_SM);
373 }
374 SurfaceRole::Raised => {
375 default_color(uniforms, "stroke", tokens::BORDER);
376 default_f32(uniforms, "stroke_width", 1.0);
377 default_f32(uniforms, "shadow", tokens::SHADOW_SM * 0.5);
378 }
379 SurfaceRole::Sunken | SurfaceRole::Input => {
380 set_color(
381 uniforms,
382 "fill",
383 palette.resolve(tokens::MUTED).darken(0.08),
384 );
385 set_color(uniforms, "stroke", tokens::INPUT.with_alpha_u8(190));
386 set_f32(uniforms, "stroke_width", 1.0);
387 set_f32(uniforms, "shadow", 0.0);
388 }
389 SurfaceRole::Popover => {
390 set_color(uniforms, "stroke", tokens::INPUT);
391 set_f32(uniforms, "stroke_width", 1.0);
392 set_f32(uniforms, "shadow", tokens::SHADOW_LG);
393 }
394 SurfaceRole::Selected => {
395 default_color(uniforms, "fill", tokens::PRIMARY.with_alpha_u8(28));
396 set_color(uniforms, "stroke", tokens::PRIMARY.with_alpha_u8(110));
397 set_f32(uniforms, "stroke_width", 1.0);
398 set_f32(uniforms, "shadow", 0.0);
399 }
400 SurfaceRole::Current => {
401 default_color(uniforms, "fill", tokens::ACCENT);
402 set_color(uniforms, "stroke", tokens::BORDER.with_alpha_u8(180));
403 set_f32(uniforms, "stroke_width", 1.0);
404 set_f32(uniforms, "shadow", 0.0);
405 }
406 SurfaceRole::Danger => {
407 set_color(uniforms, "stroke", tokens::DESTRUCTIVE);
408 set_f32(uniforms, "stroke_width", 1.0);
409 set_f32(uniforms, "shadow", 0.0);
410 }
411 }
412}
413
414fn default_color(uniforms: &mut UniformBlock, key: &'static str, color: Color) {
415 uniforms.entry(key).or_insert(UniformValue::Color(color));
416}
417
418fn set_color(uniforms: &mut UniformBlock, key: &'static str, color: Color) {
419 uniforms.insert(key, UniformValue::Color(color));
420}
421
422fn default_f32(uniforms: &mut UniformBlock, key: &'static str, value: f32) {
423 uniforms.entry(key).or_insert(UniformValue::F32(value));
424}
425
426fn set_f32(uniforms: &mut UniformBlock, key: &'static str, value: f32) {
427 uniforms.insert(key, UniformValue::F32(value));
428}
429
430fn as_f32(value: Option<&UniformValue>) -> Option<f32> {
431 match value {
432 Some(UniformValue::F32(v)) => Some(*v),
433 _ => None,
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::tree::column;
441 use crate::widgets::text::text;
442
443 #[test]
444 fn theme_can_route_icon_material() {
445 let theme = Theme::default().with_icon_material(IconMaterial::Relief);
446 assert_eq!(theme.icon_material(), IconMaterial::Relief);
447 }
448
449 #[test]
450 fn scrollbar_gutter_composes_with_padding_in_any_order() {
451 use crate::tree::Sides;
452
453 let mut a = crate::tree::scroll([text("x")])
455 .scrollbar_gutter()
456 .padding(Sides::all(8.0));
457 let mut b = crate::tree::scroll([text("x")])
459 .padding(Sides::all(8.0))
460 .scrollbar_gutter();
461 Theme::default().apply_metrics(&mut a);
462 Theme::default().apply_metrics(&mut b);
463
464 let expected = 8.0 + crate::tokens::SCROLLBAR_GUTTER;
465 assert_eq!(a.padding.right, expected);
466 assert_eq!(b.padding.right, expected);
467 assert_eq!(a.padding.left, 8.0);
469 assert_eq!(b.padding.top, 8.0);
470 }
471
472 #[test]
473 fn theme_font_family_applies_to_unset_text_nodes() {
474 let mut root = column([text("Themed")]);
475 Theme::default()
476 .with_font_family(FontFamily::Inter)
477 .apply_metrics(&mut root);
478
479 assert_eq!(root.children[0].font_family, FontFamily::Inter);
480 }
481
482 #[test]
483 fn explicit_font_family_survives_theme_default() {
484 let mut root = column([text("Pinned").roboto()]);
485 Theme::default()
486 .with_font_family(FontFamily::Inter)
487 .apply_metrics(&mut root);
488
489 assert_eq!(root.children[0].font_family, FontFamily::Roboto);
490 }
491
492 #[test]
493 fn theme_mono_font_family_applies_to_unset_text_nodes() {
494 let mut root = column([text("code()").code()]);
495 Theme::default().apply_metrics(&mut root);
496
497 assert_eq!(root.children[0].mono_font_family, FontFamily::JetBrainsMono);
501 }
502
503 #[test]
504 fn theme_mono_font_family_swap_is_independent_from_proportional() {
505 let mut root = column([text("body"), text("code()").code()]);
506 Theme::default()
507 .with_font_family(FontFamily::Inter)
508 .with_mono_font_family(FontFamily::Roboto)
509 .apply_metrics(&mut root);
510
511 assert_eq!(root.children[0].font_family, FontFamily::Inter);
512 assert_eq!(root.children[1].mono_font_family, FontFamily::Roboto);
513 assert_eq!(root.children[1].font_family, FontFamily::Inter);
515 }
516
517 #[test]
518 fn explicit_mono_font_family_survives_theme_default() {
519 let mut root = column([text("Pinned").code().jetbrains_mono()]);
520 Theme::default()
521 .with_mono_font_family(FontFamily::Roboto)
522 .apply_metrics(&mut root);
523
524 assert_eq!(root.children[0].mono_font_family, FontFamily::JetBrainsMono);
525 }
526}