1use 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#[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
78const 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 pub fn bundled() -> &'static [ThemeFamily] {
119 &FAMILIES
120 }
121
122 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 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 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#[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#[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 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 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#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
233#[serde(deny_unknown_fields)]
234pub struct Seed {
235 pub surface: Hsla,
237 pub ink: Hsla,
239 pub accent: Hsla,
240 #[serde(default)]
242 pub added: Option<Hsla>,
243 #[serde(default)]
245 pub removed: Option<Hsla>,
246 #[serde(default)]
248 pub highlight: Option<Hsla>,
249}
250
251impl Seed {
252 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 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 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 ("text", &mut self.text),
356 ("text_dim", &mut self.text_dim),
357 ("solid", &mut self.solid),
358 ("on_solid", &mut self.on_solid),
359 ("accent", &mut self.accent),
360 ("accent_strong", &mut self.accent_strong),
361 ("on_accent", &mut self.on_accent),
362 ("danger", &mut self.danger),
363 ("danger_muted", &mut self.danger_muted),
364 ("warning", &mut self.warning),
365 ("warning_muted", &mut self.warning_muted),
366 ("success", &mut self.success),
367 ("busy", &mut self.busy),
368 ("success_muted", &mut self.success_muted),
369 ("surface_raised_hover", &mut self.surface_raised_hover),
370 ("band", &mut self.band),
371 ("input_bg", &mut self.input_bg),
372 ("selection", &mut self.selection),
373 ("cursor", &mut self.cursor),
374 ("caret", &mut self.caret),
375 ("ring", &mut self.ring),
376 ("drop_line", &mut self.drop_line),
377 ("drop_target", &mut self.drop_target),
378 ("danger_strong", &mut self.danger_strong),
379 ("code_text", &mut self.code_text),
380 ("code_wash", &mut self.code_wash),
381 ("diff_add", &mut self.diff_add),
382 ("diff_del", &mut self.diff_del),
383 ("diff_hunk_bg", &mut self.diff_hunk_bg),
384 ("vibrancy_tone", &mut self.vibrancy_tone),
385 ("content_background", &mut self.content_background),
386 ("syntax.comment", &mut syntax.comment),
387 ("syntax.keyword", &mut syntax.keyword),
388 ("syntax.string", &mut syntax.string),
389 ("syntax.string_special", &mut syntax.string_special),
390 ("syntax.escape", &mut syntax.escape),
391 ("syntax.number", &mut syntax.number),
392 ("syntax.boolean", &mut syntax.boolean),
393 ("syntax.type_name", &mut syntax.type_name),
394 ("syntax.type_builtin", &mut syntax.type_builtin),
395 ("syntax.constructor", &mut syntax.constructor),
396 ("syntax.function", &mut syntax.function),
397 ("syntax.function_builtin", &mut syntax.function_builtin),
398 ("syntax.macro_name", &mut syntax.macro_name),
399 ("syntax.property", &mut syntax.property),
400 ("syntax.constant", &mut syntax.constant),
401 ("syntax.variable", &mut syntax.variable),
402 ("syntax.variable_special", &mut syntax.variable_special),
403 ("syntax.parameter", &mut syntax.parameter),
404 ("syntax.operator", &mut syntax.operator),
405 ("syntax.punctuation", &mut syntax.punctuation),
406 ("syntax.tag", &mut syntax.tag),
407 ("syntax.attribute", &mut syntax.attribute),
408 ("syntax.label", &mut syntax.label),
409 ("syntax.invalid", &mut syntax.invalid),
410 ("terminal.bg", &mut self.terminal_bg),
411 ("terminal.ansi.black", black),
412 ("terminal.ansi.red", red),
413 ("terminal.ansi.green", green),
414 ("terminal.ansi.yellow", yellow),
415 ("terminal.ansi.blue", blue),
416 ("terminal.ansi.magenta", magenta),
417 ("terminal.ansi.cyan", cyan),
418 ("terminal.ansi.white", white),
419 ("terminal.ansi.bright_black", bright_black),
420 ("terminal.ansi.bright_red", bright_red),
421 ("terminal.ansi.bright_green", bright_green),
422 ("terminal.ansi.bright_yellow", bright_yellow),
423 ("terminal.ansi.bright_blue", bright_blue),
424 ("terminal.ansi.bright_magenta", bright_magenta),
425 ("terminal.ansi.bright_cyan", bright_cyan),
426 ("terminal.ansi.bright_white", bright_white),
427 ]
428 }
429}