Skip to main content

theme/theme/
family.rs

1//! A palette as data: a named family with a dark variant, a light variant or
2//! both, each a partial set of colour tokens laid over the shipped palette for
3//! its appearance.
4
5use std::{collections::BTreeMap, sync::LazyLock};
6
7use gpui::Hsla;
8use serde::Deserialize;
9
10use crate::{
11    Appearance, color,
12    theme::{SyntaxPalette, Theme},
13};
14
15/// One theme family, as a file declares it:
16///
17/// ```toml
18/// name = "Gruvbox"
19///
20/// [dark]
21/// bg = "#282828"
22/// text = "#ebdbb2"
23///
24/// [dark.syntax]
25/// keyword = "#fb4934"
26///
27/// [light.seed]
28/// surface = "#fbf1c7"
29/// ink = "#3c3836"
30/// accent = "#076678"
31/// ```
32///
33/// Keys are the names [`Theme::tokens_mut`] gives; a nested table joins its
34/// key onto its parent's with a dot, so `[dark.syntax]` `keyword` is
35/// `syntax.keyword`. Values are `#rrggbb` or `#rrggbbaa`. A `seed` table is a
36/// [`Seed`], applied before the variant's tokens.
37///
38/// A family has at least one variant; deserializing one with neither fails.
39#[derive(Debug, Clone, Deserialize)]
40#[serde(try_from = "RawFamily")]
41pub struct ThemeFamily {
42    pub name: String,
43    pub author: Option<String>,
44    pub dark: Option<Variant>,
45    pub light: Option<Variant>,
46}
47
48#[derive(Deserialize)]
49struct RawFamily {
50    name: String,
51    #[serde(default)]
52    author: Option<String>,
53    #[serde(default)]
54    dark: Option<Variant>,
55    #[serde(default)]
56    light: Option<Variant>,
57}
58
59impl TryFrom<RawFamily> for ThemeFamily {
60    type Error = String;
61
62    fn try_from(raw: RawFamily) -> Result<Self, String> {
63        if raw.dark.is_none() && raw.light.is_none() {
64            return Err(format!(
65                "theme family `{}` has neither a dark nor a light variant",
66                raw.name
67            ));
68        }
69        Ok(Self {
70            name: raw.name,
71            author: raw.author,
72            dark: raw.dark,
73            light: raw.light,
74        })
75    }
76}
77
78/// The families bezel ships, compiled in from `assets/themes/`.
79const BUNDLED: [&str; 27] = [
80    include_str!("../../assets/themes/absolutely.toml"),
81    include_str!("../../assets/themes/ayu.toml"),
82    include_str!("../../assets/themes/catppuccin.toml"),
83    include_str!("../../assets/themes/codex.toml"),
84    include_str!("../../assets/themes/dracula.toml"),
85    include_str!("../../assets/themes/everforest.toml"),
86    include_str!("../../assets/themes/github.toml"),
87    include_str!("../../assets/themes/gruvbox.toml"),
88    include_str!("../../assets/themes/linear.toml"),
89    include_str!("../../assets/themes/lobster.toml"),
90    include_str!("../../assets/themes/material.toml"),
91    include_str!("../../assets/themes/matrix.toml"),
92    include_str!("../../assets/themes/monokai.toml"),
93    include_str!("../../assets/themes/night-owl.toml"),
94    include_str!("../../assets/themes/nord.toml"),
95    include_str!("../../assets/themes/notion.toml"),
96    include_str!("../../assets/themes/one.toml"),
97    include_str!("../../assets/themes/oscurange.toml"),
98    include_str!("../../assets/themes/proof.toml"),
99    include_str!("../../assets/themes/raycast.toml"),
100    include_str!("../../assets/themes/rose-pine.toml"),
101    include_str!("../../assets/themes/sentry.toml"),
102    include_str!("../../assets/themes/solarized.toml"),
103    include_str!("../../assets/themes/temple.toml"),
104    include_str!("../../assets/themes/tokyo-night.toml"),
105    include_str!("../../assets/themes/vercel.toml"),
106    include_str!("../../assets/themes/vscode-plus.toml"),
107];
108
109static FAMILIES: LazyLock<Vec<ThemeFamily>> = LazyLock::new(|| {
110    BUNDLED
111        .iter()
112        .map(|file| toml::from_str(file).expect("bundled theme"))
113        .collect()
114});
115
116impl ThemeFamily {
117    /// Every bundled family, in file-name order.
118    pub fn bundled() -> &'static [ThemeFamily] {
119        &FAMILIES
120    }
121
122    /// The bundled family `name` names, ignoring case.
123    pub fn named(name: &str) -> Option<&'static ThemeFamily> {
124        Self::bundled()
125            .iter()
126            .find(|family| family.name.eq_ignore_ascii_case(name))
127    }
128
129    /// The variant for `appearance`, if the family has one.
130    pub fn variant(&self, appearance: Appearance) -> Option<&Variant> {
131        match appearance {
132            Appearance::Dark => self.dark.as_ref(),
133            Appearance::Light => self.light.as_ref(),
134        }
135    }
136
137    /// The shipped palette for `appearance` with this family's variant laid
138    /// over it, carrying the family's name in [`Theme::family`]. `None` when
139    /// the family has no variant for `appearance`.
140    pub fn theme(&self, appearance: Appearance) -> Option<Theme> {
141        let variant = self.variant(appearance)?;
142        let mut theme = Theme::for_appearance(appearance);
143        variant.apply(&mut theme);
144        theme.family = Some(self.name.clone().into());
145        Some(theme)
146    }
147}
148
149/// What one variant sets: a [`Seed`] and the tokens laid over it. A token
150/// neither sets keeps the shipped palette's value.
151#[derive(Debug, Clone, Default, Deserialize)]
152#[serde(from = "RawVariant")]
153pub struct Variant {
154    seed: Option<Seed>,
155    tokens: BTreeMap<String, Hsla>,
156}
157
158#[derive(Deserialize)]
159struct RawVariant {
160    #[serde(default)]
161    seed: Option<Seed>,
162    #[serde(flatten)]
163    tokens: BTreeMap<String, Node>,
164}
165
166/// A value in a variant's table: a colour, or a table of more.
167#[derive(Deserialize)]
168#[serde(untagged)]
169enum Node {
170    Color(Hsla),
171    Table(BTreeMap<String, Node>),
172}
173
174impl From<RawVariant> for Variant {
175    fn from(raw: RawVariant) -> Self {
176        fn flatten(prefix: &str, table: BTreeMap<String, Node>, out: &mut BTreeMap<String, Hsla>) {
177            for (key, node) in table {
178                let name = match prefix {
179                    "" => key,
180                    _ => format!("{prefix}.{key}"),
181                };
182                match node {
183                    Node::Color(color) => {
184                        out.insert(name, color);
185                    }
186                    Node::Table(table) => flatten(&name, table, out),
187                }
188            }
189        }
190        let mut tokens = BTreeMap::new();
191        flatten("", raw.tokens, &mut tokens);
192        Self {
193            seed: raw.seed,
194            tokens,
195        }
196    }
197}
198
199impl Variant {
200    /// The colour this variant's tokens give `token`, if they name one. A
201    /// colour the seed decides is not one.
202    pub fn get(&self, token: &str) -> Option<Hsla> {
203        self.tokens.get(token).copied()
204    }
205
206    pub fn seed(&self) -> Option<&Seed> {
207        self.seed.as_ref()
208    }
209
210    /// Apply the seed, then write every token this variant names into
211    /// `theme`. Returns the names that are not tokens, which are skipped.
212    pub fn apply(&self, theme: &mut Theme) -> Vec<&str> {
213        if let Some(seed) = &self.seed {
214            seed.apply(theme);
215        }
216        let mut slots: BTreeMap<&str, &mut Hsla> = theme.tokens_mut().into_iter().collect();
217        self.tokens
218            .iter()
219            .filter_map(|(name, color)| match slots.get_mut(name.as_str()) {
220                Some(slot) => {
221                    **slot = *color;
222                    None
223                }
224                None => Some(name.as_str()),
225            })
226            .collect()
227    }
228}
229
230/// The few colours a whole variant is grown from: a background, the text on
231/// it and an accent, with the diff and highlight colours optional.
232#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
233#[serde(deny_unknown_fields)]
234pub struct Seed {
235    /// The main panel, [`Theme::bg`].
236    pub surface: Hsla,
237    /// Body text, [`Theme::text`].
238    pub ink: Hsla,
239    pub accent: Hsla,
240    /// Added lines and success. Unset keeps the shipped green.
241    #[serde(default)]
242    pub added: Option<Hsla>,
243    /// Removed lines and danger. Unset keeps the shipped red.
244    #[serde(default)]
245    pub removed: Option<Hsla>,
246    /// [`Theme::busy`]. Unset keeps the shipped pink.
247    #[serde(default)]
248    pub highlight: Option<Hsla>,
249}
250
251impl Seed {
252    /// Write every opaque surface, text, accent and syntax token from the
253    /// seed into `theme`, which is the shipped palette for its appearance.
254    /// Translucent tokens (hovers, borders, washes) and warning keep the
255    /// shipped values, as does the terminal's ANSI set.
256    pub fn apply(&self, theme: &mut Theme) {
257        let (bg, ink, accent) = (self.surface, self.ink, self.accent);
258        let toward = |t: f32| color::mix_oklab(bg, ink, t);
259        let dark = theme.appearance.is_dark();
260        theme.bg = bg;
261        theme.surface = toward(0.035);
262        if dark {
263            theme.surface_card = toward(0.04);
264            theme.surface_dialog = toward(0.05);
265            theme.surface_overlay = toward(0.08);
266            theme.surface_raised = toward(0.10);
267            theme.surface_raised_hover = toward(0.15);
268        } else {
269            theme.surface_card = bg;
270            theme.surface_dialog = bg;
271            theme.surface_overlay = bg;
272            theme.surface_raised = toward(0.07);
273            theme.surface_raised_hover = toward(0.12);
274            theme.input_bg = bg;
275        }
276        theme.text = ink;
277        theme.text_dim = toward(0.58);
278        theme.solid = ink;
279        theme.on_solid = bg;
280        theme.cursor = ink;
281        theme.code_text = ink;
282        theme.accent = accent;
283        theme.accent_strong = accent;
284        theme.on_accent = color::legible_on(accent, bg, ink);
285        theme.caret = accent;
286        theme.selection = color::mix_oklab(bg, accent, if dark { 0.35 } else { 0.25 });
287        if let Some(removed) = self.removed {
288            theme.danger = removed;
289            theme.danger_strong = removed;
290            theme.danger_muted = color::mix_oklab(removed, ink, 0.3);
291            theme.diff_del = removed;
292        }
293        if let Some(added) = self.added {
294            theme.success = added;
295            theme.success_muted = color::mix_oklab(added, ink, 0.3);
296            theme.diff_add = added;
297        }
298        if let Some(highlight) = self.highlight {
299            theme.busy = highlight;
300        }
301        theme.terminal_bg = bg;
302        theme.vibrancy_tone = bg;
303        // As far toward the ink as the shipped dark tone stands from its
304        // `bg`; the shipped light tone is its chrome's.
305        theme.content_background = match dark {
306            true => toward(0.22),
307            false => bg,
308        };
309        let comment = toward(0.45);
310        theme.syntax = if dark {
311            SyntaxPalette::dark(ink, comment, theme.danger)
312        } else {
313            SyntaxPalette::light(ink, comment, theme.danger)
314        };
315    }
316}
317
318impl Theme {
319    /// Every colour token by the name a [`ThemeFamily`] file sets it under.
320    ///
321    /// `text_muted` and `text_faint` are absent: [`Brand::ink`](crate::Brand::ink)
322    /// writes them from `text` on every install.
323    pub fn tokens_mut(&mut self) -> Vec<(&'static str, &mut Hsla)> {
324        let syntax = &mut self.syntax;
325        let [
326            black,
327            red,
328            green,
329            yellow,
330            blue,
331            magenta,
332            cyan,
333            white,
334            bright_black,
335            bright_red,
336            bright_green,
337            bright_yellow,
338            bright_blue,
339            bright_magenta,
340            bright_cyan,
341            bright_white,
342        ] = &mut self.terminal_ansi;
343        vec![
344            ("bg", &mut self.bg),
345            ("surface", &mut self.surface),
346            ("surface_raised", &mut self.surface_raised),
347            ("surface_card", &mut self.surface_card),
348            ("surface_dialog", &mut self.surface_dialog),
349            ("surface_overlay", &mut self.surface_overlay),
350            ("element_hover", &mut self.element_hover),
351            ("element_active", &mut self.element_active),
352            ("border_faint", &mut self.border_faint),
353            ("border", &mut self.border),
354            ("border_strong", &mut self.border_strong),
355            ("canvas_bg", &mut self.canvas_bg),
356            ("canvas_grid", &mut self.canvas_grid),
357            ("text", &mut self.text),
358            ("text_dim", &mut self.text_dim),
359            ("solid", &mut self.solid),
360            ("on_solid", &mut self.on_solid),
361            ("accent", &mut self.accent),
362            ("accent_strong", &mut self.accent_strong),
363            ("on_accent", &mut self.on_accent),
364            ("danger", &mut self.danger),
365            ("danger_muted", &mut self.danger_muted),
366            ("warning", &mut self.warning),
367            ("warning_muted", &mut self.warning_muted),
368            ("success", &mut self.success),
369            ("busy", &mut self.busy),
370            ("success_muted", &mut self.success_muted),
371            ("surface_raised_hover", &mut self.surface_raised_hover),
372            ("band", &mut self.band),
373            ("input_bg", &mut self.input_bg),
374            ("selection", &mut self.selection),
375            ("cursor", &mut self.cursor),
376            ("caret", &mut self.caret),
377            ("ring", &mut self.ring),
378            ("drop_line", &mut self.drop_line),
379            ("drop_target", &mut self.drop_target),
380            ("danger_strong", &mut self.danger_strong),
381            ("code_text", &mut self.code_text),
382            ("code_wash", &mut self.code_wash),
383            ("diff_add", &mut self.diff_add),
384            ("diff_del", &mut self.diff_del),
385            ("diff_hunk_bg", &mut self.diff_hunk_bg),
386            ("vibrancy_tone", &mut self.vibrancy_tone),
387            ("content_background", &mut self.content_background),
388            ("syntax.comment", &mut syntax.comment),
389            ("syntax.keyword", &mut syntax.keyword),
390            ("syntax.string", &mut syntax.string),
391            ("syntax.string_special", &mut syntax.string_special),
392            ("syntax.escape", &mut syntax.escape),
393            ("syntax.number", &mut syntax.number),
394            ("syntax.boolean", &mut syntax.boolean),
395            ("syntax.type_name", &mut syntax.type_name),
396            ("syntax.type_builtin", &mut syntax.type_builtin),
397            ("syntax.constructor", &mut syntax.constructor),
398            ("syntax.function", &mut syntax.function),
399            ("syntax.function_builtin", &mut syntax.function_builtin),
400            ("syntax.macro_name", &mut syntax.macro_name),
401            ("syntax.property", &mut syntax.property),
402            ("syntax.constant", &mut syntax.constant),
403            ("syntax.variable", &mut syntax.variable),
404            ("syntax.variable_special", &mut syntax.variable_special),
405            ("syntax.parameter", &mut syntax.parameter),
406            ("syntax.operator", &mut syntax.operator),
407            ("syntax.punctuation", &mut syntax.punctuation),
408            ("syntax.tag", &mut syntax.tag),
409            ("syntax.attribute", &mut syntax.attribute),
410            ("syntax.label", &mut syntax.label),
411            ("syntax.invalid", &mut syntax.invalid),
412            ("terminal.bg", &mut self.terminal_bg),
413            ("terminal.ansi.black", black),
414            ("terminal.ansi.red", red),
415            ("terminal.ansi.green", green),
416            ("terminal.ansi.yellow", yellow),
417            ("terminal.ansi.blue", blue),
418            ("terminal.ansi.magenta", magenta),
419            ("terminal.ansi.cyan", cyan),
420            ("terminal.ansi.white", white),
421            ("terminal.ansi.bright_black", bright_black),
422            ("terminal.ansi.bright_red", bright_red),
423            ("terminal.ansi.bright_green", bright_green),
424            ("terminal.ansi.bright_yellow", bright_yellow),
425            ("terminal.ansi.bright_blue", bright_blue),
426            ("terminal.ansi.bright_magenta", bright_magenta),
427            ("terminal.ansi.bright_cyan", bright_cyan),
428            ("terminal.ansi.bright_white", bright_white),
429        ]
430    }
431}