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    /// Brand or accent color for interactive elements.
19    pub accent: Option<Rgba>,
20    /// Main application background color.
21    pub background: Option<Rgba>,
22    /// Main text color on the application background.
23    pub foreground: Option<Rgba>,
24    /// Elevated surface color (cards, dialogs).
25    pub surface: Option<Rgba>,
26    /// Default border color.
27    pub border: Option<Rgba>,
28    /// Muted/subdued text color for secondary content.
29    pub muted: Option<Rgba>,
30    /// Shadow or elevation color.
31    pub shadow: Option<Rgba>,
32    // Primary (2)
33    /// Primary action background color.
34    pub primary_background: Option<Rgba>,
35    /// Text color on primary background.
36    pub primary_foreground: Option<Rgba>,
37    // Secondary (2)
38    /// Secondary action background color.
39    pub secondary_background: Option<Rgba>,
40    /// Text color on secondary background.
41    pub secondary_foreground: Option<Rgba>,
42    // Status (8)
43    /// Error or danger status color.
44    pub danger: Option<Rgba>,
45    /// Text color on danger background.
46    pub danger_foreground: Option<Rgba>,
47    /// Warning status color.
48    pub warning: Option<Rgba>,
49    /// Text color on warning background.
50    pub warning_foreground: Option<Rgba>,
51    /// Success or positive status color.
52    pub success: Option<Rgba>,
53    /// Text color on success background.
54    pub success_foreground: Option<Rgba>,
55    /// Informational status color.
56    pub info: Option<Rgba>,
57    /// Text color on info background.
58    pub info_foreground: Option<Rgba>,
59    // Interactive (4)
60    /// Background color for selected items.
61    pub selection: Option<Rgba>,
62    /// Text color on selected background.
63    pub selection_foreground: Option<Rgba>,
64    /// Hyperlink text color.
65    pub link: Option<Rgba>,
66    /// Color of keyboard focus indicators.
67    pub focus_ring: Option<Rgba>,
68    // Panel (6)
69    /// Sidebar background color.
70    pub sidebar: Option<Rgba>,
71    /// Text color on sidebar background.
72    pub sidebar_foreground: Option<Rgba>,
73    /// Tooltip background color.
74    pub tooltip: Option<Rgba>,
75    /// Text color on tooltip background.
76    pub tooltip_foreground: Option<Rgba>,
77    /// Popover/dropdown background color.
78    pub popover: Option<Rgba>,
79    /// Text color on popover background.
80    pub popover_foreground: Option<Rgba>,
81    // Component (7)
82    /// Button background color.
83    pub button: Option<Rgba>,
84    /// Text color on button background.
85    pub button_foreground: Option<Rgba>,
86    /// Text input field background color.
87    pub input: Option<Rgba>,
88    /// Text color inside input fields.
89    pub input_foreground: Option<Rgba>,
90    /// Color for disabled UI elements.
91    pub disabled: Option<Rgba>,
92    /// Separator/divider line color.
93    pub separator: Option<Rgba>,
94    /// Alternating row background for lists and tables.
95    pub alternate_row: Option<Rgba>,
96}
97
98impl_merge!(ThemeColors {
99    option {
100        accent, background, foreground, surface, border, muted, shadow,
101        primary_background, primary_foreground,
102        secondary_background, secondary_foreground,
103        danger, danger_foreground, warning, warning_foreground,
104        success, success_foreground, info, info_foreground,
105        selection, selection_foreground, link, focus_ring,
106        sidebar, sidebar_foreground, tooltip, tooltip_foreground,
107        popover, popover_foreground,
108        button, button_foreground, input, input_foreground,
109        disabled, separator, alternate_row
110    }
111});
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::Rgba;
117
118    // === Field count validation ===
119
120    #[test]
121    fn total_color_roles_is_36() {
122        // 7 core + 2 primary + 2 secondary + 8 status +
123        // 4 interactive + 6 panel + 7 component = 36
124        assert_eq!(7 + 2 + 2 + 8 + 4 + 6 + 7, 36);
125    }
126
127    // === is_empty tests ===
128
129    #[test]
130    fn theme_colors_default_is_empty() {
131        assert!(ThemeColors::default().is_empty());
132    }
133
134    #[test]
135    fn theme_colors_not_empty_when_field_set() {
136        let tc = ThemeColors {
137            accent: Some(Rgba::rgb(255, 0, 0)),
138            ..Default::default()
139        };
140        assert!(!tc.is_empty());
141    }
142
143    // === merge tests ===
144
145    #[test]
146    fn merge_some_replaces_none() {
147        let mut base = ThemeColors::default();
148        let overlay = ThemeColors {
149            accent: Some(Rgba::rgb(255, 0, 0)),
150            ..Default::default()
151        };
152        base.merge(&overlay);
153        assert_eq!(base.accent, Some(Rgba::rgb(255, 0, 0)));
154    }
155
156    #[test]
157    fn merge_none_preserves_base() {
158        let mut base = ThemeColors {
159            accent: Some(Rgba::rgb(0, 0, 255)),
160            ..Default::default()
161        };
162        let overlay = ThemeColors::default(); // all None
163        base.merge(&overlay);
164        assert_eq!(base.accent, Some(Rgba::rgb(0, 0, 255)));
165    }
166
167    #[test]
168    fn merge_some_replaces_some() {
169        let mut base = ThemeColors {
170            accent: Some(Rgba::rgb(0, 0, 255)),
171            ..Default::default()
172        };
173        let overlay = ThemeColors {
174            accent: Some(Rgba::rgb(255, 0, 0)),
175            ..Default::default()
176        };
177        base.merge(&overlay);
178        assert_eq!(base.accent, Some(Rgba::rgb(255, 0, 0)));
179    }
180
181    #[test]
182    fn merge_flat_fields_across_groups() {
183        let mut base = ThemeColors {
184            background: Some(Rgba::rgb(255, 255, 255)),
185            danger: Some(Rgba::rgb(200, 0, 0)),
186            ..Default::default()
187        };
188
189        let overlay = ThemeColors {
190            accent: Some(Rgba::rgb(0, 120, 215)),
191            danger: Some(Rgba::rgb(255, 0, 0)), // override
192            ..Default::default()
193        };
194
195        base.merge(&overlay);
196
197        // overlay accent applied
198        assert_eq!(base.accent, Some(Rgba::rgb(0, 120, 215)));
199        // base background preserved (overlay had None)
200        assert_eq!(base.background, Some(Rgba::rgb(255, 255, 255)));
201        // overlay danger replaced base danger
202        assert_eq!(base.danger, Some(Rgba::rgb(255, 0, 0)));
203    }
204
205    #[test]
206    fn merge_primary_and_secondary_fields() {
207        let mut base = ThemeColors {
208            primary_background: Some(Rgba::rgb(0, 0, 255)),
209            ..Default::default()
210        };
211
212        let overlay = ThemeColors {
213            secondary_foreground: Some(Rgba::rgb(255, 255, 255)),
214            ..Default::default()
215        };
216
217        base.merge(&overlay);
218
219        // Primary base preserved
220        assert_eq!(base.primary_background, Some(Rgba::rgb(0, 0, 255)));
221        // Secondary overlay applied
222        assert_eq!(base.secondary_foreground, Some(Rgba::rgb(255, 255, 255)));
223    }
224}