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