1use crate::tokens::{Palette, Radius, Spacing};
2use iced::Color;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum ColorToken {
7 Background,
8 Foreground,
9 Card,
10 CardForeground,
11 Popover,
12 PopoverForeground,
13 Border,
14 Input,
15 Ring,
16 Primary,
17 PrimaryForeground,
18 Secondary,
19 SecondaryForeground,
20 Accent,
21 AccentForeground,
22 Muted,
23 MutedForeground,
24 Destructive,
25 DestructiveForeground,
26 Chart1,
27 Chart2,
28 Chart3,
29 Chart4,
30 Chart5,
31 Sidebar,
32 SidebarForeground,
33 SidebarPrimary,
34 SidebarPrimaryForeground,
35 SidebarAccent,
36 SidebarAccentForeground,
37 SidebarBorder,
38 SidebarRing,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum RadiusToken {
43 Sm,
44 Md,
45 Lg,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum SpacingToken {
50 Xs,
51 Sm,
52 Md,
53 Lg,
54}
55
56pub trait ThemeTokensSource {
57 fn color(&self, token: ColorToken) -> Color;
58 fn radius(&self, token: RadiusToken) -> f32;
59 fn spacing(&self, token: SpacingToken) -> f32;
60
61 fn styles(&self) -> ThemeStyles {
62 ThemeStyles::default()
63 }
64
65 fn registry(&self) -> ThemeTokenRegistry {
66 ThemeTokenRegistry::default()
67 }
68}
69
70#[derive(Clone, Debug, Default)]
71pub struct ThemeTokenRegistry {
72 pub colors: BTreeMap<String, Color>,
73 pub numbers: BTreeMap<String, f32>,
74 pub durations_ms: BTreeMap<String, u64>,
75 pub strings: BTreeMap<String, String>,
76}
77
78impl ThemeTokenRegistry {
79 pub fn set_color(&mut self, key: impl Into<String>, value: Color) {
80 self.colors.insert(key.into(), value);
81 }
82
83 pub fn set_number(&mut self, key: impl Into<String>, value: f32) {
84 self.numbers.insert(key.into(), value);
85 }
86
87 pub fn set_duration_ms(&mut self, key: impl Into<String>, value: u64) {
88 self.durations_ms.insert(key.into(), value);
89 }
90
91 pub fn set_string(&mut self, key: impl Into<String>, value: impl Into<String>) {
92 self.strings.insert(key.into(), value.into());
93 }
94
95 pub fn color(&self, key: &str) -> Option<Color> {
96 self.colors.get(key).copied()
97 }
98
99 pub fn number(&self, key: &str) -> Option<f32> {
100 self.numbers.get(key).copied()
101 }
102
103 pub fn duration_ms(&self, key: &str) -> Option<u64> {
104 self.durations_ms.get(key).copied()
105 }
106
107 pub fn string(&self, key: &str) -> Option<&str> {
108 self.strings.get(key).map(String::as_str)
109 }
110}
111
112#[derive(Clone, Copy, Debug)]
113pub struct ShadowStyle {
114 pub opacity: f32,
115 pub offset_y: f32,
116 pub blur_radius: f32,
117}
118
119impl Default for ShadowStyle {
120 fn default() -> Self {
121 Self {
122 opacity: 0.12,
123 offset_y: 4.0,
124 blur_radius: 12.0,
125 }
126 }
127}
128
129#[derive(Clone, Copy, Debug)]
130pub struct CommandStyleTokens {
131 pub min_width: f32,
132 pub input_padding_y: f32,
133 pub input_padding_x: f32,
134 pub list_item_gap: f32,
135 pub group_item_gap: f32,
136 pub list_max_height: f32,
137}
138
139impl Default for CommandStyleTokens {
140 fn default() -> Self {
141 Self {
142 min_width: 280.0,
143 input_padding_y: 8.0,
144 input_padding_x: 12.0,
145 list_item_gap: 4.0,
146 group_item_gap: 2.0,
147 list_max_height: 300.0,
148 }
149 }
150}
151
152#[derive(Clone, Copy, Debug)]
153pub struct TabsStyleTokens {
154 pub list_padding_size1: f32,
155 pub list_padding_size2: f32,
156 pub gap: f32,
157 pub line_gap: f32,
158 pub indicator_height: f32,
159 pub active_pill_shadow: ShadowStyle,
160}
161
162impl Default for TabsStyleTokens {
163 fn default() -> Self {
164 Self {
165 list_padding_size1: 2.0,
166 list_padding_size2: 3.0,
167 gap: 6.0,
168 line_gap: 6.0,
169 indicator_height: 2.0,
170 active_pill_shadow: ShadowStyle {
171 opacity: 0.18,
172 offset_y: 1.0,
173 blur_radius: 6.0,
174 },
175 }
176 }
177}
178
179#[derive(Clone, Copy, Debug)]
180pub struct SwitchStyleTokens {
181 pub base_height: f32,
182 pub base_width: f32,
183 pub base_thumb: f32,
184 pub track_shadow: ShadowStyle,
185 pub animation_ms: u64,
186}
187
188impl Default for SwitchStyleTokens {
189 fn default() -> Self {
190 Self {
191 base_height: 18.4,
192 base_width: 32.0,
193 base_thumb: 14.0,
194 track_shadow: ShadowStyle {
195 opacity: 0.05,
196 offset_y: 1.0,
197 blur_radius: 2.0,
198 },
199 animation_ms: 150,
200 }
201 }
202}
203
204#[derive(Clone, Copy, Debug)]
205pub struct ToastStyleTokens {
206 pub width: f32,
207 pub max_width: f32,
208 pub height: f32,
209 pub horizontal_margin: f32,
210 pub vertical_margin: f32,
211 pub narrow_viewport_padding: f32,
212 pub gap: f32,
213 pub max_visible: usize,
214 pub max_viewport_height_ratio: f32,
215 pub close_inset: f32,
216 pub close_size: f32,
217 pub close_glyph_nudge_x: f32,
218 pub close_glyph_nudge_y: f32,
219 pub animation_ms: u64,
220 pub shadow: ShadowStyle,
221}
222
223impl Default for ToastStyleTokens {
224 fn default() -> Self {
225 Self {
226 width: 360.0,
227 max_width: 452.0,
228 height: 64.0,
229 horizontal_margin: 16.0,
230 vertical_margin: 16.0,
231 narrow_viewport_padding: 8.0,
232 gap: 8.0,
233 max_visible: 3,
234 max_viewport_height_ratio: 0.618,
235 close_inset: 10.0,
236 close_size: 14.0,
237 close_glyph_nudge_x: 1.0,
238 close_glyph_nudge_y: 1.0,
239 animation_ms: 300,
240 shadow: ShadowStyle {
241 opacity: 0.15,
242 offset_y: 12.0,
243 blur_radius: 28.0,
244 },
245 }
246 }
247}
248
249#[derive(Clone, Copy, Debug)]
250pub struct MenuStyleTokens {
251 pub border_width: f32,
252 pub shadow: ShadowStyle,
253}
254
255impl Default for MenuStyleTokens {
256 fn default() -> Self {
257 Self {
258 border_width: 1.0,
259 shadow: ShadowStyle::default(),
260 }
261 }
262}
263
264#[derive(Clone, Copy, Debug)]
265pub struct InputStyleTokens {
266 pub size1_padding_y: f32,
267 pub size1_padding_x: f32,
268 pub size2_padding_y: f32,
269 pub size2_padding_x: f32,
270 pub size3_padding_y: f32,
271 pub size3_padding_x: f32,
272 pub border_width: f32,
273 pub focused_border_width: f32,
274}
275
276impl Default for InputStyleTokens {
277 fn default() -> Self {
278 Self {
279 size1_padding_y: 6.0,
280 size1_padding_x: 10.0,
281 size2_padding_y: 8.0,
282 size2_padding_x: 12.0,
283 size3_padding_y: 10.0,
284 size3_padding_x: 14.0,
285 border_width: 1.0,
286 focused_border_width: 1.5,
287 }
288 }
289}
290
291#[derive(Clone, Copy, Debug)]
292pub struct SidebarStyleTokens {
293 pub expanded_width: f32,
294 pub collapsed_width: f32,
295 pub header_footer_padding: f32,
296 pub content_padding: f32,
297 pub group_spacing: f32,
298 pub menu_spacing: f32,
299}
300
301impl Default for SidebarStyleTokens {
302 fn default() -> Self {
303 Self {
304 expanded_width: 240.0,
305 collapsed_width: 64.0,
306 header_footer_padding: 12.0,
307 content_padding: 8.0,
308 group_spacing: 8.0,
309 menu_spacing: 4.0,
310 }
311 }
312}
313
314#[derive(Clone, Copy, Debug)]
315pub struct FieldStyleTokens {
316 pub spacing: f32,
317 pub label_size: u32,
318 pub description_size: u32,
319 pub error_size: u32,
320}
321
322impl Default for FieldStyleTokens {
323 fn default() -> Self {
324 Self {
325 spacing: 4.0,
326 label_size: 14,
327 description_size: 12,
328 error_size: 12,
329 }
330 }
331}
332
333#[derive(Clone, Copy, Debug)]
334pub struct NavigationMenuStyleTokens {
335 pub border_width: f32,
336 pub shadow: ShadowStyle,
337}
338
339impl Default for NavigationMenuStyleTokens {
340 fn default() -> Self {
341 Self {
342 border_width: 1.0,
343 shadow: ShadowStyle {
344 opacity: 0.18,
345 offset_y: 8.0,
346 blur_radius: 22.0,
347 },
348 }
349 }
350}
351
352#[derive(Clone, Copy, Debug)]
353pub struct ScrollAreaStyleTokens {
354 pub size1_scrollbar_width: f32,
355 pub size2_scrollbar_width: f32,
356 pub size3_scrollbar_width: f32,
357 pub default_scrollbar_margin: f32,
358}
359
360impl Default for ScrollAreaStyleTokens {
361 fn default() -> Self {
362 Self {
363 size1_scrollbar_width: 4.0,
364 size2_scrollbar_width: 8.0,
365 size3_scrollbar_width: 12.0,
366 default_scrollbar_margin: 4.0,
367 }
368 }
369}
370
371#[derive(Clone, Copy, Debug)]
372pub struct EmptyStyleTokens {
373 pub root_gap: f32,
374 pub root_padding: f32,
375 pub root_max_width: f32,
376 pub root_min_height: f32,
377 pub header_gap: f32,
378 pub header_max_width: f32,
379 pub media_size: f32,
380 pub media_icon_size: f32,
381 pub title_size: f32,
382 pub description_size: f32,
383 pub description_max_width: f32,
384 pub content_gap: f32,
385 pub content_max_width: f32,
386}
387
388impl Default for EmptyStyleTokens {
389 fn default() -> Self {
390 Self {
391 root_gap: 24.0,
392 root_padding: 24.0,
393 root_max_width: 384.0,
394 root_min_height: 0.0,
395 header_gap: 8.0,
396 header_max_width: 384.0,
397 media_size: 40.0,
398 media_icon_size: 24.0,
399 title_size: 18.0,
400 description_size: 14.0,
401 description_max_width: 320.0,
402 content_gap: 16.0,
403 content_max_width: 384.0,
404 }
405 }
406}
407
408#[derive(Clone, Copy, Debug, Default)]
409pub struct ThemeStyles {
410 pub command: CommandStyleTokens,
411 pub tabs: TabsStyleTokens,
412 pub switch: SwitchStyleTokens,
413 pub toast: ToastStyleTokens,
414 pub menu: MenuStyleTokens,
415 pub input: InputStyleTokens,
416 pub sidebar: SidebarStyleTokens,
417 pub field: FieldStyleTokens,
418 pub navigation_menu: NavigationMenuStyleTokens,
419 pub scroll_area: ScrollAreaStyleTokens,
420 pub empty: EmptyStyleTokens,
421}
422
423#[derive(Clone, Debug)]
424pub struct Theme {
425 pub palette: Palette,
426 pub radius: Radius,
427 pub spacing: Spacing,
428 pub styles: ThemeStyles,
429 pub registry: ThemeTokenRegistry,
430}
431
432impl Theme {
433 pub fn from_parts(
434 palette: Palette,
435 radius: Radius,
436 spacing: Spacing,
437 styles: ThemeStyles,
438 ) -> Self {
439 Self::from_parts_with_registry(
440 palette,
441 radius,
442 spacing,
443 styles,
444 ThemeTokenRegistry::default(),
445 )
446 }
447
448 pub fn from_parts_with_registry(
449 palette: Palette,
450 radius: Radius,
451 spacing: Spacing,
452 styles: ThemeStyles,
453 registry: ThemeTokenRegistry,
454 ) -> Self {
455 Self {
456 palette,
457 radius,
458 spacing,
459 styles,
460 registry,
461 }
462 }
463
464 pub fn light() -> Self {
465 Self::from_parts(
466 Palette::light(),
467 Radius::default(),
468 Spacing::default(),
469 ThemeStyles::default(),
470 )
471 }
472
473 pub fn dark() -> Self {
474 Self::from_parts(
475 Palette::dark(),
476 Radius::default(),
477 Spacing::default(),
478 ThemeStyles::default(),
479 )
480 }
481
482 pub fn with_palette(palette: Palette) -> Self {
483 Self::from_parts(
484 palette,
485 Radius::default(),
486 Spacing::default(),
487 ThemeStyles::default(),
488 )
489 }
490
491 pub fn with_radius(mut self, radius: Radius) -> Self {
492 self.radius = radius;
493 self
494 }
495
496 pub fn with_spacing(mut self, spacing: Spacing) -> Self {
497 self.spacing = spacing;
498 self
499 }
500
501 pub fn with_styles(mut self, styles: ThemeStyles) -> Self {
502 self.styles = styles;
503 self
504 }
505
506 pub fn with_registry(mut self, registry: ThemeTokenRegistry) -> Self {
507 self.registry = registry;
508 self
509 }
510
511 pub fn from_tokens(source: &impl ThemeTokensSource) -> Self {
512 let palette = Palette {
513 background: source.color(ColorToken::Background),
514 foreground: source.color(ColorToken::Foreground),
515 card: source.color(ColorToken::Card),
516 card_foreground: source.color(ColorToken::CardForeground),
517 popover: source.color(ColorToken::Popover),
518 popover_foreground: source.color(ColorToken::PopoverForeground),
519 border: source.color(ColorToken::Border),
520 input: source.color(ColorToken::Input),
521 ring: source.color(ColorToken::Ring),
522 primary: source.color(ColorToken::Primary),
523 primary_foreground: source.color(ColorToken::PrimaryForeground),
524 secondary: source.color(ColorToken::Secondary),
525 secondary_foreground: source.color(ColorToken::SecondaryForeground),
526 accent: source.color(ColorToken::Accent),
527 accent_foreground: source.color(ColorToken::AccentForeground),
528 muted: source.color(ColorToken::Muted),
529 muted_foreground: source.color(ColorToken::MutedForeground),
530 destructive: source.color(ColorToken::Destructive),
531 destructive_foreground: source.color(ColorToken::DestructiveForeground),
532 chart_1: source.color(ColorToken::Chart1),
533 chart_2: source.color(ColorToken::Chart2),
534 chart_3: source.color(ColorToken::Chart3),
535 chart_4: source.color(ColorToken::Chart4),
536 chart_5: source.color(ColorToken::Chart5),
537 sidebar: source.color(ColorToken::Sidebar),
538 sidebar_foreground: source.color(ColorToken::SidebarForeground),
539 sidebar_primary: source.color(ColorToken::SidebarPrimary),
540 sidebar_primary_foreground: source.color(ColorToken::SidebarPrimaryForeground),
541 sidebar_accent: source.color(ColorToken::SidebarAccent),
542 sidebar_accent_foreground: source.color(ColorToken::SidebarAccentForeground),
543 sidebar_border: source.color(ColorToken::SidebarBorder),
544 sidebar_ring: source.color(ColorToken::SidebarRing),
545 };
546
547 let radius = Radius {
548 sm: source.radius(RadiusToken::Sm),
549 md: source.radius(RadiusToken::Md),
550 lg: source.radius(RadiusToken::Lg),
551 };
552
553 let spacing = Spacing {
554 xs: source.spacing(SpacingToken::Xs),
555 sm: source.spacing(SpacingToken::Sm),
556 md: source.spacing(SpacingToken::Md),
557 lg: source.spacing(SpacingToken::Lg),
558 };
559
560 Self::from_parts_with_registry(palette, radius, spacing, source.styles(), source.registry())
561 }
562}
563
564impl Default for Theme {
565 fn default() -> Self {
566 Self::light()
567 }
568}