Skip to main content

native_theme/model/
colors.rs

1// Theme colors: flat 36-field struct covering all semantic color roles.
2
3use serde::{Deserialize, Serialize};
4
5use crate::Rgba;
6
7/// All theme colors as a flat set of 36 semantic color roles.
8///
9/// Organized into logical groups (core, primary, secondary, status,
10/// interactive, panel, component) but stored as direct fields for
11/// simpler access and flatter TOML serialization.
12#[serde_with::skip_serializing_none]
13#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
14#[serde(default)]
15#[non_exhaustive]
16pub struct ThemeColors {
17    // Core (7)
18    pub accent: Option<Rgba>,
19    pub background: Option<Rgba>,
20    pub foreground: Option<Rgba>,
21    pub surface: Option<Rgba>,
22    pub border: Option<Rgba>,
23    pub muted: Option<Rgba>,
24    pub shadow: Option<Rgba>,
25    // Primary (2)
26    pub primary_background: Option<Rgba>,
27    pub primary_foreground: Option<Rgba>,
28    // Secondary (2)
29    pub secondary_background: Option<Rgba>,
30    pub secondary_foreground: Option<Rgba>,
31    // Status (8)
32    pub danger: Option<Rgba>,
33    pub danger_foreground: Option<Rgba>,
34    pub warning: Option<Rgba>,
35    pub warning_foreground: Option<Rgba>,
36    pub success: Option<Rgba>,
37    pub success_foreground: Option<Rgba>,
38    pub info: Option<Rgba>,
39    pub info_foreground: Option<Rgba>,
40    // Interactive (4)
41    pub selection: Option<Rgba>,
42    pub selection_foreground: Option<Rgba>,
43    pub link: Option<Rgba>,
44    pub focus_ring: Option<Rgba>,
45    // Panel (6)
46    pub sidebar: Option<Rgba>,
47    pub sidebar_foreground: Option<Rgba>,
48    pub tooltip: Option<Rgba>,
49    pub tooltip_foreground: Option<Rgba>,
50    pub popover: Option<Rgba>,
51    pub popover_foreground: Option<Rgba>,
52    // Component (7)
53    pub button: Option<Rgba>,
54    pub button_foreground: Option<Rgba>,
55    pub input: Option<Rgba>,
56    pub input_foreground: Option<Rgba>,
57    pub disabled: Option<Rgba>,
58    pub separator: Option<Rgba>,
59    pub alternate_row: Option<Rgba>,
60}
61
62impl_merge!(ThemeColors {
63    option {
64        accent, background, foreground, surface, border, muted, shadow,
65        primary_background, primary_foreground,
66        secondary_background, secondary_foreground,
67        danger, danger_foreground, warning, warning_foreground,
68        success, success_foreground, info, info_foreground,
69        selection, selection_foreground, link, focus_ring,
70        sidebar, sidebar_foreground, tooltip, tooltip_foreground,
71        popover, popover_foreground,
72        button, button_foreground, input, input_foreground,
73        disabled, separator, alternate_row
74    }
75});
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::Rgba;
81
82    // === Field count validation ===
83
84    #[test]
85    fn total_color_roles_is_36() {
86        // 7 core + 2 primary + 2 secondary + 8 status +
87        // 4 interactive + 6 panel + 7 component = 36
88        assert_eq!(7 + 2 + 2 + 8 + 4 + 6 + 7, 36);
89    }
90
91    // === is_empty tests ===
92
93    #[test]
94    fn theme_colors_default_is_empty() {
95        assert!(ThemeColors::default().is_empty());
96    }
97
98    #[test]
99    fn theme_colors_not_empty_when_field_set() {
100        let tc = ThemeColors {
101            accent: Some(Rgba::rgb(255, 0, 0)),
102            ..Default::default()
103        };
104        assert!(!tc.is_empty());
105    }
106
107    // === merge tests ===
108
109    #[test]
110    fn merge_some_replaces_none() {
111        let mut base = ThemeColors::default();
112        let overlay = ThemeColors {
113            accent: Some(Rgba::rgb(255, 0, 0)),
114            ..Default::default()
115        };
116        base.merge(&overlay);
117        assert_eq!(base.accent, Some(Rgba::rgb(255, 0, 0)));
118    }
119
120    #[test]
121    fn merge_none_preserves_base() {
122        let mut base = ThemeColors {
123            accent: Some(Rgba::rgb(0, 0, 255)),
124            ..Default::default()
125        };
126        let overlay = ThemeColors::default(); // all None
127        base.merge(&overlay);
128        assert_eq!(base.accent, Some(Rgba::rgb(0, 0, 255)));
129    }
130
131    #[test]
132    fn merge_some_replaces_some() {
133        let mut base = ThemeColors {
134            accent: Some(Rgba::rgb(0, 0, 255)),
135            ..Default::default()
136        };
137        let overlay = ThemeColors {
138            accent: Some(Rgba::rgb(255, 0, 0)),
139            ..Default::default()
140        };
141        base.merge(&overlay);
142        assert_eq!(base.accent, Some(Rgba::rgb(255, 0, 0)));
143    }
144
145    #[test]
146    fn merge_flat_fields_across_groups() {
147        let mut base = ThemeColors {
148            background: Some(Rgba::rgb(255, 255, 255)),
149            danger: Some(Rgba::rgb(200, 0, 0)),
150            ..Default::default()
151        };
152
153        let overlay = ThemeColors {
154            accent: Some(Rgba::rgb(0, 120, 215)),
155            danger: Some(Rgba::rgb(255, 0, 0)), // override
156            ..Default::default()
157        };
158
159        base.merge(&overlay);
160
161        // overlay accent applied
162        assert_eq!(base.accent, Some(Rgba::rgb(0, 120, 215)));
163        // base background preserved (overlay had None)
164        assert_eq!(base.background, Some(Rgba::rgb(255, 255, 255)));
165        // overlay danger replaced base danger
166        assert_eq!(base.danger, Some(Rgba::rgb(255, 0, 0)));
167    }
168
169    #[test]
170    fn merge_primary_and_secondary_fields() {
171        let mut base = ThemeColors {
172            primary_background: Some(Rgba::rgb(0, 0, 255)),
173            ..Default::default()
174        };
175
176        let overlay = ThemeColors {
177            secondary_foreground: Some(Rgba::rgb(255, 255, 255)),
178            ..Default::default()
179        };
180
181        base.merge(&overlay);
182
183        // Primary base preserved
184        assert_eq!(base.primary_background, Some(Rgba::rgb(0, 0, 255)));
185        // Secondary overlay applied
186        assert_eq!(base.secondary_foreground, Some(Rgba::rgb(255, 255, 255)));
187    }
188}