Skip to main content

iced_shadcn/
theme.rs

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}