use gpui_kit::{Hsla, Rgba, rgb};
use super::{Look, look};
pub mod neutrals {
pub const CHROME: (u32, u32) = (0x222222, 0xe4e4e4);
pub const PANEL: (u32, u32) = (0x2a2a2a, 0xf2f2f2);
pub const RAISED: (u32, u32) = (0x313131, 0xfafafa);
pub const CONTROL: (u32, u32) = (0x3a3a3a, 0xe0e0e0);
pub const HOVER: (u32, u32) = (0x454545, 0xd4d4d4);
pub const BORDER: (u32, u32) = (0x171717, 0xc6c6c6);
pub const TEXT: (u32, u32) = (0xe6e6e6, 0x1c1c1c);
pub const TEXT_MUTED: (u32, u32) = (0xb8b8b8, 0x555555);
pub const TEXT_DISABLED: (u32, u32) = (0x6e6e6e, 0x9a9a9a);
pub const DANGER: (u32, u32) = (0xef6b6b, 0xb3261e);
pub const SUCCESS: (u32, u32) = (0x4caf7a, 0x1e7a46);
pub const WARNING: (u32, u32) = (0xe0a030, 0x8a5a00);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Colors {
pub chrome: Hsla,
pub panel: Hsla,
pub raised: Hsla,
pub control: Hsla,
pub hover: Hsla,
pub border: Hsla,
pub text: Hsla,
pub text_muted: Hsla,
pub text_disabled: Hsla,
pub accent: Hsla,
pub accent_hover: Hsla,
pub on_accent: Hsla,
pub accent_mark: Hsla,
pub danger: Hsla,
pub success: Hsla,
pub warning: Hsla,
pub diff_added: Hsla,
pub diff_removed: Hsla,
}
pub const DIFF_TINT: f32 = 0.16;
impl Colors {
pub fn resolve(look: &Look) -> Colors {
let dark = look.dark;
let n = |pair: (u32, u32)| rgb_hsla(if dark { pair.0 } else { pair.1 });
let a = look.active_palette().accent(dark);
Colors {
chrome: n(neutrals::CHROME),
panel: n(neutrals::PANEL),
raised: n(neutrals::RAISED),
control: n(neutrals::CONTROL),
hover: n(neutrals::HOVER),
border: n(neutrals::BORDER),
text: n(neutrals::TEXT),
text_muted: n(neutrals::TEXT_MUTED),
text_disabled: n(neutrals::TEXT_DISABLED),
accent: rgb_hsla(a.fill),
accent_hover: rgb_hsla(a.fill_hover),
on_accent: rgb_hsla(a.on_fill),
accent_mark: rgb_hsla(a.mark),
danger: n(neutrals::DANGER),
success: n(neutrals::SUCCESS),
warning: n(neutrals::WARNING),
diff_added: mix(n(neutrals::PANEL), n(neutrals::SUCCESS), DIFF_TINT),
diff_removed: mix(n(neutrals::PANEL), n(neutrals::DANGER), DIFF_TINT),
}
}
}
pub fn colors() -> Colors {
Colors::resolve(&look())
}
pub fn chrome() -> Hsla {
colors().chrome
}
pub fn panel() -> Hsla {
colors().panel
}
pub fn raised() -> Hsla {
colors().raised
}
pub fn control() -> Hsla {
colors().control
}
pub fn hover() -> Hsla {
colors().hover
}
pub fn border() -> Hsla {
colors().border
}
pub fn text() -> Hsla {
colors().text
}
pub fn text_muted() -> Hsla {
colors().text_muted
}
pub fn text_disabled() -> Hsla {
colors().text_disabled
}
pub fn accent() -> Hsla {
colors().accent
}
pub fn accent_hover() -> Hsla {
colors().accent_hover
}
pub fn on_accent() -> Hsla {
colors().on_accent
}
pub fn accent_mark() -> Hsla {
colors().accent_mark
}
pub fn danger() -> Hsla {
colors().danger
}
pub fn success() -> Hsla {
colors().success
}
pub fn warning() -> Hsla {
colors().warning
}
pub fn diff_added() -> Hsla {
colors().diff_added
}
pub fn diff_removed() -> Hsla {
colors().diff_removed
}
pub fn mix(a: Hsla, b: Hsla, t: f32) -> Hsla {
let (a, b) = (a.to_rgb(), b.to_rgb());
let m = |x: f32, y: f32| x + (y - x) * t;
Rgba { r: m(a.r, b.r), g: m(a.g, b.g), b: m(a.b, b.b), a: 1.0 }.into()
}
pub fn rgb_hsla(c: u32) -> Hsla {
rgb(c).into()
}
pub fn rgb8(c: [u8; 3]) -> Hsla {
rgb_hsla(((c[0] as u32) << 16) | ((c[1] as u32) << 8) | c[2] as u32)
}
pub fn luminance(c: Hsla) -> f32 {
let Rgba { r, g, b, .. } = c.to_rgb();
let lin = |v: f32| if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) };
0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
}
pub fn contrast(a: Hsla, b: Hsla) -> f32 {
let (la, lb) = (luminance(a), luminance(b));
(la.max(lb) + 0.05) / (la.min(lb) + 0.05)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::Palette;
use crate::theme::Rgb;
fn looks() -> Vec<Look> {
let mut accents: Vec<Option<Rgb>> = vec![None];
accents.extend(Rgb::SUGGESTED.map(Some));
accents.extend([0xfffacd, 0x0a1030, 0xff0000, 0xffffff, 0x000000].map(|c| Some(Rgb(c))));
let mut out = Vec::new();
for p in Palette::APPS {
for a in &accents {
for dark in [true, false] {
out.push(Look::new(p).with_accent(*a).with_dark(dark));
}
}
}
out
}
#[test]
fn neutrals_are_plain_greys() {
use neutrals::*;
for (name, pair) in [("chrome", CHROME), ("panel", PANEL), ("raised", RAISED), ("control", CONTROL), ("hover", HOVER), ("border", BORDER), ("text", TEXT), ("text_muted", TEXT_MUTED), ("text_disabled", TEXT_DISABLED)] {
for c in [pair.0, pair.1] {
let (r, g, b) = (c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff);
assert!(r == g && g == b, "{name}: #{c:06x} is tinted");
}
}
}
#[test]
fn text_passes_aa_on_every_surface() {
for l in looks() {
let c = Colors::resolve(&l);
for (sname, s) in [("chrome", c.chrome), ("panel", c.panel), ("raised", c.raised), ("control", c.control), ("hover", c.hover)] {
for (tname, t) in [("text", c.text), ("text_muted", c.text_muted)] {
let r = contrast(t, s);
assert!(r >= 4.5, "{} dark={}: {tname} on {sname} is {r:.2}:1", l.palette.name, l.dark);
}
}
for (name, t) in [("danger", c.danger), ("success", c.success), ("warning", c.warning)] {
let r = contrast(t, c.panel);
assert!(r >= 4.5, "dark={}: {name} on panel is {r:.2}:1", l.dark);
}
}
}
#[test]
fn accents_pass_contrast() {
for l in looks() {
let c = Colors::resolve(&l);
let name = format!("{} {:?} dark={}", l.palette.name, l.accent, l.dark);
for (fname, f) in [("fill", c.accent), ("fill_hover", c.accent_hover)] {
let r = contrast(c.on_accent, f);
assert!(r >= 4.5, "{name}: text on {fname} is {r:.2}:1");
}
for (sname, s, min) in [("panel", c.panel, 4.5), ("raised", c.raised, 4.5), ("control", c.control, 3.0), ("hover", c.hover, 3.0)] {
let r = contrast(c.accent_mark, s);
assert!(r >= min, "{name}: mark on {sname} is {r:.2}:1");
}
}
}
#[test]
fn diff_lines_pass_contrast() {
for dark in [true, false] {
let c = Colors::resolve(&Look::new(Palette::LIME).with_dark(dark));
for (bname, bg, mark) in [("added", c.diff_added, c.success), ("removed", c.diff_removed, c.danger)] {
for (tname, t) in [("text", c.text), ("text_muted", c.text_muted)] {
let r = contrast(t, bg);
assert!(r >= 4.5, "dark={dark}: {tname} on the {bname} line is {r:.2}:1");
}
let r = contrast(mark, bg);
assert!(r >= 3.0, "dark={dark}: the sign on the {bname} line is {r:.2}:1");
assert!(bg != c.panel, "dark={dark}: the {bname} line is not tinted");
}
}
}
#[test]
fn surfaces_step_apart() {
for dark in [true, false] {
let c = Colors::resolve(&Look { dark, ..Look::new(Palette::LIME) });
let order = [c.chrome, c.panel, c.raised];
let l: Vec<f32> = order.iter().map(|c| luminance(*c)).collect();
assert!(l[0] < l[1] && l[1] < l[2], "dark={dark}: chrome < panel < raised in lightness");
}
}
#[test]
fn contrast_of_known_pairs() {
assert!((contrast(rgb_hsla(0x000000), rgb_hsla(0xffffff)) - 21.0).abs() < 0.01);
let r = contrast(rgb_hsla(0x767676), rgb_hsla(0xffffff));
assert!((r - 4.54).abs() < 0.01, "{r}");
let r = contrast(rgb_hsla(0x777777), rgb_hsla(0xffffff));
assert!((r - 4.48).abs() < 0.01, "{r}");
assert_eq!(rgb8([0x12, 0x34, 0x56]), rgb_hsla(0x123456));
}
}