oxidize_pdf/dashboard/
theme.rs

1//! Dashboard Theming System
2//!
3//! This module provides a comprehensive theming system for dashboards, including
4//! color palettes, typography, spacing, and pre-defined themes for different
5//! use cases (corporate, minimal, dark, colorful, etc.).
6
7use crate::graphics::Color;
8
9/// Font weight enumeration
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FontWeight {
12    Normal,
13    Bold,
14}
15
16/// Main theme configuration for dashboards
17#[derive(Debug, Clone)]
18pub struct DashboardTheme {
19    /// Color palette for the theme
20    pub colors: ThemeColors,
21    /// Typography settings
22    pub typography: Typography,
23    /// Spacing and layout settings
24    pub spacing: ThemeSpacing,
25    /// Border and shadow settings
26    pub borders: ThemeBorders,
27    /// Background settings
28    pub backgrounds: ThemeBackgrounds,
29}
30
31impl DashboardTheme {
32    /// Create a new custom theme
33    pub fn new(colors: ThemeColors, typography: Typography) -> Self {
34        Self {
35            colors,
36            typography,
37            spacing: ThemeSpacing::default(),
38            borders: ThemeBorders::default(),
39            backgrounds: ThemeBackgrounds::default(),
40        }
41    }
42
43    /// Corporate theme - professional, blue-based palette
44    pub fn corporate() -> Self {
45        let colors = ThemeColors {
46            primary: Color::hex("#1f4788"),
47            secondary: Color::hex("#4a90a4"),
48            accent: Color::hex("#87ceeb"),
49            success: Color::hex("#28a745"),
50            warning: Color::hex("#ffc107"),
51            danger: Color::hex("#dc3545"),
52            info: Color::hex("#17a2b8"),
53            light: Color::hex("#f8f9fa"),
54            dark: Color::hex("#343a40"),
55            muted: Color::hex("#6c757d"),
56            background: Color::white(),
57            surface: Color::hex("#f0f4f8"), // Light blue-gray instead of white
58            text_primary: Color::hex("#212529"),
59            text_secondary: Color::hex("#6c757d"),
60            text_muted: Color::hex("#adb5bd"),
61            border: Color::hex("#dee2e6"),
62        };
63
64        let typography = Typography {
65            title_font: "Helvetica-Bold".to_string(),
66            title_size: 24.0,
67            title_color: colors.text_primary,
68            heading_font: "Helvetica-Bold".to_string(),
69            heading_size: 18.0,
70            heading_color: colors.text_primary,
71            body_font: "Helvetica".to_string(),
72            body_size: 12.0,
73            body_color: colors.text_primary,
74            caption_font: "Helvetica".to_string(),
75            caption_size: 10.0,
76            caption_color: colors.text_secondary,
77            line_height: 1.4,
78        };
79
80        Self {
81            colors,
82            typography,
83            spacing: ThemeSpacing::corporate(),
84            borders: ThemeBorders::corporate(),
85            backgrounds: ThemeBackgrounds::corporate(),
86        }
87    }
88
89    /// Minimal theme - clean, grayscale palette
90    pub fn minimal() -> Self {
91        let colors = ThemeColors {
92            primary: Color::hex("#000000"),
93            secondary: Color::hex("#666666"),
94            accent: Color::hex("#999999"),
95            success: Color::hex("#4caf50"),
96            warning: Color::hex("#ff9800"),
97            danger: Color::hex("#f44336"),
98            info: Color::hex("#2196f3"),
99            light: Color::hex("#fafafa"),
100            dark: Color::hex("#212121"),
101            muted: Color::hex("#757575"),
102            background: Color::white(),
103            surface: Color::hex("#ffffff"),
104            text_primary: Color::hex("#212121"),
105            text_secondary: Color::hex("#757575"),
106            text_muted: Color::hex("#bdbdbd"),
107            border: Color::hex("#e0e0e0"),
108        };
109
110        let typography = Typography::minimal();
111
112        Self {
113            colors,
114            typography,
115            spacing: ThemeSpacing::minimal(),
116            borders: ThemeBorders::minimal(),
117            backgrounds: ThemeBackgrounds::minimal(),
118        }
119    }
120
121    /// Dark theme - dark background with light text
122    pub fn dark() -> Self {
123        let colors = ThemeColors {
124            primary: Color::hex("#bb86fc"),
125            secondary: Color::hex("#03dac6"),
126            accent: Color::hex("#cf6679"),
127            success: Color::hex("#4caf50"),
128            warning: Color::hex("#ff9800"),
129            danger: Color::hex("#f44336"),
130            info: Color::hex("#2196f3"),
131            light: Color::hex("#424242"),
132            dark: Color::hex("#121212"),
133            muted: Color::hex("#757575"),
134            background: Color::hex("#121212"),
135            surface: Color::hex("#1e1e1e"),
136            text_primary: Color::hex("#ffffff"),
137            text_secondary: Color::hex("#b3b3b3"),
138            text_muted: Color::hex("#666666"),
139            border: Color::hex("#333333"),
140        };
141
142        let typography = Typography::dark();
143
144        Self {
145            colors,
146            typography,
147            spacing: ThemeSpacing::default(),
148            borders: ThemeBorders::dark(),
149            backgrounds: ThemeBackgrounds::dark(),
150        }
151    }
152
153    /// Colorful theme - vibrant, multi-color palette
154    pub fn colorful() -> Self {
155        let colors = ThemeColors {
156            primary: Color::hex("#e91e63"),
157            secondary: Color::hex("#9c27b0"),
158            accent: Color::hex("#ff5722"),
159            success: Color::hex("#4caf50"),
160            warning: Color::hex("#ff9800"),
161            danger: Color::hex("#f44336"),
162            info: Color::hex("#2196f3"),
163            light: Color::hex("#fce4ec"),
164            dark: Color::hex("#880e4f"),
165            muted: Color::hex("#ad1457"),
166            background: Color::hex("#fafafa"),
167            surface: Color::white(),
168            text_primary: Color::hex("#212121"),
169            text_secondary: Color::hex("#757575"),
170            text_muted: Color::hex("#bdbdbd"),
171            border: Color::hex("#e1bee7"),
172        };
173
174        let typography = Typography::colorful();
175
176        Self {
177            colors,
178            typography,
179            spacing: ThemeSpacing::colorful(),
180            borders: ThemeBorders::colorful(),
181            backgrounds: ThemeBackgrounds::colorful(),
182        }
183    }
184
185    /// Set custom color palette
186    pub fn set_color_palette(&mut self, colors: Vec<Color>) {
187        if !colors.is_empty() {
188            self.colors.primary = colors[0];
189            if colors.len() > 1 {
190                self.colors.secondary = colors[1];
191            }
192            if colors.len() > 2 {
193                self.colors.accent = colors[2];
194            }
195        }
196    }
197
198    /// Set typography configuration
199    pub fn set_typography(&mut self, typography: Typography) {
200        self.typography = typography;
201    }
202
203    /// Get title text style
204    pub fn title_style(&self) -> TextStyle {
205        TextStyle {
206            font_name: self.typography.title_font.clone(),
207            font_size: self.typography.title_size,
208            color: self.typography.title_color,
209            weight: FontWeight::Bold,
210            alignment: TextAlignment::Center,
211        }
212    }
213
214    /// Get subtitle text style
215    pub fn subtitle_style(&self) -> TextStyle {
216        TextStyle {
217            font_name: self.typography.heading_font.clone(),
218            font_size: self.typography.heading_size,
219            color: self.typography.heading_color,
220            weight: FontWeight::Normal,
221            alignment: TextAlignment::Center,
222        }
223    }
224
225    /// Get footer text style
226    pub fn footer_style(&self) -> TextStyle {
227        TextStyle {
228            font_name: self.typography.caption_font.clone(),
229            font_size: self.typography.caption_size,
230            color: self.typography.caption_color,
231            weight: FontWeight::Normal,
232            alignment: TextAlignment::Left,
233        }
234    }
235
236    /// Get title height in points
237    pub fn title_height(&self) -> f64 {
238        self.typography.title_size * self.typography.line_height
239    }
240
241    /// Get footer height in points
242    pub fn footer_height(&self) -> f64 {
243        self.typography.caption_size * self.typography.line_height
244    }
245}
246
247impl Default for DashboardTheme {
248    fn default() -> Self {
249        Self::corporate()
250    }
251}
252
253/// Color palette for dashboard themes
254#[derive(Debug, Clone)]
255pub struct ThemeColors {
256    /// Primary brand color
257    pub primary: Color,
258    /// Secondary brand color
259    pub secondary: Color,
260    /// Accent color for highlights
261    pub accent: Color,
262    /// Success state color (green)
263    pub success: Color,
264    /// Warning state color (orange)
265    pub warning: Color,
266    /// Danger/error state color (red)
267    pub danger: Color,
268    /// Information state color (blue)
269    pub info: Color,
270    /// Light neutral color
271    pub light: Color,
272    /// Dark neutral color
273    pub dark: Color,
274    /// Muted color for less important elements
275    pub muted: Color,
276    /// Main background color
277    pub background: Color,
278    /// Surface color (cards, panels)
279    pub surface: Color,
280    /// Primary text color
281    pub text_primary: Color,
282    /// Secondary text color
283    pub text_secondary: Color,
284    /// Muted text color
285    pub text_muted: Color,
286    /// Border color
287    pub border: Color,
288}
289
290/// Typography configuration for dashboard themes
291#[derive(Debug, Clone)]
292pub struct Typography {
293    /// Font for titles
294    pub title_font: String,
295    /// Title font size
296    pub title_size: f64,
297    /// Title text color
298    pub title_color: Color,
299    /// Font for headings
300    pub heading_font: String,
301    /// Heading font size
302    pub heading_size: f64,
303    /// Heading text color
304    pub heading_color: Color,
305    /// Font for body text
306    pub body_font: String,
307    /// Body font size
308    pub body_size: f64,
309    /// Body text color
310    pub body_color: Color,
311    /// Font for captions
312    pub caption_font: String,
313    /// Caption font size
314    pub caption_size: f64,
315    /// Caption text color
316    pub caption_color: Color,
317    /// Line height multiplier
318    pub line_height: f64,
319}
320
321impl Typography {
322    /// Professional typography setup
323    pub fn professional() -> Self {
324        Self {
325            title_font: "Helvetica-Bold".to_string(),
326            title_size: 24.0,
327            title_color: Color::hex("#212529"),
328            heading_font: "Helvetica-Bold".to_string(),
329            heading_size: 18.0,
330            heading_color: Color::hex("#212529"),
331            body_font: "Helvetica".to_string(),
332            body_size: 12.0,
333            body_color: Color::hex("#212529"),
334            caption_font: "Helvetica".to_string(),
335            caption_size: 10.0,
336            caption_color: Color::hex("#6c757d"),
337            line_height: 1.4,
338        }
339    }
340
341    /// Minimal typography setup
342    pub fn minimal() -> Self {
343        Self {
344            title_font: "Helvetica-Light".to_string(),
345            title_size: 28.0,
346            title_color: Color::hex("#212121"),
347            heading_font: "Helvetica".to_string(),
348            heading_size: 16.0,
349            heading_color: Color::hex("#212121"),
350            body_font: "Helvetica".to_string(),
351            body_size: 11.0,
352            body_color: Color::hex("#212121"),
353            caption_font: "Helvetica".to_string(),
354            caption_size: 9.0,
355            caption_color: Color::hex("#757575"),
356            line_height: 1.5,
357        }
358    }
359
360    /// Dark theme typography
361    pub fn dark() -> Self {
362        Self {
363            title_font: "Helvetica-Bold".to_string(),
364            title_size: 24.0,
365            title_color: Color::white(),
366            heading_font: "Helvetica-Bold".to_string(),
367            heading_size: 18.0,
368            heading_color: Color::white(),
369            body_font: "Helvetica".to_string(),
370            body_size: 12.0,
371            body_color: Color::white(),
372            caption_font: "Helvetica".to_string(),
373            caption_size: 10.0,
374            caption_color: Color::hex("#b3b3b3"),
375            line_height: 1.4,
376        }
377    }
378
379    /// Colorful theme typography
380    pub fn colorful() -> Self {
381        Self {
382            title_font: "Helvetica-Bold".to_string(),
383            title_size: 26.0,
384            title_color: Color::hex("#880e4f"),
385            heading_font: "Helvetica-Bold".to_string(),
386            heading_size: 18.0,
387            heading_color: Color::hex("#ad1457"),
388            body_font: "Helvetica".to_string(),
389            body_size: 12.0,
390            body_color: Color::hex("#212121"),
391            caption_font: "Helvetica".to_string(),
392            caption_size: 10.0,
393            caption_color: Color::hex("#757575"),
394            line_height: 1.4,
395        }
396    }
397}
398
399impl Default for Typography {
400    fn default() -> Self {
401        Self::professional()
402    }
403}
404
405/// Text style for rendering
406#[derive(Debug, Clone)]
407pub struct TextStyle {
408    /// Font name
409    pub font_name: String,
410    /// Font size in points
411    pub font_size: f64,
412    /// Text color
413    pub color: Color,
414    /// Font weight
415    pub weight: FontWeight,
416    /// Text alignment
417    pub alignment: TextAlignment,
418}
419
420/// Text alignment options
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum TextAlignment {
423    Left,
424    Center,
425    Right,
426}
427
428/// Spacing configuration for themes
429#[derive(Debug, Clone)]
430pub struct ThemeSpacing {
431    /// Extra small spacing (4pt)
432    pub xs: f64,
433    /// Small spacing (8pt)
434    pub sm: f64,
435    /// Medium spacing (16pt)
436    pub md: f64,
437    /// Large spacing (24pt)
438    pub lg: f64,
439    /// Extra large spacing (32pt)
440    pub xl: f64,
441}
442
443impl ThemeSpacing {
444    pub fn corporate() -> Self {
445        Self {
446            xs: 4.0,
447            sm: 8.0,
448            md: 16.0,
449            lg: 24.0,
450            xl: 32.0,
451        }
452    }
453
454    pub fn minimal() -> Self {
455        Self {
456            xs: 2.0,
457            sm: 6.0,
458            md: 12.0,
459            lg: 20.0,
460            xl: 28.0,
461        }
462    }
463
464    pub fn colorful() -> Self {
465        Self {
466            xs: 6.0,
467            sm: 10.0,
468            md: 18.0,
469            lg: 28.0,
470            xl: 36.0,
471        }
472    }
473}
474
475impl Default for ThemeSpacing {
476    fn default() -> Self {
477        Self::corporate()
478    }
479}
480
481/// Border configuration for themes
482#[derive(Debug, Clone)]
483pub struct ThemeBorders {
484    /// Thin border width
485    pub thin: f64,
486    /// Medium border width
487    pub medium: f64,
488    /// Thick border width
489    pub thick: f64,
490    /// Border radius for rounded corners
491    pub radius: f64,
492}
493
494impl ThemeBorders {
495    pub fn corporate() -> Self {
496        Self {
497            thin: 0.5,
498            medium: 1.0,
499            thick: 2.0,
500            radius: 4.0,
501        }
502    }
503
504    pub fn minimal() -> Self {
505        Self {
506            thin: 0.25,
507            medium: 0.5,
508            thick: 1.0,
509            radius: 2.0,
510        }
511    }
512
513    pub fn dark() -> Self {
514        Self {
515            thin: 0.5,
516            medium: 1.0,
517            thick: 2.0,
518            radius: 6.0,
519        }
520    }
521
522    pub fn colorful() -> Self {
523        Self {
524            thin: 1.0,
525            medium: 2.0,
526            thick: 3.0,
527            radius: 8.0,
528        }
529    }
530}
531
532impl Default for ThemeBorders {
533    fn default() -> Self {
534        Self::corporate()
535    }
536}
537
538/// Background configuration for themes
539#[derive(Debug, Clone)]
540pub struct ThemeBackgrounds {
541    /// Primary background color
542    pub primary: Color,
543    /// Secondary background color
544    pub secondary: Color,
545    /// Card/panel background color
546    pub surface: Color,
547    /// Hover state background
548    pub hover: Color,
549}
550
551impl ThemeBackgrounds {
552    pub fn corporate() -> Self {
553        Self {
554            primary: Color::white(),
555            secondary: Color::hex("#f8f9fa"),
556            surface: Color::white(),
557            hover: Color::hex("#f5f5f5"),
558        }
559    }
560
561    pub fn minimal() -> Self {
562        Self {
563            primary: Color::white(),
564            secondary: Color::hex("#fafafa"),
565            surface: Color::white(),
566            hover: Color::hex("#f0f0f0"),
567        }
568    }
569
570    pub fn dark() -> Self {
571        Self {
572            primary: Color::hex("#121212"),
573            secondary: Color::hex("#1e1e1e"),
574            surface: Color::hex("#2d2d2d"),
575            hover: Color::hex("#3d3d3d"),
576        }
577    }
578
579    pub fn colorful() -> Self {
580        Self {
581            primary: Color::hex("#fafafa"),
582            secondary: Color::hex("#fce4ec"),
583            surface: Color::white(),
584            hover: Color::hex("#f8bbd9"),
585        }
586    }
587}
588
589impl Default for ThemeBackgrounds {
590    fn default() -> Self {
591        Self::corporate()
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    #[test]
600    fn test_theme_creation() {
601        let theme = DashboardTheme::corporate();
602        assert_eq!(theme.typography.title_size, 24.0);
603        assert_eq!(theme.colors.primary, Color::hex("#1f4788"));
604    }
605
606    #[test]
607    fn test_theme_variants() {
608        let corporate = DashboardTheme::corporate();
609        let minimal = DashboardTheme::minimal();
610        let dark = DashboardTheme::dark();
611        let colorful = DashboardTheme::colorful();
612
613        // Themes should have different color schemes
614        assert_ne!(corporate.colors.primary, minimal.colors.primary);
615        assert_ne!(minimal.colors.background, dark.colors.background);
616        assert_ne!(dark.colors.accent, colorful.colors.accent);
617    }
618
619    #[test]
620    fn test_typography_variants() {
621        let professional = Typography::professional();
622        let minimal = Typography::minimal();
623
624        assert_eq!(professional.title_font, "Helvetica-Bold");
625        assert_eq!(minimal.title_font, "Helvetica-Light");
626        assert_ne!(professional.title_size, minimal.title_size);
627    }
628
629    #[test]
630    fn test_theme_spacing() {
631        let spacing = ThemeSpacing::corporate();
632        assert_eq!(spacing.xs, 4.0);
633        assert_eq!(spacing.sm, 8.0);
634        assert_eq!(spacing.md, 16.0);
635        assert_eq!(spacing.lg, 24.0);
636        assert_eq!(spacing.xl, 32.0);
637    }
638
639    #[test]
640    fn test_text_style_creation() {
641        let theme = DashboardTheme::default();
642        let title_style = theme.title_style();
643
644        assert_eq!(title_style.alignment, TextAlignment::Center);
645        assert_eq!(title_style.weight, FontWeight::Bold);
646    }
647}