use std::collections::BTreeMap;
use gpui::Hsla;
use serde::Deserialize;
use crate::{
Appearance, color,
theme::{SyntaxPalette, Theme},
};
#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "RawFamily")]
pub struct ThemeFamily {
pub name: String,
pub author: Option<String>,
pub dark: Option<Variant>,
pub light: Option<Variant>,
}
#[derive(Deserialize)]
struct RawFamily {
name: String,
#[serde(default)]
author: Option<String>,
#[serde(default)]
dark: Option<Variant>,
#[serde(default)]
light: Option<Variant>,
}
impl TryFrom<RawFamily> for ThemeFamily {
type Error = String;
fn try_from(raw: RawFamily) -> Result<Self, String> {
if raw.dark.is_none() && raw.light.is_none() {
return Err(format!(
"theme family `{}` has neither a dark nor a light variant",
raw.name
));
}
Ok(Self {
name: raw.name,
author: raw.author,
dark: raw.dark,
light: raw.light,
})
}
}
impl ThemeFamily {
pub fn variant(&self, appearance: Appearance) -> Option<&Variant> {
match appearance {
Appearance::Dark => self.dark.as_ref(),
Appearance::Light => self.light.as_ref(),
}
}
pub fn theme(&self, appearance: Appearance) -> Option<Theme> {
let variant = self.variant(appearance)?;
let mut theme = Theme::for_appearance(appearance);
variant.apply(&mut theme);
theme.family = Some(self.name.clone().into());
Some(theme)
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(from = "RawVariant")]
pub struct Variant {
seed: Option<Seed>,
tokens: BTreeMap<String, Hsla>,
}
#[derive(Deserialize)]
struct RawVariant {
#[serde(default)]
seed: Option<Seed>,
#[serde(flatten)]
tokens: BTreeMap<String, Node>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Node {
Color(Hsla),
Table(BTreeMap<String, Node>),
}
impl From<RawVariant> for Variant {
fn from(raw: RawVariant) -> Self {
fn flatten(prefix: &str, table: BTreeMap<String, Node>, out: &mut BTreeMap<String, Hsla>) {
for (key, node) in table {
let name = match prefix {
"" => key,
_ => format!("{prefix}.{key}"),
};
match node {
Node::Color(color) => {
out.insert(name, color);
}
Node::Table(table) => flatten(&name, table, out),
}
}
}
let mut tokens = BTreeMap::new();
flatten("", raw.tokens, &mut tokens);
Self {
seed: raw.seed,
tokens,
}
}
}
impl Variant {
pub fn get(&self, token: &str) -> Option<Hsla> {
self.tokens.get(token).copied()
}
pub fn seed(&self) -> Option<&Seed> {
self.seed.as_ref()
}
pub fn apply(&self, theme: &mut Theme) -> Vec<&str> {
if let Some(seed) = &self.seed {
seed.apply(theme);
}
let mut slots: BTreeMap<&str, &mut Hsla> = theme.tokens_mut().into_iter().collect();
self.tokens
.iter()
.filter_map(|(name, color)| match slots.get_mut(name.as_str()) {
Some(slot) => {
**slot = *color;
None
}
None => Some(name.as_str()),
})
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Seed {
pub surface: Hsla,
pub ink: Hsla,
pub accent: Hsla,
#[serde(default)]
pub added: Option<Hsla>,
#[serde(default)]
pub removed: Option<Hsla>,
#[serde(default)]
pub highlight: Option<Hsla>,
}
impl Seed {
pub fn apply(&self, theme: &mut Theme) {
let (bg, ink, accent) = (self.surface, self.ink, self.accent);
let toward = |t: f32| color::mix_oklab(bg, ink, t);
let dark = theme.appearance.is_dark();
theme.bg = bg;
theme.surface = toward(0.035);
if dark {
theme.surface_card = toward(0.04);
theme.surface_dialog = toward(0.05);
theme.surface_overlay = toward(0.08);
theme.surface_raised = toward(0.10);
theme.surface_raised_hover = toward(0.15);
} else {
theme.surface_card = bg;
theme.surface_dialog = bg;
theme.surface_overlay = bg;
theme.surface_raised = toward(0.07);
theme.surface_raised_hover = toward(0.12);
theme.input_bg = bg;
}
theme.text = ink;
theme.text_dim = toward(0.58);
theme.solid = ink;
theme.on_solid = bg;
theme.cursor = ink;
theme.code_text = ink;
theme.accent = accent;
theme.accent_strong = accent;
theme.on_accent = if color::contrast_ratio(accent, bg) >= color::contrast_ratio(accent, ink)
{
bg
} else {
ink
};
theme.caret = accent;
theme.selection = color::mix_oklab(bg, accent, if dark { 0.35 } else { 0.25 });
if let Some(removed) = self.removed {
theme.danger = removed;
theme.danger_strong = removed;
theme.danger_muted = color::mix_oklab(removed, ink, 0.3);
theme.diff_del = removed;
}
if let Some(added) = self.added {
theme.success = added;
theme.success_muted = color::mix_oklab(added, ink, 0.3);
theme.diff_add = added;
}
if let Some(highlight) = self.highlight {
theme.busy = highlight;
}
theme.terminal_bg = bg;
theme.vibrancy_tone = bg;
let comment = toward(0.45);
theme.syntax = if dark {
SyntaxPalette::dark(ink, comment, theme.danger)
} else {
SyntaxPalette::light(ink, comment, theme.danger)
};
}
}
impl Theme {
pub fn tokens_mut(&mut self) -> Vec<(&'static str, &mut Hsla)> {
let syntax = &mut self.syntax;
let [
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
bright_black,
bright_red,
bright_green,
bright_yellow,
bright_blue,
bright_magenta,
bright_cyan,
bright_white,
] = &mut self.terminal_ansi;
vec![
("bg", &mut self.bg),
("surface", &mut self.surface),
("surface_raised", &mut self.surface_raised),
("surface_card", &mut self.surface_card),
("surface_dialog", &mut self.surface_dialog),
("surface_overlay", &mut self.surface_overlay),
("element_hover", &mut self.element_hover),
("element_active", &mut self.element_active),
("border_faint", &mut self.border_faint),
("border", &mut self.border),
("border_strong", &mut self.border_strong),
("text", &mut self.text),
("text_dim", &mut self.text_dim),
("solid", &mut self.solid),
("on_solid", &mut self.on_solid),
("accent", &mut self.accent),
("accent_strong", &mut self.accent_strong),
("on_accent", &mut self.on_accent),
("danger", &mut self.danger),
("danger_muted", &mut self.danger_muted),
("warning", &mut self.warning),
("warning_muted", &mut self.warning_muted),
("success", &mut self.success),
("busy", &mut self.busy),
("success_muted", &mut self.success_muted),
("surface_raised_hover", &mut self.surface_raised_hover),
("band", &mut self.band),
("input_bg", &mut self.input_bg),
("selection", &mut self.selection),
("cursor", &mut self.cursor),
("caret", &mut self.caret),
("ring", &mut self.ring),
("drop_line", &mut self.drop_line),
("drop_target", &mut self.drop_target),
("danger_strong", &mut self.danger_strong),
("code_text", &mut self.code_text),
("code_wash", &mut self.code_wash),
("diff_add", &mut self.diff_add),
("diff_del", &mut self.diff_del),
("diff_hunk_bg", &mut self.diff_hunk_bg),
("vibrancy_tone", &mut self.vibrancy_tone),
("syntax.comment", &mut syntax.comment),
("syntax.keyword", &mut syntax.keyword),
("syntax.string", &mut syntax.string),
("syntax.string_special", &mut syntax.string_special),
("syntax.escape", &mut syntax.escape),
("syntax.number", &mut syntax.number),
("syntax.boolean", &mut syntax.boolean),
("syntax.type_name", &mut syntax.type_name),
("syntax.type_builtin", &mut syntax.type_builtin),
("syntax.constructor", &mut syntax.constructor),
("syntax.function", &mut syntax.function),
("syntax.function_builtin", &mut syntax.function_builtin),
("syntax.macro_name", &mut syntax.macro_name),
("syntax.property", &mut syntax.property),
("syntax.constant", &mut syntax.constant),
("syntax.variable", &mut syntax.variable),
("syntax.variable_special", &mut syntax.variable_special),
("syntax.parameter", &mut syntax.parameter),
("syntax.operator", &mut syntax.operator),
("syntax.punctuation", &mut syntax.punctuation),
("syntax.tag", &mut syntax.tag),
("syntax.attribute", &mut syntax.attribute),
("syntax.label", &mut syntax.label),
("syntax.invalid", &mut syntax.invalid),
("terminal.bg", &mut self.terminal_bg),
("terminal.ansi.black", black),
("terminal.ansi.red", red),
("terminal.ansi.green", green),
("terminal.ansi.yellow", yellow),
("terminal.ansi.blue", blue),
("terminal.ansi.magenta", magenta),
("terminal.ansi.cyan", cyan),
("terminal.ansi.white", white),
("terminal.ansi.bright_black", bright_black),
("terminal.ansi.bright_red", bright_red),
("terminal.ansi.bright_green", bright_green),
("terminal.ansi.bright_yellow", bright_yellow),
("terminal.ansi.bright_blue", bright_blue),
("terminal.ansi.bright_magenta", bright_magenta),
("terminal.ansi.bright_cyan", bright_cyan),
("terminal.ansi.bright_white", bright_white),
]
}
}