Skip to main content

fission_theme/
lib.rs

1//! Design token system and component themes for the Fission UI framework.
2//!
3//! This crate defines the complete visual language: colors, spacing, typography,
4//! corner radii, elevations (box shadows), and per-component theme overrides.
5//! It follows the Material Design 3 token architecture.
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use fission_theme::Theme;
11//!
12//! let light = Theme::default();
13//! let dark = Theme::dark();
14//! ```
15
16use fission_ir::op::{BoxShadow, Color, Stroke};
17use serde::{Deserialize, Serialize};
18
19/// Semantic color palette for the application.
20///
21/// Provides primary, secondary, surface, background, error, border, and text
22/// colors. Each color has an `on_*` counterpart for content displayed on that
23/// surface (e.g., `on_primary` is the text/icon color used on `primary` backgrounds).
24///
25/// The [`Default`] implementation provides a light theme. Use [`ColorTokens::dark()`]
26/// for dark mode colors.
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct ColorTokens {
29    pub primary: Color,
30    pub on_primary: Color,
31    pub secondary: Color,
32    pub on_secondary: Color,
33    pub surface: Color,
34    pub on_surface: Color,
35    pub background: Color,
36    pub on_background: Color,
37    pub error: Color,
38    pub on_error: Color,
39    pub border: Color,
40    pub text_primary: Color,
41    pub text_secondary: Color,
42}
43
44impl Default for ColorTokens {
45    fn default() -> Self {
46        Self {
47            primary: Color { r: 103, g: 85, b: 143, a: 255 }, // Purple 40
48            on_primary: Color::WHITE,
49            secondary: Color { r: 98, g: 91, b: 113, a: 255 },
50            on_secondary: Color::WHITE,
51            surface: Color { r: 255, g: 251, b: 254, a: 255 },
52            on_surface: Color { r: 28, g: 27, b: 31, a: 255 },
53            background: Color { r: 255, g: 251, b: 254, a: 255 },
54            on_background: Color { r: 28, g: 27, b: 31, a: 255 },
55            error: Color { r: 179, g: 38, b: 30, a: 255 },
56            on_error: Color::WHITE,
57            border: Color { r: 188, g: 188, b: 188, a: 255 },
58            text_primary: Color { r: 28, g: 27, b: 31, a: 255 },
59            text_secondary: Color { r: 86, g: 86, b: 86, a: 255 },
60        }
61    }
62}
63
64impl ColorTokens {
65    pub fn dark() -> Self {
66        Self {
67            primary: Color { r: 187, g: 134, b: 252, a: 255 },
68            on_primary: Color { r: 0, g: 0, b: 0, a: 255 },
69            secondary: Color { r: 3, g: 218, b: 197, a: 255 },
70            on_secondary: Color { r: 0, g: 0, b: 0, a: 255 },
71            surface: Color { r: 30, g: 30, b: 30, a: 255 },
72            on_surface: Color { r: 230, g: 230, b: 230, a: 255 },
73            background: Color { r: 18, g: 18, b: 18, a: 255 },
74            on_background: Color { r: 230, g: 230, b: 230, a: 255 },
75            error: Color { r: 207, g: 102, b: 121, a: 255 },
76            on_error: Color { r: 0, g: 0, b: 0, a: 255 },
77            border: Color { r: 60, g: 60, b: 60, a: 255 },
78            text_primary: Color { r: 230, g: 230, b: 230, a: 255 },
79            text_secondary: Color { r: 160, g: 160, b: 160, a: 255 },
80        }
81    }
82}
83
84/// Standard spacing scale used for padding, margins, and gaps.
85///
86/// Values: `none` (0), `xs` (4), `s` (8), `m` (16), `l` (24), `xl` (32).
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88pub struct SpacingTokens {
89    pub none: f32, // 0
90    pub xs: f32,   // 4
91    pub s: f32,    // 8
92    pub m: f32,    // 16
93    pub l: f32,    // 24
94    pub xl: f32,   // 32
95}
96
97impl Default for SpacingTokens {
98    fn default() -> Self {
99        Self {
100            none: 0.0,
101            xs: 4.0,
102            s: 8.0,
103            m: 16.0,
104            l: 24.0,
105            xl: 32.0,
106        }
107    }
108}
109
110/// Font size scale for text elements.
111///
112/// Sizes: `label_large_size` (15), `body_medium_size` (15), `body_large_size` (17),
113/// `heading_size` (28).
114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
115pub struct TypographyTokens {
116    pub label_large_size: f32,
117    pub body_medium_size: f32,
118    pub body_large_size: f32,
119    pub heading_size: f32,
120}
121
122impl Default for TypographyTokens {
123    fn default() -> Self {
124        Self {
125            label_large_size: 15.0,
126            body_medium_size: 15.0,
127            body_large_size: 17.0,
128            heading_size: 28.0,
129        }
130    }
131}
132
133/// Corner radius scale for rounded containers.
134///
135/// Values: `small` (4), `medium` (8), `large` (12), `full` (9999 -- fully rounded pill).
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
137pub struct RadiusTokens {
138    pub small: f32,
139    pub medium: f32,
140    pub large: f32,
141    pub full: f32,
142}
143
144impl Default for RadiusTokens {
145    fn default() -> Self {
146        Self {
147            small: 4.0,
148            medium: 8.0,
149            large: 12.0,
150            full: 9999.0,
151        }
152    }
153}
154
155/// Box shadow levels for surface elevation.
156///
157/// Six levels (0-5). Levels 0, 4, and 5 default to `None`. Levels 1-3 provide
158/// progressively stronger shadows with increasing blur radius and y-offset.
159#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
160pub struct ElevationTokens {
161    pub level0: Option<BoxShadow>,
162    pub level1: Option<BoxShadow>,
163    pub level2: Option<BoxShadow>,
164    pub level3: Option<BoxShadow>,
165    pub level4: Option<BoxShadow>,
166    pub level5: Option<BoxShadow>,
167}
168
169impl Default for ElevationTokens {
170    fn default() -> Self {
171        let black_alpha = |a| Color { r: 0, g: 0, b: 0, a };
172        Self {
173            level0: None,
174            level1: Some(BoxShadow { color: black_alpha(40), offset: (0.0, 1.0), blur_radius: 2.0 }),
175            level2: Some(BoxShadow { color: black_alpha(60), offset: (0.0, 2.0), blur_radius: 4.0 }),
176            level3: Some(BoxShadow { color: black_alpha(60), offset: (0.0, 4.0), blur_radius: 8.0 }),
177            level4: None,
178            level5: None,
179        }
180    }
181}
182
183/// The complete set of primitive design tokens.
184///
185/// Combines [`ColorTokens`], [`SpacingTokens`], [`TypographyTokens`],
186/// [`RadiusTokens`], and [`ElevationTokens`]. The [`Default`] implementation
187/// provides light-mode values. Use [`Tokens::dark()`] for dark mode.
188#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
189pub struct Tokens {
190    pub colors: ColorTokens,
191    pub spacing: SpacingTokens,
192    pub typography: TypographyTokens,
193    pub radii: RadiusTokens,
194    pub elevations: ElevationTokens,
195}
196
197impl Tokens {
198    pub fn dark() -> Self {
199        Self {
200            colors: ColorTokens::dark(),
201            spacing: SpacingTokens::default(),
202            typography: TypographyTokens::default(),
203            radii: RadiusTokens::default(),
204            elevations: ElevationTokens::default(),
205        }
206    }
207}
208
209// --- Component Themes ---
210
211/// Visual parameters for the `Button` widget.
212///
213/// Includes dimensions, padding, corner radius, text size, elevation for
214/// rest/hover/pressed states, and an optional focus stroke.
215#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
216pub struct ButtonTheme {
217    pub height: f32,
218    pub padding_horizontal: f32,
219    pub padding_vertical: f32,
220    pub radius: f32,
221    pub text_size: f32,
222    pub elevation_rest: Option<BoxShadow>,
223    pub elevation_hover: Option<BoxShadow>,
224    pub elevation_pressed: Option<BoxShadow>,
225    pub focus_stroke: Option<Stroke>,
226}
227
228impl ButtonTheme {
229    pub fn from_tokens(tokens: &Tokens) -> Self {
230        Self {
231            height: 42.0,
232            padding_horizontal: tokens.spacing.m,
233            padding_vertical: tokens.spacing.s,
234            radius: tokens.radii.full,
235            text_size: tokens.typography.label_large_size,
236            elevation_rest: tokens.elevations.level1,
237            elevation_hover: tokens.elevations.level2,
238            elevation_pressed: tokens.elevations.level0,
239            focus_stroke: Some(Stroke {
240                color: tokens.colors.on_background,
241                width: 2.0,
242            }),
243        }
244    }
245}
246
247/// Visual parameters for the `TextInput` widget.
248///
249/// Controls height, horizontal padding, corner radius, font size, and colors
250/// for border, focus ring, text, and placeholder.
251#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
252pub struct TextInputTheme {
253    pub height: f32,
254    pub padding_h: f32,
255    pub radius: f32,
256    pub font_size: f32,
257    pub border_color: Color,
258    pub border_width: f32,
259    pub focus_color: Color,
260    pub text_color: Color,
261    pub placeholder_color: Color,
262}
263
264impl TextInputTheme {
265    pub fn from_tokens(tokens: &Tokens) -> Self {
266        Self {
267            height: 40.0,
268            padding_h: tokens.spacing.m,
269            radius: tokens.radii.small,
270            font_size: tokens.typography.body_large_size,
271            border_color: tokens.colors.border,
272            border_width: 1.0,
273            focus_color: tokens.colors.primary,
274            text_color: tokens.colors.text_primary,
275            placeholder_color: tokens.colors.text_secondary,
276        }
277    }
278}
279
280/// Visual parameters for the `Calendar` widget.
281#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
282pub struct CalendarTheme {
283    pub bg_color: Color,
284    pub border_color: Color,
285    pub radius: f32,
286    pub selected_bg: Color,
287    pub selected_text: Color,
288    pub today_outline: Color,
289}
290
291impl CalendarTheme {
292    pub fn from_tokens(tokens: &Tokens) -> Self {
293        Self {
294            bg_color: tokens.colors.surface,
295            border_color: tokens.colors.border,
296            radius: tokens.radii.medium,
297            selected_bg: tokens.colors.primary,
298            selected_text: tokens.colors.on_primary,
299            today_outline: tokens.colors.secondary,
300        }
301    }
302}
303
304/// Visual parameters for the `Pagination` widget.
305#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
306pub struct PaginationTheme {
307    pub spacing: f32,
308    pub active_bg: Color,
309    pub active_text: Color,
310}
311
312impl PaginationTheme {
313    pub fn from_tokens(tokens: &Tokens) -> Self {
314        Self {
315            spacing: tokens.spacing.s,
316            active_bg: tokens.colors.primary,
317            active_text: tokens.colors.on_primary,
318        }
319    }
320}
321
322/// Visual parameters for the `Timeline` widget.
323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
324pub struct TimelineTheme {
325    pub dot_size: f32,
326    pub line_width: f32,
327    pub dot_color: Color,
328    pub line_color: Color,
329}
330
331impl TimelineTheme {
332    pub fn from_tokens(tokens: &Tokens) -> Self {
333        Self {
334            dot_size: 12.0,
335            line_width: 2.0,
336            dot_color: tokens.colors.primary,
337            line_color: tokens.colors.border,
338        }
339    }
340}
341
342/// Visual parameters for the `SegmentedControl` widget.
343#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
344pub struct SegmentedControlTheme {
345    pub bg_color: Color,
346    pub border_color: Color,
347    pub radius: f32,
348    pub active_bg: Color,
349    pub active_text: Color,
350}
351
352impl SegmentedControlTheme {
353    pub fn from_tokens(tokens: &Tokens) -> Self {
354        Self {
355            bg_color: tokens.colors.surface,
356            border_color: tokens.colors.border,
357            radius: tokens.radii.full,
358            active_bg: tokens.colors.primary,
359            active_text: tokens.colors.on_primary,
360        }
361    }
362}
363
364/// Visual parameters for the `Alert` widget, with per-severity background colors.
365#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
366pub struct AlertTheme {
367    pub info_bg: Color,
368    pub warning_bg: Color,
369    pub error_bg: Color,
370    pub success_bg: Color,
371    pub radius: f32,
372}
373
374impl AlertTheme {
375    pub fn from_tokens(tokens: &Tokens) -> Self {
376        Self {
377            info_bg: Color { r: 230, g: 242, b: 255, a: 255 },
378            warning_bg: Color { r: 255, g: 244, b: 229, a: 255 },
379            error_bg: tokens.colors.error.with_alpha(30),
380            success_bg: Color { r: 237, g: 247, b: 237, a: 255 },
381            radius: tokens.radii.medium,
382        }
383    }
384}
385
386/// Visual parameters for the `Badge` widget.
387#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
388pub struct BadgeTheme {
389    pub radius: f32,
390    pub font_size: f32,
391}
392
393impl BadgeTheme {
394    pub fn from_tokens(tokens: &Tokens) -> Self {
395        Self {
396            radius: tokens.radii.full,
397            font_size: 10.0,
398        }
399    }
400}
401
402/// Visual parameters for the `Tabs` widget.
403#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
404pub struct TabsTheme {
405    pub active_color: Color,
406    pub inactive_color: Color,
407    pub indicator_height: f32,
408    pub background: Color,
409    pub divider_color: Color,
410}
411
412impl TabsTheme {
413    pub fn from_tokens(tokens: &Tokens) -> Self {
414        Self {
415            active_color: tokens.colors.primary,
416            inactive_color: tokens.colors.text_secondary,
417            indicator_height: 3.0,
418            background: tokens.colors.background,
419            divider_color: tokens.colors.border.with_alpha(120),
420        }
421    }
422}
423
424/// Visual parameters for the `Modal` widget.
425///
426/// Controls the dialog background color, corner radius, shadow, and maximum width.
427#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
428pub struct ModalTheme {
429    pub bg_color: Color,
430    pub radius: f32,
431    pub shadow: Option<BoxShadow>,
432    pub max_width: f32,
433}
434
435impl ModalTheme {
436    pub fn from_tokens(tokens: &Tokens) -> Self {
437        Self {
438            bg_color: tokens.colors.surface,
439            radius: tokens.radii.large,
440            shadow: tokens.elevations.level3,
441            max_width: 600.0,
442        }
443    }
444}
445
446/// Visual parameters for the `TreeView` widget.
447#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
448pub struct TreeViewTheme {
449    pub indent: f32,
450    pub selected_bg: Color,
451    pub hover_bg: Color,
452}
453
454impl TreeViewTheme {
455    pub fn from_tokens(tokens: &Tokens) -> Self {
456        Self {
457            indent: 16.0,
458            selected_bg: tokens.colors.primary.with_alpha(52),
459            hover_bg: tokens.colors.surface,
460        }
461    }
462}
463
464/// Visual parameters for the `ProgressBar` widget.
465#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
466pub struct ProgressTheme {
467    pub height: f32,
468    pub track_color: Color,
469    pub bar_color: Color,
470}
471
472impl ProgressTheme {
473    pub fn from_tokens(tokens: &Tokens) -> Self {
474        Self {
475            height: 8.0,
476            track_color: tokens.colors.border,
477            bar_color: tokens.colors.primary,
478        }
479    }
480}
481
482/// Visual parameters for the `Tooltip` widget.
483#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
484pub struct TooltipTheme {
485    pub bg_color: Color,
486    pub text_color: Color,
487    pub radius: f32,
488    pub font_size: f32,
489}
490
491impl TooltipTheme {
492    pub fn from_tokens(tokens: &Tokens) -> Self {
493        Self {
494            bg_color: Color { r: 50, g: 50, b: 50, a: 255 },
495            text_color: Color::WHITE,
496            radius: tokens.radii.small,
497            font_size: 12.0,
498        }
499    }
500}
501
502/// Aggregates all per-component visual themes.
503///
504/// Each field holds the theme for a specific widget type. Construct via
505/// [`ComponentTheme::from_tokens()`] to derive all values from the primitive tokens.
506#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
507pub struct ComponentTheme {
508    pub button: ButtonTheme,
509    pub text_input: TextInputTheme,
510    pub calendar: CalendarTheme,
511    pub pagination: PaginationTheme,
512    pub timeline: TimelineTheme,
513    pub segmented_control: SegmentedControlTheme,
514    pub alert: AlertTheme,
515    pub badge: BadgeTheme,
516    pub tabs: TabsTheme,
517    pub modal: ModalTheme,
518    pub tree_view: TreeViewTheme,
519    pub progress: ProgressTheme,
520    pub tooltip: TooltipTheme,
521}
522
523impl ComponentTheme {
524    pub fn from_tokens(tokens: &Tokens) -> Self {
525        Self {
526            button: ButtonTheme::from_tokens(tokens),
527            text_input: TextInputTheme::from_tokens(tokens),
528            calendar: CalendarTheme::from_tokens(tokens),
529            pagination: PaginationTheme::from_tokens(tokens),
530            timeline: TimelineTheme::from_tokens(tokens),
531            segmented_control: SegmentedControlTheme::from_tokens(tokens),
532            alert: AlertTheme::from_tokens(tokens),
533            badge: BadgeTheme::from_tokens(tokens),
534            tabs: TabsTheme::from_tokens(tokens),
535            modal: ModalTheme::from_tokens(tokens),
536            tree_view: TreeViewTheme::from_tokens(tokens),
537            progress: ProgressTheme::from_tokens(tokens),
538            tooltip: TooltipTheme::from_tokens(tokens),
539        }
540    }
541}
542
543/// The top-level theme combining primitive [`Tokens`] and derived [`ComponentTheme`].
544///
545/// Use [`Theme::default()`] for light mode and [`Theme::dark()`] for dark mode.
546/// For custom themes, construct [`Tokens`] manually and derive components via
547/// [`ComponentTheme::from_tokens()`].
548#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
549pub struct Theme {
550    pub tokens: Tokens,
551    pub components: ComponentTheme,
552}
553
554impl Default for Theme {
555    fn default() -> Self {
556        let tokens = Tokens::default();
557        let components = ComponentTheme::from_tokens(&tokens);
558        Self { tokens, components }
559    }
560}
561
562impl Theme {
563    pub fn dark() -> Self {
564        let tokens = Tokens::dark();
565        let components = ComponentTheme::from_tokens(&tokens);
566        Self { tokens, components }
567    }
568}
569
570/// Bundled font files embedded at compile time.
571///
572/// Provides Noto Sans Regular (the default) and Inter 24pt Regular.
573pub mod fonts {
574    pub const NOTO_SANS_REGULAR_TTF: &[u8] = include_bytes!("../fonts/Noto_Sans/static/NotoSans-Regular.ttf");
575    pub const INTER_24PT_REGULAR_TTF: &[u8] = include_bytes!("../fonts/Inter/static/Inter_24pt-Regular.ttf");
576    #[inline]
577    pub fn default_font_bytes() -> &'static [u8] { NOTO_SANS_REGULAR_TTF }
578}