1use serde::{Deserialize, Serialize};
4
5use crate::Rgba;
6
7#[serde_with::skip_serializing_none]
13#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
14#[serde(default)]
15#[non_exhaustive]
16pub struct ThemeColors {
17 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 pub primary_background: Option<Rgba>,
27 pub primary_foreground: Option<Rgba>,
28 pub secondary_background: Option<Rgba>,
30 pub secondary_foreground: Option<Rgba>,
31 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 pub selection: Option<Rgba>,
42 pub selection_foreground: Option<Rgba>,
43 pub link: Option<Rgba>,
44 pub focus_ring: Option<Rgba>,
45 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 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 #[test]
85 fn total_color_roles_is_36() {
86 assert_eq!(7 + 2 + 2 + 8 + 4 + 6 + 7, 36);
89 }
90
91 #[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 #[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(); 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)), ..Default::default()
157 };
158
159 base.merge(&overlay);
160
161 assert_eq!(base.accent, Some(Rgba::rgb(0, 120, 215)));
163 assert_eq!(base.background, Some(Rgba::rgb(255, 255, 255)));
165 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 assert_eq!(base.primary_background, Some(Rgba::rgb(0, 0, 255)));
185 assert_eq!(base.secondary_foreground, Some(Rgba::rgb(255, 255, 255)));
187 }
188}