Skip to main content

gpui_kit_material_color/
palette.rs

1//! Built-in source palettes and the independent light/dark preference.
2//! Upstream colors and adaptations are recorded in `PALETTES.md`; their
3//! licenses are in `licenses/`.
4
5#[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
31/// Every built-in palette, light ones first, in presentation order.
32pub 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/// A source palette as `0xRRGGBB` values. [`crate::Scheme::from_palette`]
93/// derives the semantic roles from it; the stored colors are never painted
94/// with an adjustment silently applied.
95#[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}