//! Themes: MetalUI's Bone and Graphite, plus popular color schemes (Tokyo
//! Night, Catppuccin, GitHub and more) rebuilt as MetalUI palettes, so caps
//! still rise over sunk wells in every one of them, grouped into the families
//! the Appearance page shows as cards.
use std::sync::LazyLock;
use gpui_kit::component::{h_flex, v_flex};
use gpui_kit::*;
use crate::settings::{self, ThemeMode};
use crate::ui::metal::{self, BONE, GRAPHITE, Palette, hex, shadows, vertical};
/// A published color scheme, by the few colors a palette is built from.
pub struct Scheme {
pub id: &'static str,
pub name: &'static str,
pub dark: bool,
/// The editor background, which the sheet takes.
pub bg: &'static str,
/// The panels around the editor, which the page takes.
pub panel: &'static str,
pub fg: &'static str,
/// Secondary text: comments, descriptions.
pub muted: &'static str,
/// Primary buttons.
pub accent: &'static str,
pub green: &'static str,
pub red: &'static str,
pub yellow: &'static str,
pub blue: &'static str,
/// Orange, pink, teal, magenta, blue and violet, standing in for MetalUI's
/// ember, blush, tide, spark, dusk and iris.
pub tints: [&'static str; 6],
}
/// The schemes, from their published palettes. Dark ones first, then light,
/// in the order the Themes page shows them.
pub const SCHEMES: &[Scheme] = &[
Scheme {
id: "tokyo-night",
name: "Tokyo Night",
dark: true,
bg: "#1a1b26",
panel: "#16161e",
fg: "#c0caf5",
muted: "#737aa2",
accent: "#7aa2f7",
green: "#9ece6a",
red: "#f7768e",
yellow: "#e0af68",
blue: "#7aa2f7",
tints: ["#ff9e64", "#f7768e", "#73daca", "#bb9af7", "#7aa2f7", "#9d7cd8"],
},
Scheme {
id: "tokyo-night-storm",
name: "Tokyo Night Storm",
dark: true,
bg: "#24283b",
panel: "#1f2335",
fg: "#c0caf5",
muted: "#737aa2",
accent: "#7aa2f7",
green: "#9ece6a",
red: "#f7768e",
yellow: "#e0af68",
blue: "#7aa2f7",
tints: ["#ff9e64", "#f7768e", "#73daca", "#bb9af7", "#7aa2f7", "#9d7cd8"],
},
Scheme {
id: "catppuccin-mocha",
name: "Catppuccin Mocha",
dark: true,
bg: "#1e1e2e",
panel: "#181825",
fg: "#cdd6f4",
muted: "#7f849c",
accent: "#cba6f7",
green: "#a6e3a1",
red: "#f38ba8",
yellow: "#f9e2af",
blue: "#89b4fa",
tints: ["#fab387", "#f5c2e7", "#94e2d5", "#cba6f7", "#89b4fa", "#b4befe"],
},
Scheme {
id: "catppuccin-macchiato",
name: "Catppuccin Macchiato",
dark: true,
bg: "#24273a",
panel: "#1e2030",
fg: "#cad3f5",
muted: "#8087a2",
accent: "#c6a0f6",
green: "#a6da95",
red: "#ed8796",
yellow: "#eed49f",
blue: "#8aadf4",
tints: ["#f5a97f", "#f5bde6", "#8bd5ca", "#c6a0f6", "#8aadf4", "#b7bdf8"],
},
Scheme {
id: "catppuccin-frappe",
name: "Catppuccin Frappé",
dark: true,
bg: "#303446",
panel: "#292c3c",
fg: "#c6d0f5",
muted: "#838ba7",
accent: "#ca9ee6",
green: "#a6d189",
red: "#e78284",
yellow: "#e5c890",
blue: "#8caaee",
tints: ["#ef9f76", "#f4b8e4", "#81c8be", "#ca9ee6", "#8caaee", "#babbf1"],
},
Scheme {
id: "github-dark",
name: "GitHub Dark",
dark: true,
bg: "#0d1117",
panel: "#010409",
fg: "#e6edf3",
muted: "#7d8590",
accent: "#238636",
green: "#3fb950",
red: "#f85149",
yellow: "#d29922",
blue: "#4493f8",
tints: ["#ffa657", "#db61a2", "#a5d6ff", "#d2a8ff", "#79c0ff", "#a371f7"],
},
Scheme {
id: "github-dark-dimmed",
name: "GitHub Dark Dimmed",
dark: true,
bg: "#22272e",
panel: "#1c2128",
fg: "#adbac7",
muted: "#768390",
accent: "#347d39",
green: "#57ab5a",
red: "#e5534b",
yellow: "#c69026",
blue: "#539bf5",
tints: ["#f69d50", "#c96198", "#96d0ff", "#dcbdfb", "#6cb6ff", "#986ee2"],
},
Scheme {
id: "one-dark",
name: "One Dark",
dark: true,
bg: "#282c34",
panel: "#21252b",
fg: "#abb2bf",
muted: "#7f848e",
accent: "#61afef",
green: "#98c379",
red: "#e06c75",
yellow: "#e5c07b",
blue: "#61afef",
tints: ["#d19a66", "#e06c75", "#56b6c2", "#c678dd", "#61afef", "#e5c07b"],
},
Scheme {
id: "nord",
name: "Nord",
dark: true,
bg: "#3b4252",
panel: "#2e3440",
fg: "#eceff4",
muted: "#616e88",
accent: "#88c0d0",
green: "#a3be8c",
red: "#bf616a",
yellow: "#ebcb8b",
blue: "#81a1c1",
tints: ["#d08770", "#bf616a", "#8fbcbb", "#b48ead", "#81a1c1", "#5e81ac"],
},
Scheme {
id: "gruvbox-dark",
name: "Gruvbox Dark",
dark: true,
bg: "#282828",
panel: "#1d2021",
fg: "#ebdbb2",
muted: "#a89984",
accent: "#fe8019",
green: "#b8bb26",
red: "#fb4934",
yellow: "#fabd2f",
blue: "#83a598",
tints: ["#fe8019", "#fb4934", "#8ec07c", "#d3869b", "#83a598", "#fabd2f"],
},
Scheme {
id: "rose-pine",
name: "Rosé Pine",
dark: true,
bg: "#1f1d2e",
panel: "#191724",
fg: "#e0def4",
muted: "#908caa",
accent: "#ebbcba",
green: "#9ccfd8",
red: "#eb6f92",
yellow: "#f6c177",
blue: "#c4a7e7",
tints: ["#ebbcba", "#eb6f92", "#9ccfd8", "#c4a7e7", "#31748f", "#f6c177"],
},
Scheme {
id: "rose-pine-moon",
name: "Rosé Pine Moon",
dark: true,
bg: "#2a273f",
panel: "#232136",
fg: "#e0def4",
muted: "#908caa",
accent: "#ea9a97",
green: "#9ccfd8",
red: "#eb6f92",
yellow: "#f6c177",
blue: "#c4a7e7",
tints: ["#ea9a97", "#eb6f92", "#9ccfd8", "#c4a7e7", "#3e8fb0", "#f6c177"],
},
Scheme {
id: "solarized-dark",
name: "Solarized Dark",
dark: true,
bg: "#002b36",
panel: "#00212b",
fg: "#93a1a1",
muted: "#586e75",
accent: "#268bd2",
green: "#859900",
red: "#dc322f",
yellow: "#b58900",
blue: "#268bd2",
tints: ["#cb4b16", "#d33682", "#2aa198", "#6c71c4", "#268bd2", "#b58900"],
},
Scheme {
id: "dracula",
name: "Dracula",
dark: true,
bg: "#282a36",
panel: "#21222c",
fg: "#f8f8f2",
muted: "#6272a4",
accent: "#bd93f9",
green: "#50fa7b",
red: "#ff5555",
yellow: "#f1fa8c",
blue: "#8be9fd",
tints: ["#ffb86c", "#ff79c6", "#8be9fd", "#bd93f9", "#50fa7b", "#f1fa8c"],
},
Scheme {
id: "tokyo-night-day",
name: "Tokyo Night Day",
dark: false,
bg: "#e1e2e7",
panel: "#d0d5e3",
fg: "#3760bf",
muted: "#6172b0",
accent: "#2e7de9",
green: "#587539",
red: "#f52a65",
yellow: "#8c6c3e",
blue: "#2e7de9",
tints: ["#b15c00", "#f52a65", "#118c74", "#9854f1", "#2e7de9", "#7847bd"],
},
Scheme {
id: "catppuccin-latte",
name: "Catppuccin Latte",
dark: false,
bg: "#eff1f5",
panel: "#e6e9ef",
fg: "#4c4f69",
muted: "#8c8fa1",
accent: "#8839ef",
green: "#40a02b",
red: "#d20f39",
yellow: "#df8e1d",
blue: "#1e66f5",
tints: ["#fe640b", "#ea76cb", "#179299", "#8839ef", "#1e66f5", "#7287fd"],
},
Scheme {
id: "github-light",
name: "GitHub Light",
dark: false,
bg: "#ffffff",
panel: "#f6f8fa",
fg: "#1f2328",
muted: "#656d76",
accent: "#1f883d",
green: "#1a7f37",
red: "#d1242f",
yellow: "#9a6700",
blue: "#0969da",
tints: ["#bc4c00", "#bf3989", "#1b7c83", "#8250df", "#0550ae", "#6639ba"],
},
Scheme {
id: "one-light",
name: "One Light",
dark: false,
bg: "#fafafa",
panel: "#eaeaeb",
fg: "#383a42",
muted: "#a0a1a7",
accent: "#4078f2",
green: "#50a14f",
red: "#e45649",
yellow: "#c18401",
blue: "#4078f2",
tints: ["#986801", "#e45649", "#0184bc", "#a626a4", "#4078f2", "#c18401"],
},
Scheme {
id: "gruvbox-light",
name: "Gruvbox Light",
dark: false,
bg: "#fbf1c7",
panel: "#f2e5bc",
fg: "#3c3836",
muted: "#7c6f64",
accent: "#af3a03",
green: "#79740e",
red: "#9d0006",
yellow: "#b57614",
blue: "#076678",
tints: ["#af3a03", "#9d0006", "#427b58", "#8f3f71", "#076678", "#b57614"],
},
Scheme {
id: "rose-pine-dawn",
name: "Rosé Pine Dawn",
dark: false,
bg: "#fffaf3",
panel: "#faf4ed",
fg: "#575279",
muted: "#797593",
accent: "#d7827e",
green: "#56949f",
red: "#b4637a",
yellow: "#ea9d34",
blue: "#907aa9",
tints: ["#d7827e", "#b4637a", "#56949f", "#907aa9", "#286983", "#ea9d34"],
},
Scheme {
id: "solarized-light",
name: "Solarized Light",
dark: false,
bg: "#fdf6e3",
panel: "#eee8d5",
fg: "#586e75",
muted: "#93a1a1",
accent: "#268bd2",
green: "#859900",
red: "#dc322f",
yellow: "#b58900",
blue: "#268bd2",
tints: ["#cb4b16", "#d33682", "#2aa198", "#6c71c4", "#268bd2", "#b58900"],
},
];
impl Palette {
/// Builds a full MetalUI palette from a scheme. Every surface steps off
/// the scheme's background the way Graphite's (dark) or Bone's (light)
/// step off theirs, inks are nudged until they read on those surfaces, and
/// the shadow recipes are Graphite's, or Bone's tinted to the scheme's hue.
pub fn from_scheme(scheme: &Scheme) -> Palette {
let dark = scheme.dark;
let bg = hex(scheme.bg);
let page = hex(scheme.panel);
let (s_hi, s_lo, well, cap, pressed, row_on, kbd) = if dark {
(
lighten(bg, 0.029),
dim(bg, 0.87),
(dim(bg, 0.54), dim(bg, 0.66)),
(lighten(bg, 0.049), lighten(bg, 0.014)),
(dim(bg, 0.66), dim(bg, 0.77)),
(lighten(bg, 0.059), lighten(bg, 0.021)),
(lighten(bg, 0.07), lighten(bg, 0.021)),
)
} else {
(
lighten(bg, 0.023),
lighten(bg, -0.026),
(lighten(bg, -0.067), lighten(bg, -0.039)),
(lighten(bg, 0.06), lighten(bg, -0.008)),
(lighten(bg, -0.057), lighten(bg, -0.029)),
(lighten(bg, 0.06), bg),
(lighten(bg, 0.06), lighten(bg, -0.015)),
)
};
// Inks get lighter on dark themes and darker on light ones until they
// read on everything text sits on.
let behind = [page, s_hi, bg, row_on.0, well.0];
let ink = legible(hex(scheme.fg), &behind, 4.5, dark);
let ink3 = legible(hex(scheme.muted), &behind, 3.5, dark);
let ink2 = legible(mix(ink, ink3, 0.5), &behind, 4.5, dark);
let text = |color: &str, ratio: f32| legible(hex(color), &[s_hi, bg], ratio, dark);
let accent = hex(scheme.accent);
let primary_ink = [white(), if dark { page } else { ink }]
.into_iter()
.max_by(|a, b| contrast(*a, accent).total_cmp(&contrast(*b, accent)))
.unwrap_or(white());
// Danger caps carry white text.
let danger = legible(hex(scheme.red), &[white()], 3.5, false);
let (primary_sh, primary_pressed_sh) = colored_cap(accent, dark);
let (danger_sh, danger_pressed_sh) = colored_cap(danger, dark);
// Bone's shadows are warm browns; these take the scheme's hue instead.
// Graphite's are black and white, so tinting leaves them be.
let tone = hsla(bg.h, bg.s.min(0.25), 0.14, 1.);
let metal: &Palette = if dark { &GRAPHITE } else { &BONE };
Palette {
dark,
page,
s_hi,
s: bg,
s_lo,
well_top: well.0,
well_bot: well.1,
ink,
ink2,
ink3,
icon: mix(ink, ink2, 0.4),
engrave: ink.alpha(if dark { 0.5 } else { 0.65 }),
tag_ring: ink.alpha(if dark { 0.16 } else { 0.2 }),
rule: black().alpha(if dark { 0.45 } else { 0.075 }),
// A near-black sheet swallows a dark rule, so its lip carries it.
rule_lip: white().alpha(if dark { 0.035 + (0.14 - bg.l).max(0.) * 0.5 } else { 0.8 }),
row_hover: white().alpha(if dark { 0.06 } else { 0.7 }),
menu_bg: if dark { mix(bg, s_lo, 0.6) } else { lighten(bg, 0.015) }.alpha(0.98),
menu_row_hover: if dark { white().alpha(0.06) } else { black().alpha(0.05) },
field_ink: mix(ink3, bg, 0.2),
cap,
pressed,
row_on,
kbd,
primary: (lighten(accent, 0.04), lighten(accent, -0.04)),
primary_pressed: (lighten(accent, -0.1), lighten(accent, -0.06)),
primary_ink,
danger: (lighten(danger, 0.04), lighten(danger, -0.04)),
danger_pressed: (lighten(danger, -0.1), lighten(danger, -0.045)),
region: if dark {
(white().alpha(0.02), white().alpha(0.012))
} else {
(tone.alpha(0.028), tone.alpha(0.012))
},
green_deep: hex(scheme.green),
green_ink: text(scheme.green, 4.5),
red: text(scheme.red, 4.),
amber: hex(scheme.yellow),
amber_ink: text(scheme.yellow, 4.5),
blue_ink: text(scheme.blue, 4.5),
led_off: if dark {
(mix(ink, bg, 0.45), mix(ink, bg, 0.8))
} else {
(lighten(bg, -0.13), lighten(bg, -0.18))
},
tints: scheme.tints.map(|tint| text(tint, 3.)),
raise: retint(&metal.raise, tone),
raise_lite: retint(&metal.raise_lite, tone),
raise_sm: retint(&metal.raise_sm, tone),
btn: retint(&metal.btn, tone),
pressed_sh: retint(&metal.pressed_sh, tone),
well: retint(&metal.well, tone),
region_sh: retint(&metal.region_sh, tone),
primary_sh,
primary_pressed_sh,
danger_sh,
danger_pressed_sh,
led_off_sh: metal.led_off_sh.clone(),
}
}
}
/// The raised and pressed shadows of a cap in a color of its own. On light
/// themes they're MetalUI's danger recipe, shaded in the cap's hue; on dark
/// ones the page is darker than any shade of it, so they fall black.
fn colored_cap(color: Hsla, dark: bool) -> (Vec<BoxShadow>, Vec<BoxShadow>) {
let shade = |l: f32, a: f32| css(hsla(color.h, color.s, l, a));
let (edge, near, far, deep, ring) = if dark {
let black = |a: f32| css(black().alpha(a));
(black(0.6), black(0.35), black(0.45), black(0.35), black(0.6))
} else {
(shade(0.25, 0.3), shade(0.22, 0.14), shade(0.37, 0.24), shade(0.16, 0.45), shade(0.22, 0.55))
};
(
shadows(&format!(
"inset 0 0 6px 1px #ffffff24, inset 1px 2px 2px -1px #ffffff52, 0 0 0 .5px {edge}, 0 1px 2px {near}, \
0 5px 12px -4px {far}"
)),
shadows(&format!("inset 0 1px 3px {deep}, 0 0 0 .5px {ring}")),
)
}
/// Turns a recipe's tinted shadows to `tone`'s hue and saturation, keeping
/// their lightness and alpha. Black and white layers stay as they are.
fn retint(layers: &[BoxShadow], tone: Hsla) -> Vec<BoxShadow> {
layers
.iter()
.map(|layer| {
let mut layer = layer.clone();
if layer.color.s > 0. {
layer.color = hsla(tone.h, tone.s, layer.color.l, layer.color.a);
}
layer
})
.collect()
}
// ---- color math ----------------------------------------------------------------
/// Shifts lightness by `by`, darker when negative.
fn lighten(color: Hsla, by: f32) -> Hsla {
Hsla { l: (color.l + by).clamp(0., 1.), ..color }
}
/// Scales lightness, for darkening an already dark color in proportion.
fn dim(color: Hsla, factor: f32) -> Hsla {
Hsla { l: color.l * factor, ..color }
}
/// Blends `a` toward `b` in RGB.
fn mix(a: Hsla, b: Hsla, t: f32) -> Hsla {
let (a, b) = (a.to_rgb(), b.to_rgb());
let lerp = |x: f32, y: f32| x + (y - x) * t;
Rgba { r: lerp(a.r, b.r), g: lerp(a.g, b.g), b: lerp(a.b, b.b), a: lerp(a.a, b.a) }.into()
}
/// `#rrggbbaa`, as `metal::shadows` reads it.
fn css(color: Hsla) -> String {
let c = color.to_rgb();
let byte = |v: f32| (v.clamp(0., 1.) * 255.).round() as u8;
format!("#{:02x}{:02x}{:02x}{:02x}", byte(c.r), byte(c.g), byte(c.b), byte(c.a))
}
/// WCAG relative luminance.
fn luminance(color: Hsla) -> f32 {
let c = color.to_rgb();
let linear = |v: f32| if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) };
0.2126 * linear(c.r) + 0.7152 * linear(c.g) + 0.0722 * linear(c.b)
}
/// WCAG contrast ratio, 1 to 21.
pub fn contrast(a: Hsla, b: Hsla) -> f32 {
let (a, b) = (luminance(a), luminance(b));
(a.max(b) + 0.05) / (a.min(b) + 0.05)
}
/// Moves `color` lighter (or darker) until it reads at `ratio` on every one
/// of `surfaces`.
fn legible(mut color: Hsla, surfaces: &[Hsla], ratio: f32, lighter: bool) -> Hsla {
for _ in 0..100 {
if surfaces.iter().all(|surface| contrast(color, *surface) >= ratio) {
break;
}
color = lighten(color, if lighter { 0.01 } else { -0.01 });
}
color
}
// ---- the catalog -----------------------------------------------------------------
/// A theme on the Themes page.
pub struct Theme {
pub id: &'static str,
pub name: &'static str,
pub palette: &'static Palette,
}
static THEMES: LazyLock<Vec<Theme>> = LazyLock::new(|| {
let metal = [("graphite", "Graphite", &*GRAPHITE), ("bone", "Bone", &*BONE)];
let schemes = SCHEMES.iter().map(|scheme| {
let palette: &'static Palette = Box::leak(Box::new(Palette::from_scheme(scheme)));
(scheme.id, scheme.name, palette)
});
metal.into_iter().chain(schemes).map(|(id, name, palette)| Theme { id, name, palette }).collect()
});
/// Every theme, Bone and Graphite first.
pub fn all() -> &'static [Theme] {
&THEMES
}
/// The palette of the theme with this id, if it's of the right kind, or
/// Graphite or Bone.
pub fn find(id: &str, dark: bool) -> &'static Palette {
all()
.iter()
.find(|theme| theme.id == id && theme.palette.dark == dark)
.map_or(if dark { &*GRAPHITE } else { &*BONE }, |theme| theme.palette)
}
/// The palette the settings pick: the dark or light theme, by mode.
pub fn chosen(system_dark: bool) -> &'static Palette {
let (mode, light, dark) =
settings::get(|settings| (settings.theme_mode, settings.light_theme.clone(), settings.dark_theme.clone()));
match mode {
ThemeMode::Light => find(&light, false),
ThemeMode::Dark => find(&dark, true),
ThemeMode::System if system_dark => find(&dark, true),
ThemeMode::System => find(&light, false),
}
}
/// Switches between light, dark and the desktop's choice, and applies it.
pub fn set_mode(mode: ThemeMode, window: &Window, cx: &mut App) {
settings::update(|settings| settings.theme_mode = mode);
metal::sync_appearance(window, cx);
}
/// Makes `theme` the light or dark theme, switching to its mode unless the
/// mode follows the desktop, and applies it.
pub fn pick(theme: &Theme, window: &Window, cx: &mut App) {
let (id, dark) = (theme.id.to_string(), theme.palette.dark);
settings::update(|settings| {
if dark {
settings.dark_theme = id;
} else {
settings.light_theme = id;
}
if settings.theme_mode != ThemeMode::System {
settings.theme_mode = if dark { ThemeMode::Dark } else { ThemeMode::Light };
}
});
metal::sync_appearance(window, cx);
}
/// Themes from one color scheme, shown as one card with a swatch per
/// variant, light ones first.
pub struct Family {
pub name: &'static str,
/// Theme ids.
pub themes: &'static [&'static str],
}
pub const FAMILIES: &[Family] = &[
Family { name: "Vectrum", themes: &["bone", "graphite"] },
Family { name: "Tokyo Night", themes: &["tokyo-night-day", "tokyo-night", "tokyo-night-storm"] },
Family { name: "Catppuccin", themes: &["catppuccin-latte", "catppuccin-mocha", "catppuccin-macchiato", "catppuccin-frappe"] },
Family { name: "GitHub", themes: &["github-light", "github-dark", "github-dark-dimmed"] },
Family { name: "One", themes: &["one-light", "one-dark"] },
Family { name: "Nord", themes: &["nord"] },
Family { name: "Gruvbox", themes: &["gruvbox-light", "gruvbox-dark"] },
Family { name: "Rosé Pine", themes: &["rose-pine-dawn", "rose-pine", "rose-pine-moon"] },
Family { name: "Solarized", themes: &["solarized-light", "solarized-dark"] },
Family { name: "Dracula", themes: &["dracula"] },
];
impl Family {
/// The family's themes, light ones first.
pub fn members(&self) -> impl Iterator<Item = &'static Theme> + '_ {
self.themes.iter().filter_map(|id| all().iter().find(|theme| theme.id == *id))
}
}
/// Makes `theme` the light or dark theme without touching the mode.
pub fn assign(theme: &Theme, window: &Window, cx: &mut App) {
let id = theme.id.to_string();
settings::update(|settings| {
if theme.palette.dark {
settings.dark_theme = id;
} else {
settings.light_theme = id;
}
});
metal::sync_appearance(window, cx);
}
/// A miniature of the app in a theme: the sheet on the table, a thread list
/// with one thread picked, a code well and a composer.
pub fn preview(c: &Palette) -> Div {
let line = |width: f32, color: Hsla| div().flex_none().h(px(4.)).w(px(width)).rounded_full().bg(color);
h_flex()
.size_full()
.overflow_hidden()
.rounded(px(14.))
.bg(c.page)
.p(px(8.))
.gap(px(8.))
.child(
v_flex()
.w(px(30.))
.gap(px(6.))
.child(
div()
.p(px(3.))
.rounded(px(4.))
.bg(vertical(c.row_on))
.shadow(c.raise_sm.clone())
.child(line(20., c.ink2)),
)
.child(div().px(px(3.)).child(line(16., c.ink3.opacity(0.7))))
.child(div().px(px(3.)).child(line(20., c.ink3.opacity(0.7)))),
)
.child(
v_flex()
.flex_1()
.h_full()
.p(px(8.))
.gap(px(5.))
.rounded(px(10.))
.bg(vertical((c.s_hi, c.s)))
.shadow(c.raise_sm.clone())
.child(line(40., c.ink.opacity(0.8)))
.child(line(56., c.ink3.opacity(0.7)))
.child(
v_flex()
.gap(px(3.))
.p(px(4.))
.rounded(px(5.))
.bg(vertical((c.well_top, c.well_bot)))
.shadow(c.well.clone())
.child(h_flex().gap(px(3.)).child(line(10., c.blue_ink)).child(line(18., c.green_ink)))
.child(h_flex().gap(px(3.)).child(line(14., c.tints[3])).child(line(10., c.amber_ink))),
)
.child(div().flex_1())
.child(
h_flex()
.h(px(16.))
.px(px(4.))
.rounded(px(6.))
.bg(vertical(c.row_on))
.shadow(c.raise_sm.clone())
.child(div().flex_1())
.child(div().size(px(8.)).rounded_full().bg(vertical(c.primary))),
),
)
}
#[cfg(test)]
mod tests {
use super::{SCHEMES, all, contrast, find, luminance};
use crate::ui::metal::{BONE, GRAPHITE};
use gpui_kit::white;
#[test]
fn every_theme_reads() {
for theme in all() {
let (id, p) = (theme.id, theme.palette);
for surface in [p.page, p.s_hi, p.s] {
assert!(contrast(p.ink, surface) >= 4.5, "{id}: ink on {surface:?}");
assert!(contrast(p.ink2, surface) >= 3., "{id}: ink2 on {surface:?}");
assert!(contrast(p.ink3, surface) >= 3., "{id}: ink3 on {surface:?}");
assert!(contrast(p.blue_ink, surface) >= 3., "{id}: blue ink on {surface:?}");
assert!(contrast(p.green_ink, surface) >= 3., "{id}: green ink on {surface:?}");
}
assert!(contrast(p.primary_ink, p.primary.1) >= 3., "{id}: primary cap");
assert!(contrast(white(), p.danger.1) >= 3., "{id}: danger cap");
assert!(luminance(p.page) < luminance(p.s_hi), "{id}: the sheet should stand off the page");
assert_eq!(p.dark, luminance(p.s) < 0.18, "{id}: dark or light");
}
}
#[test]
fn theme_ids_are_unique() {
let themes = all();
assert_eq!(themes.len(), SCHEMES.len() + 2);
for (ix, theme) in themes.iter().enumerate() {
assert!(themes[ix + 1..].iter().all(|other| other.id != theme.id), "{} twice", theme.id);
}
}
#[test]
fn every_theme_has_one_family() {
for theme in all() {
let families = super::FAMILIES.iter().filter(|family| family.themes.contains(&theme.id)).count();
assert_eq!(families, 1, "{} is in {families} families", theme.id);
}
for family in super::FAMILIES {
assert_eq!(family.members().count(), family.themes.len(), "{} names a missing theme", family.name);
}
}
#[test]
fn unknown_themes_fall_back() {
assert!(std::ptr::eq(find("graphite", true), &*GRAPHITE));
assert!(std::ptr::eq(find("no-such-theme", true), &*GRAPHITE));
assert!(std::ptr::eq(find("tokyo-night", false), &*BONE));
let latte = find("catppuccin-latte", false);
assert!(!latte.dark && !std::ptr::eq(latte, &*BONE));
}
}