//! 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. Also the Themes page. use std::{rc::Rc, sync::LazyLock}; use gpui_kit::component::{h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::*; use crate::settings::{self, ThemeMode}; use crate::ui::metal::{self, BONE, GRAPHITE, Palette, hex, shadows, vertical}; use crate::ui::settings::{radio, row, section}; /// 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, Vec) { 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 { 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> = 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); } // ---- the Themes page ------------------------------------------------------------- const MODES: &[(ThemeMode, &str)] = &[(ThemeMode::Light, "Light"), (ThemeMode::Dark, "Dark"), (ThemeMode::System, "System")]; /// Repaints the settings view after a change. pub type Changed = Rc; /// The Themes settings page: the mode, then a card per theme, dark and light. pub fn page(cx: &App, changed: Changed) -> Vec { let p = metal::palette(cx); let (mode, light, dark) = settings::get(|settings| (settings.theme_mode, settings.light_theme.clone(), settings.dark_theme.clone())); let on_mode = changed.clone(); let mode = section( "Theme", None, vec![row( "Mode", "Light and Dark keep to the themes picked below. System switches between them with the desktop.", metal::segmented("theme-mode", MODES, mode, p, move |mode, window, cx| { set_mode(mode, window, cx); on_mode(window, cx); }), p, )], p, ); let dark_themes = section( "Dark themes", Some("Shown in dark mode, and in System mode while the desktop is dark."), vec![cards(find(&dark, true), p, &changed)], p, ); let light_themes = section( "Light themes", Some("Shown in light mode, and in System mode while the desktop is light."), vec![cards(find(&light, false), p, &changed)], p, ); vec![mode, dark_themes, light_themes] } /// A grid of the themes of `chosen`'s kind, `chosen` ringed. fn cards(chosen: &'static Palette, p: &'static Palette, changed: &Changed) -> AnyElement { let themes = all().iter().filter(|theme| theme.palette.dark == chosen.dark); div() .grid() .grid_cols(3) .gap(px(16.)) .p(px(18.)) .children(themes.map(|theme| { let selected = std::ptr::eq(theme.palette, chosen); let shown = std::ptr::eq(theme.palette, p); let ring = if selected { BoxShadow::new(px(0.), px(0.), p.green_deep).spread_radius(px(2.)) } else { BoxShadow::new(px(0.), px(0.), p.rule).spread_radius(px(1.)) }; let changed = changed.clone(); v_flex() .id(SharedString::from(format!("theme-{}", theme.id))) .min_w_0() .gap(px(10.)) .cursor_pointer() .on_click(move |_, window, cx| { pick(theme, window, cx); changed(window, cx); }) .child(div().h(px(96.)).rounded(px(14.)).shadow(vec![ring]).child(preview(theme.palette))) .child( h_flex() .gap(px(8.)) .child(radio(selected, p)) .child( div() .flex_1() .min_w_0() .truncate() .text_size(px(13.5)) .font_weight(FontWeight::MEDIUM) .text_color(p.ink) .child(theme.name), ) .when(shown, |this| this.child(metal::tag("On", p))), ) })) .into_any_element() } /// 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 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)); } }