Skip to main content

armas_basic/
theme.rs

1//! ARMAS Theme System (shadcn/ui style)
2//!
3//! Serializable theme system for egui applications.
4//! Uses shadcn/ui naming conventions for simplicity and maintainability.
5
6use egui::Color32;
7use serde::{Deserialize, Serialize};
8
9/// Complete theme with colors and spacing
10///
11/// # Example
12///
13/// ```rust
14/// use armas_basic::Theme;
15///
16/// let theme = Theme::dark();
17/// let bg = theme.background();
18/// let fg = theme.foreground();
19/// let primary = theme.primary();
20/// ```
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct Theme {
23    /// Color palette
24    pub colors: ColorPalette,
25
26    /// Spacing configuration
27    pub spacing: Spacing,
28
29    /// Typography / font size scale
30    pub typography: Typography,
31}
32
33/// Color palette using shadcn/ui naming conventions
34/// All colors stored as [R, G, B] for serializability
35#[derive(Clone, Debug, Serialize, Deserialize)]
36pub struct ColorPalette {
37    /// Default background color
38    pub background: [u8; 3],
39    /// Default foreground (text) color
40    pub foreground: [u8; 3],
41
42    /// Card background color for elevated surfaces
43    pub card: [u8; 3],
44    /// Card foreground (text) color
45    pub card_foreground: [u8; 3],
46
47    /// Popover background color
48    pub popover: [u8; 3],
49    /// Popover foreground (text) color
50    pub popover_foreground: [u8; 3],
51
52    /// Primary brand color
53    pub primary: [u8; 3],
54    /// Primary foreground (text) color
55    pub primary_foreground: [u8; 3],
56
57    /// Secondary color
58    pub secondary: [u8; 3],
59    /// Secondary foreground (text) color
60    pub secondary_foreground: [u8; 3],
61
62    /// Muted/subtle background color
63    pub muted: [u8; 3],
64    /// Muted foreground (text) color
65    pub muted_foreground: [u8; 3],
66
67    /// Accent color
68    pub accent: [u8; 3],
69    /// Accent foreground (text) color
70    pub accent_foreground: [u8; 3],
71
72    /// Destructive/error color
73    pub destructive: [u8; 3],
74    /// Destructive foreground (text) color
75    pub destructive_foreground: [u8; 3],
76
77    /// Border color
78    pub border: [u8; 3],
79    /// Input border color
80    pub input: [u8; 3],
81    /// Focus ring color
82    pub ring: [u8; 3],
83
84    /// Chart color 1 for data visualization
85    pub chart_1: [u8; 3],
86    /// Chart color 2 for data visualization
87    pub chart_2: [u8; 3],
88    /// Chart color 3 for data visualization
89    pub chart_3: [u8; 3],
90    /// Chart color 4 for data visualization
91    pub chart_4: [u8; 3],
92    /// Chart color 5 for data visualization
93    pub chart_5: [u8; 3],
94
95    /// Hover state color
96    pub hover: [u8; 3],
97    /// Focus state color
98    pub focus: [u8; 3],
99
100    /// Sidebar background color
101    pub sidebar: [u8; 3],
102    /// Sidebar foreground (text) color
103    pub sidebar_foreground: [u8; 3],
104    /// Sidebar primary color
105    pub sidebar_primary: [u8; 3],
106    /// Sidebar primary foreground (text) color
107    pub sidebar_primary_foreground: [u8; 3],
108    /// Sidebar accent color
109    pub sidebar_accent: [u8; 3],
110    /// Sidebar accent foreground (text) color
111    pub sidebar_accent_foreground: [u8; 3],
112    /// Sidebar border color
113    pub sidebar_border: [u8; 3],
114    /// Sidebar focus ring color
115    pub sidebar_ring: [u8; 3],
116}
117
118/// Spacing configuration for layouts
119#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct Spacing {
121    /// 2XS spacing (2px)
122    pub xxs: f32,
123    /// Extra small spacing (4px)
124    pub xs: f32,
125    /// Small spacing (8px)
126    pub sm: f32,
127    /// Medium spacing (16px)
128    pub md: f32,
129    /// Large spacing (24px)
130    pub lg: f32,
131    /// Extra large spacing (32px)
132    pub xl: f32,
133    /// 2XL spacing (48px)
134    pub xxl: f32,
135
136    /// Micro corner radius (2px)
137    pub corner_radius_micro: u8,
138    /// Tiny corner radius (4px)
139    pub corner_radius_tiny: u8,
140    /// Small corner radius (8px)
141    pub corner_radius_small: u8,
142    /// Standard corner radius (12px)
143    pub corner_radius: u8,
144    /// Large corner radius (16px)
145    pub corner_radius_large: u8,
146}
147
148/// Font size scale following a modular type system.
149///
150/// Based on a 1.2 minor-third scale with 14px base, matching
151/// common UI/design system conventions (Tailwind, shadcn/ui).
152#[derive(Clone, Debug, Serialize, Deserialize)]
153pub struct Typography {
154    /// Extra-extra-small (10px) — fine print, badges
155    pub xxs: f32,
156    /// Extra-small (11px) — captions, metadata
157    pub xs: f32,
158    /// Small (12px) — secondary labels, descriptions
159    pub sm: f32,
160    /// Base/default (14px) — body text, inputs, buttons
161    pub base: f32,
162    /// Large (16px) — section headers, emphasized text
163    pub lg: f32,
164    /// Extra-large (18px) — page titles, headings
165    pub xl: f32,
166    /// 2XL (24px) — hero headings, display text
167    pub xxl: f32,
168}
169
170impl Default for Typography {
171    fn default() -> Self {
172        Self {
173            xxs: 10.0,
174            xs: 11.0,
175            sm: 12.0,
176            base: 14.0,
177            lg: 16.0,
178            xl: 18.0,
179            xxl: 24.0,
180        }
181    }
182}
183
184impl Default for Theme {
185    fn default() -> Self {
186        Self::dark()
187    }
188}
189
190impl Theme {
191    /// Dark theme using Zinc color palette (shadcn default)
192    #[must_use]
193    pub const fn dark() -> Self {
194        Self {
195            colors: ColorPalette {
196                background: [9, 9, 11],      // zinc-950
197                foreground: [250, 250, 250], // zinc-50
198
199                card: [9, 9, 11],                 // zinc-950
200                card_foreground: [250, 250, 250], // zinc-50
201
202                popover: [9, 9, 11],                 // zinc-950
203                popover_foreground: [250, 250, 250], // zinc-50
204
205                primary: [250, 250, 250],         // zinc-50
206                primary_foreground: [24, 24, 27], // zinc-900
207
208                secondary: [39, 39, 42],               // zinc-800
209                secondary_foreground: [250, 250, 250], // zinc-50
210
211                muted: [39, 39, 42],               // zinc-800
212                muted_foreground: [161, 161, 170], // zinc-400
213
214                accent: [39, 39, 42],               // zinc-800
215                accent_foreground: [250, 250, 250], // zinc-50
216
217                destructive: [127, 29, 29],              // red-900
218                destructive_foreground: [250, 250, 250], // zinc-50
219
220                border: [39, 39, 42],  // zinc-800
221                input: [82, 82, 91],   // zinc-600
222                ring: [212, 212, 216], // zinc-300
223
224                chart_1: [59, 130, 246], // blue-500
225                chart_2: [34, 197, 94],  // green-500
226                chart_3: [234, 179, 8],  // yellow-500
227                chart_4: [168, 85, 247], // purple-500
228                chart_5: [249, 115, 22], // orange-500
229
230                hover: [39, 39, 42],    // zinc-800
231                focus: [250, 250, 250], // zinc-50
232
233                // Sidebar (slightly lighter than background for distinction)
234                sidebar: [9, 9, 11],                      // zinc-950 (same as bg)
235                sidebar_foreground: [250, 250, 250],      // zinc-50
236                sidebar_primary: [250, 250, 250],         // zinc-50
237                sidebar_primary_foreground: [24, 24, 27], // zinc-900
238                sidebar_accent: [39, 39, 42],             // zinc-800
239                sidebar_accent_foreground: [250, 250, 250], // zinc-50
240                sidebar_border: [39, 39, 42],             // zinc-800
241                sidebar_ring: [212, 212, 216],            // zinc-300
242            },
243            spacing: Spacing {
244                xxs: 2.0,
245                xs: 4.0,
246                sm: 8.0,
247                md: 16.0,
248                lg: 24.0,
249                xl: 32.0,
250                xxl: 48.0,
251                corner_radius_micro: 2,
252                corner_radius_tiny: 4,
253                corner_radius_small: 8,
254                corner_radius: 12,
255                corner_radius_large: 16,
256            },
257            typography: Typography {
258                xxs: 10.0,
259                xs: 11.0,
260                sm: 12.0,
261                base: 14.0,
262                lg: 16.0,
263                xl: 18.0,
264                xxl: 24.0,
265            },
266        }
267    }
268
269    /// Light theme using Zinc color palette
270    #[must_use]
271    pub const fn light() -> Self {
272        Self {
273            colors: ColorPalette {
274                background: [255, 255, 255], // white
275                foreground: [9, 9, 11],      // zinc-950
276
277                card: [255, 255, 255],       // white
278                card_foreground: [9, 9, 11], // zinc-950
279
280                popover: [255, 255, 255],       // white
281                popover_foreground: [9, 9, 11], // zinc-950
282
283                primary: [24, 24, 27],               // zinc-900
284                primary_foreground: [250, 250, 250], // zinc-50
285
286                secondary: [244, 244, 245],         // zinc-100
287                secondary_foreground: [24, 24, 27], // zinc-900
288
289                muted: [244, 244, 245],            // zinc-100
290                muted_foreground: [113, 113, 122], // zinc-500
291
292                accent: [244, 244, 245],         // zinc-100
293                accent_foreground: [24, 24, 27], // zinc-900
294
295                destructive: [239, 68, 68],              // red-500
296                destructive_foreground: [250, 250, 250], // zinc-50
297
298                border: [228, 228, 231], // zinc-200
299                input: [228, 228, 231],  // zinc-200
300                ring: [24, 24, 27],      // zinc-900
301
302                chart_1: [59, 130, 246], // blue-500
303                chart_2: [34, 197, 94],  // green-500
304                chart_3: [234, 179, 8],  // yellow-500
305                chart_4: [168, 85, 247], // purple-500
306                chart_5: [249, 115, 22], // orange-500
307
308                hover: [244, 244, 245], // zinc-100
309                focus: [24, 24, 27],    // zinc-900
310
311                // Sidebar (slightly darker than background for distinction)
312                sidebar: [250, 250, 250],                    // zinc-50
313                sidebar_foreground: [9, 9, 11],              // zinc-950
314                sidebar_primary: [24, 24, 27],               // zinc-900
315                sidebar_primary_foreground: [250, 250, 250], // zinc-50
316                sidebar_accent: [244, 244, 245],             // zinc-100
317                sidebar_accent_foreground: [24, 24, 27],     // zinc-900
318                sidebar_border: [228, 228, 231],             // zinc-200
319                sidebar_ring: [24, 24, 27],                  // zinc-900
320            },
321            spacing: Spacing {
322                xxs: 2.0,
323                xs: 4.0,
324                sm: 8.0,
325                md: 16.0,
326                lg: 24.0,
327                xl: 32.0,
328                xxl: 48.0,
329                corner_radius_micro: 2,
330                corner_radius_tiny: 4,
331                corner_radius_small: 8,
332                corner_radius: 12,
333                corner_radius_large: 16,
334            },
335            typography: Typography {
336                xxs: 10.0,
337                xs: 11.0,
338                sm: 12.0,
339                base: 14.0,
340                lg: 16.0,
341                xl: 18.0,
342                xxl: 24.0,
343            },
344        }
345    }
346
347    // =========================================================================
348    // Color accessor methods (shadcn naming)
349    // =========================================================================
350
351    /// Background color
352    #[must_use]
353    pub const fn background(&self) -> Color32 {
354        let [r, g, b] = self.colors.background;
355        Color32::from_rgb(r, g, b)
356    }
357
358    /// Foreground/text color
359    #[must_use]
360    pub const fn foreground(&self) -> Color32 {
361        let [r, g, b] = self.colors.foreground;
362        Color32::from_rgb(r, g, b)
363    }
364
365    /// Card background color
366    #[must_use]
367    pub const fn card(&self) -> Color32 {
368        let [r, g, b] = self.colors.card;
369        Color32::from_rgb(r, g, b)
370    }
371
372    /// Card foreground color
373    #[must_use]
374    pub const fn card_foreground(&self) -> Color32 {
375        let [r, g, b] = self.colors.card_foreground;
376        Color32::from_rgb(r, g, b)
377    }
378
379    /// Popover background color
380    #[must_use]
381    pub const fn popover(&self) -> Color32 {
382        let [r, g, b] = self.colors.popover;
383        Color32::from_rgb(r, g, b)
384    }
385
386    /// Popover foreground color
387    #[must_use]
388    pub const fn popover_foreground(&self) -> Color32 {
389        let [r, g, b] = self.colors.popover_foreground;
390        Color32::from_rgb(r, g, b)
391    }
392
393    /// Primary brand color
394    #[must_use]
395    pub const fn primary(&self) -> Color32 {
396        let [r, g, b] = self.colors.primary;
397        Color32::from_rgb(r, g, b)
398    }
399
400    /// Primary foreground color
401    #[must_use]
402    pub const fn primary_foreground(&self) -> Color32 {
403        let [r, g, b] = self.colors.primary_foreground;
404        Color32::from_rgb(r, g, b)
405    }
406
407    /// Secondary color
408    #[must_use]
409    pub const fn secondary(&self) -> Color32 {
410        let [r, g, b] = self.colors.secondary;
411        Color32::from_rgb(r, g, b)
412    }
413
414    /// Secondary foreground color
415    #[must_use]
416    pub const fn secondary_foreground(&self) -> Color32 {
417        let [r, g, b] = self.colors.secondary_foreground;
418        Color32::from_rgb(r, g, b)
419    }
420
421    /// Muted color (subtle backgrounds)
422    #[must_use]
423    pub const fn muted(&self) -> Color32 {
424        let [r, g, b] = self.colors.muted;
425        Color32::from_rgb(r, g, b)
426    }
427
428    /// Muted foreground color
429    #[must_use]
430    pub const fn muted_foreground(&self) -> Color32 {
431        let [r, g, b] = self.colors.muted_foreground;
432        Color32::from_rgb(r, g, b)
433    }
434
435    /// Accent color
436    #[must_use]
437    pub const fn accent(&self) -> Color32 {
438        let [r, g, b] = self.colors.accent;
439        Color32::from_rgb(r, g, b)
440    }
441
442    /// Accent foreground color
443    #[must_use]
444    pub const fn accent_foreground(&self) -> Color32 {
445        let [r, g, b] = self.colors.accent_foreground;
446        Color32::from_rgb(r, g, b)
447    }
448
449    /// Destructive/error color
450    #[must_use]
451    pub const fn destructive(&self) -> Color32 {
452        let [r, g, b] = self.colors.destructive;
453        Color32::from_rgb(r, g, b)
454    }
455
456    /// Destructive foreground color
457    #[must_use]
458    pub const fn destructive_foreground(&self) -> Color32 {
459        let [r, g, b] = self.colors.destructive_foreground;
460        Color32::from_rgb(r, g, b)
461    }
462
463    /// Border color
464    #[must_use]
465    pub const fn border(&self) -> Color32 {
466        let [r, g, b] = self.colors.border;
467        Color32::from_rgb(r, g, b)
468    }
469
470    /// Input border color
471    #[must_use]
472    pub const fn input(&self) -> Color32 {
473        let [r, g, b] = self.colors.input;
474        Color32::from_rgb(r, g, b)
475    }
476
477    /// Focus ring color
478    #[must_use]
479    pub const fn ring(&self) -> Color32 {
480        let [r, g, b] = self.colors.ring;
481        Color32::from_rgb(r, g, b)
482    }
483
484    /// Chart color 1 (blue)
485    #[must_use]
486    pub const fn chart_1(&self) -> Color32 {
487        let [r, g, b] = self.colors.chart_1;
488        Color32::from_rgb(r, g, b)
489    }
490
491    /// Chart color 2 (green) - also used for success
492    #[must_use]
493    pub const fn chart_2(&self) -> Color32 {
494        let [r, g, b] = self.colors.chart_2;
495        Color32::from_rgb(r, g, b)
496    }
497
498    /// Chart color 3 (yellow) - also used for warning
499    #[must_use]
500    pub const fn chart_3(&self) -> Color32 {
501        let [r, g, b] = self.colors.chart_3;
502        Color32::from_rgb(r, g, b)
503    }
504
505    /// Chart color 4 (purple) - also used for info
506    #[must_use]
507    pub const fn chart_4(&self) -> Color32 {
508        let [r, g, b] = self.colors.chart_4;
509        Color32::from_rgb(r, g, b)
510    }
511
512    /// Chart color 5 (orange)
513    #[must_use]
514    pub const fn chart_5(&self) -> Color32 {
515        let [r, g, b] = self.colors.chart_5;
516        Color32::from_rgb(r, g, b)
517    }
518
519    /// Hover state color
520    #[must_use]
521    pub const fn hover(&self) -> Color32 {
522        let [r, g, b] = self.colors.hover;
523        Color32::from_rgb(r, g, b)
524    }
525
526    /// Focus state color
527    #[must_use]
528    pub const fn focus(&self) -> Color32 {
529        let [r, g, b] = self.colors.focus;
530        Color32::from_rgb(r, g, b)
531    }
532
533    // =========================================================================
534    // Sidebar color accessors
535    // =========================================================================
536
537    /// Sidebar background color
538    #[must_use]
539    pub const fn sidebar(&self) -> Color32 {
540        let [r, g, b] = self.colors.sidebar;
541        Color32::from_rgb(r, g, b)
542    }
543
544    /// Sidebar foreground/text color
545    #[must_use]
546    pub const fn sidebar_foreground(&self) -> Color32 {
547        let [r, g, b] = self.colors.sidebar_foreground;
548        Color32::from_rgb(r, g, b)
549    }
550
551    /// Sidebar primary color
552    #[must_use]
553    pub const fn sidebar_primary(&self) -> Color32 {
554        let [r, g, b] = self.colors.sidebar_primary;
555        Color32::from_rgb(r, g, b)
556    }
557
558    /// Sidebar primary foreground color
559    #[must_use]
560    pub const fn sidebar_primary_foreground(&self) -> Color32 {
561        let [r, g, b] = self.colors.sidebar_primary_foreground;
562        Color32::from_rgb(r, g, b)
563    }
564
565    /// Sidebar accent color (hover/active background)
566    #[must_use]
567    pub const fn sidebar_accent(&self) -> Color32 {
568        let [r, g, b] = self.colors.sidebar_accent;
569        Color32::from_rgb(r, g, b)
570    }
571
572    /// Sidebar accent foreground color
573    #[must_use]
574    pub const fn sidebar_accent_foreground(&self) -> Color32 {
575        let [r, g, b] = self.colors.sidebar_accent_foreground;
576        Color32::from_rgb(r, g, b)
577    }
578
579    /// Sidebar border color
580    #[must_use]
581    pub const fn sidebar_border(&self) -> Color32 {
582        let [r, g, b] = self.colors.sidebar_border;
583        Color32::from_rgb(r, g, b)
584    }
585
586    /// Sidebar focus ring color
587    #[must_use]
588    pub const fn sidebar_ring(&self) -> Color32 {
589        let [r, g, b] = self.colors.sidebar_ring;
590        Color32::from_rgb(r, g, b)
591    }
592}