1#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
8pub enum Appearance {
9 #[default]
10 System,
11 Light,
12 Dark,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
18pub enum PaletteId {
19 Paper,
20 Ink,
21 NordSnow,
22 Nord,
23 TokyoDay,
24 TokyoNight,
25 CatppuccinLatte,
26 CatppuccinFrappe,
27 CatppuccinMacchiato,
28 CatppuccinMocha,
29}
30
31pub const ALL: [PaletteId; 10] = [
33 PaletteId::Paper,
34 PaletteId::NordSnow,
35 PaletteId::TokyoDay,
36 PaletteId::CatppuccinLatte,
37 PaletteId::Ink,
38 PaletteId::Nord,
39 PaletteId::TokyoNight,
40 PaletteId::CatppuccinFrappe,
41 PaletteId::CatppuccinMacchiato,
42 PaletteId::CatppuccinMocha,
43];
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
48pub struct ThemePreferences {
49 pub mode: Appearance,
50 pub light: PaletteId,
51 pub dark: PaletteId,
52}
53
54impl Default for ThemePreferences {
55 fn default() -> Self {
56 Self {
57 mode: Appearance::System,
58 light: PaletteId::Paper,
59 dark: PaletteId::Ink,
60 }
61 }
62}
63
64impl ThemePreferences {
65 pub fn resolve(&self, system_dark: bool) -> PaletteId {
66 match self.mode {
67 Appearance::Dark => self.dark,
68 Appearance::Light => self.light,
69 Appearance::System => {
70 if system_dark {
71 self.dark
72 } else {
73 self.light
74 }
75 }
76 }
77 }
78
79 pub fn select(&mut self, palette: PaletteId) {
80 if palette.is_dark() {
81 self.dark = palette;
82 } else {
83 self.light = palette;
84 }
85 }
86
87 pub fn valid(&self) -> bool {
88 !self.light.is_dark() && self.dark.is_dark()
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct Palette {
97 pub canvas: u32,
98 pub surface: u32,
99 pub inset: u32,
100 pub text: u32,
101 pub muted: u32,
102 pub subtle: u32,
103 pub border: u32,
104 pub control: u32,
105 pub accent: u32,
106 pub on_accent: u32,
107 pub success: u32,
108 pub warning: u32,
109 pub danger: u32,
110 pub selection: u32,
111}
112
113macro_rules! palette {
114 ($canvas:expr,$surface:expr,$inset:expr,$text:expr,$muted:expr,$subtle:expr,$border:expr,$control:expr,$accent:expr,$on:expr,$success:expr,$warning:expr,$danger:expr,$selection:expr) => {
115 Palette {
116 canvas: $canvas,
117 surface: $surface,
118 inset: $inset,
119 text: $text,
120 muted: $muted,
121 subtle: $subtle,
122 border: $border,
123 control: $control,
124 accent: $accent,
125 on_accent: $on,
126 success: $success,
127 warning: $warning,
128 danger: $danger,
129 selection: $selection,
130 }
131 };
132}
133
134impl PaletteId {
135 pub fn name(self) -> &'static str {
136 match self {
137 Self::Paper => "Paper",
138 Self::Ink => "Ink",
139 Self::NordSnow => "Nord Snow",
140 Self::Nord => "Nord",
141 Self::TokyoDay => "Tokyo Day",
142 Self::TokyoNight => "Tokyo Night",
143 Self::CatppuccinLatte => "Catppuccin Latte",
144 Self::CatppuccinFrappe => "Catppuccin Frappé",
145 Self::CatppuccinMacchiato => "Catppuccin Macchiato",
146 Self::CatppuccinMocha => "Catppuccin Mocha",
147 }
148 }
149
150 pub fn is_dark(self) -> bool {
151 !matches!(
152 self,
153 Self::Paper | Self::NordSnow | Self::TokyoDay | Self::CatppuccinLatte
154 )
155 }
156
157 pub fn colors(self) -> Palette {
158 match self {
159 Self::Paper => palette!(
160 0xf6f7f9, 0xffffff, 0xeff1f5, 0x202632, 0x626b7b, 0x7e8795, 0xe1e5eb, 0xa2aaba,
161 0x3968d7, 0xffffff, 0x297749, 0x966111, 0xc2394c, 0xdde7fd
162 ),
163 Self::Ink => palette!(
164 0x16191f, 0x20242d, 0x1a1e26, 0xe7eaf0, 0xa7afbf, 0x8590a2, 0x333b49, 0x647187,
165 0x8baffb, 0x16213a, 0x93cfa4, 0xe3bc77, 0xee9aa8, 0x334769
166 ),
167 Self::NordSnow => palette!(
168 0xeceff4, 0xf8f9fb, 0xe5e9f0, 0x2e3440, 0x4c566a, 0x65738b, 0xd8dee9, 0x8393aa,
169 0x5e81ac, 0xffffff, 0x4f7651, 0x8e6937, 0xa94452, 0xd8e4f0
170 ),
171 Self::Nord => palette!(
172 0x2e3440, 0x3b4252, 0x323946, 0xeceff4, 0xd8dee9, 0xa5b1c4, 0x4c566a, 0x6c7a94,
173 0x88c0d0, 0x2e3440, 0xa3be8c, 0xebcb8b, 0xbf616a, 0x4c566a
174 ),
175 Self::TokyoDay => palette!(
176 0xe1e2e7, 0xebecef, 0xd5d8e2, 0x343b58, 0x596078, 0x737b96, 0xc4c8da, 0x8990b3,
177 0x2e7de9, 0xffffff, 0x587539, 0x8c6c3e, 0xc62454, 0xb7c1e3
178 ),
179 Self::TokyoNight => palette!(
180 0x1a1b26, 0x24283b, 0x16161e, 0xc0caf5, 0xa9b1d6, 0x7982a9, 0x3b4261, 0x565f89,
181 0x7aa2f7, 0x16161e, 0x9ece6a, 0xe0af68, 0xf7768e, 0x283457
182 ),
183 Self::CatppuccinLatte => palette!(
184 0xeff1f5, 0xf7f8fa, 0xe6e9ef, 0x4c4f69, 0x6c6f85, 0x7c7f93, 0xccd0da, 0x9ca0b0,
185 0x1e66f5, 0xffffff, 0x388a25, 0x996011, 0xd20f39, 0xdce0e8
186 ),
187 Self::CatppuccinFrappe => palette!(
188 0x292c3c, 0x303446, 0x232634, 0xc6d0f5, 0xa5adce, 0x838ba7, 0x414559, 0x626880,
189 0x8caaee, 0x232634, 0xa6d189, 0xe5c890, 0xe78284, 0x414559
190 ),
191 Self::CatppuccinMacchiato => palette!(
192 0x1e2030, 0x24273a, 0x181926, 0xcad3f5, 0xa5adcb, 0x8087a2, 0x363a4f, 0x5b6078,
193 0x8aadf4, 0x181926, 0xa6da95, 0xeed49f, 0xed8796, 0x363a4f
194 ),
195 Self::CatppuccinMocha => palette!(
196 0x181825, 0x1e1e2e, 0x11111b, 0xcdd6f4, 0xa6adc8, 0x7f849c, 0x313244, 0x585b70,
197 0x89b4fa, 0x11111b, 0xa6e3a1, 0xf9e2af, 0xf38ba8, 0x313244
198 ),
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn system_appearance_uses_independently_configured_palettes() {
209 let mut prefs = ThemePreferences::default();
210 prefs.select(PaletteId::CatppuccinLatte);
211 prefs.select(PaletteId::TokyoNight);
212 assert_eq!(prefs.resolve(false), PaletteId::CatppuccinLatte);
213 assert_eq!(prefs.resolve(true), PaletteId::TokyoNight);
214 prefs.mode = Appearance::Light;
215 assert_eq!(prefs.resolve(true), PaletteId::CatppuccinLatte);
216 prefs.mode = Appearance::Dark;
217 assert_eq!(prefs.resolve(false), PaletteId::TokyoNight);
218 }
219
220 #[cfg(feature = "serde")]
221 #[test]
222 fn every_palette_roundtrips_and_has_distinct_content_layers() {
223 for palette in ALL {
224 let serialized = serde_json::to_string(&palette).unwrap();
225 assert_eq!(
226 serde_json::from_str::<PaletteId>(&serialized).unwrap(),
227 palette
228 );
229 let c = palette.colors();
230 assert_ne!(c.canvas, c.surface, "{}", palette.name());
231 assert_ne!(c.text, c.surface, "{}", palette.name());
232 assert_ne!(c.accent, c.on_accent, "{}", palette.name());
233 }
234 assert!(ThemePreferences::default().valid());
235 assert!(
236 !ThemePreferences {
237 light: PaletteId::Ink,
238 ..Default::default()
239 }
240 .valid()
241 );
242 assert!(serde_json::from_str::<PaletteId>("\"unknown_theme\"").is_err());
243 }
244}