1use gpui_kit::{Hsla, Rgba, rgb};
13
14use super::{Look, look};
15
16pub mod neutrals {
19 pub const CHROME: (u32, u32) = (0x222222, 0xe4e4e4);
21 pub const PANEL: (u32, u32) = (0x2a2a2a, 0xf2f2f2);
23 pub const RAISED: (u32, u32) = (0x313131, 0xfafafa);
25 pub const CONTROL: (u32, u32) = (0x3a3a3a, 0xe0e0e0);
27 pub const HOVER: (u32, u32) = (0x454545, 0xd4d4d4);
29 pub const BORDER: (u32, u32) = (0x171717, 0xc6c6c6);
31 pub const TEXT: (u32, u32) = (0xe6e6e6, 0x1c1c1c);
33 pub const TEXT_MUTED: (u32, u32) = (0xb8b8b8, 0x555555);
35 pub const TEXT_DISABLED: (u32, u32) = (0x6e6e6e, 0x9a9a9a);
37 pub const DANGER: (u32, u32) = (0xef6b6b, 0xb3261e);
39 pub const SUCCESS: (u32, u32) = (0x4caf7a, 0x1e7a46);
41 pub const WARNING: (u32, u32) = (0xe0a030, 0x8a5a00);
43}
44
45#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct Colors {
48 pub chrome: Hsla,
50 pub panel: Hsla,
52 pub raised: Hsla,
54 pub control: Hsla,
56 pub hover: Hsla,
58 pub border: Hsla,
60 pub text: Hsla,
62 pub text_muted: Hsla,
64 pub text_disabled: Hsla,
66 pub accent: Hsla,
68 pub accent_hover: Hsla,
70 pub on_accent: Hsla,
72 pub accent_mark: Hsla,
74 pub danger: Hsla,
76 pub success: Hsla,
78 pub warning: Hsla,
80 pub diff_added: Hsla,
82 pub diff_removed: Hsla,
84}
85
86pub const DIFF_TINT: f32 = 0.16;
88
89impl Colors {
90 pub fn resolve(look: &Look) -> Colors {
92 let dark = look.dark;
93 let n = |pair: (u32, u32)| rgb_hsla(if dark { pair.0 } else { pair.1 });
94 let a = look.active_palette().accent(dark);
95 Colors {
96 chrome: n(neutrals::CHROME),
97 panel: n(neutrals::PANEL),
98 raised: n(neutrals::RAISED),
99 control: n(neutrals::CONTROL),
100 hover: n(neutrals::HOVER),
101 border: n(neutrals::BORDER),
102 text: n(neutrals::TEXT),
103 text_muted: n(neutrals::TEXT_MUTED),
104 text_disabled: n(neutrals::TEXT_DISABLED),
105 accent: rgb_hsla(a.fill),
106 accent_hover: rgb_hsla(a.fill_hover),
107 on_accent: rgb_hsla(a.on_fill),
108 accent_mark: rgb_hsla(a.mark),
109 danger: n(neutrals::DANGER),
110 success: n(neutrals::SUCCESS),
111 warning: n(neutrals::WARNING),
112 diff_added: mix(n(neutrals::PANEL), n(neutrals::SUCCESS), DIFF_TINT),
113 diff_removed: mix(n(neutrals::PANEL), n(neutrals::DANGER), DIFF_TINT),
114 }
115 }
116}
117
118pub fn colors() -> Colors {
120 Colors::resolve(&look())
121}
122
123pub fn chrome() -> Hsla {
125 colors().chrome
126}
127pub fn panel() -> Hsla {
129 colors().panel
130}
131pub fn raised() -> Hsla {
133 colors().raised
134}
135pub fn control() -> Hsla {
137 colors().control
138}
139pub fn hover() -> Hsla {
141 colors().hover
142}
143pub fn border() -> Hsla {
145 colors().border
146}
147pub fn text() -> Hsla {
149 colors().text
150}
151pub fn text_muted() -> Hsla {
153 colors().text_muted
154}
155pub fn text_disabled() -> Hsla {
157 colors().text_disabled
158}
159pub fn accent() -> Hsla {
161 colors().accent
162}
163pub fn accent_hover() -> Hsla {
165 colors().accent_hover
166}
167pub fn on_accent() -> Hsla {
169 colors().on_accent
170}
171pub fn accent_mark() -> Hsla {
173 colors().accent_mark
174}
175pub fn danger() -> Hsla {
177 colors().danger
178}
179pub fn success() -> Hsla {
181 colors().success
182}
183pub fn warning() -> Hsla {
185 colors().warning
186}
187
188pub fn diff_added() -> Hsla {
190 colors().diff_added
191}
192pub fn diff_removed() -> Hsla {
194 colors().diff_removed
195}
196
197pub fn mix(a: Hsla, b: Hsla, t: f32) -> Hsla {
199 let (a, b) = (a.to_rgb(), b.to_rgb());
200 let m = |x: f32, y: f32| x + (y - x) * t;
201 Rgba { r: m(a.r, b.r), g: m(a.g, b.g), b: m(a.b, b.b), a: 1.0 }.into()
202}
203
204pub fn rgb_hsla(c: u32) -> Hsla {
206 rgb(c).into()
207}
208
209pub fn rgb8(c: [u8; 3]) -> Hsla {
211 rgb_hsla(((c[0] as u32) << 16) | ((c[1] as u32) << 8) | c[2] as u32)
212}
213
214pub fn luminance(c: Hsla) -> f32 {
216 let Rgba { r, g, b, .. } = c.to_rgb();
217 let lin = |v: f32| if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) };
218 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
219}
220
221pub fn contrast(a: Hsla, b: Hsla) -> f32 {
223 let (la, lb) = (luminance(a), luminance(b));
224 (la.max(lb) + 0.05) / (la.min(lb) + 0.05)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::theme::Palette;
231
232 use crate::theme::Rgb;
233
234 fn looks() -> Vec<Look> {
236 let mut accents: Vec<Option<Rgb>> = vec![None];
237 accents.extend(Rgb::SUGGESTED.map(Some));
238 accents.extend([0xfffacd, 0x0a1030, 0xff0000, 0xffffff, 0x000000].map(|c| Some(Rgb(c))));
239 let mut out = Vec::new();
240 for p in Palette::APPS {
241 for a in &accents {
242 for dark in [true, false] {
243 out.push(Look::new(p).with_accent(*a).with_dark(dark));
244 }
245 }
246 }
247 out
248 }
249
250 #[test]
251 fn neutrals_are_plain_greys() {
252 use neutrals::*;
253 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)] {
254 for c in [pair.0, pair.1] {
255 let (r, g, b) = (c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff);
256 assert!(r == g && g == b, "{name}: #{c:06x} is tinted");
257 }
258 }
259 }
260
261 #[test]
262 fn text_passes_aa_on_every_surface() {
263 for l in looks() {
264 let c = Colors::resolve(&l);
265 for (sname, s) in [("chrome", c.chrome), ("panel", c.panel), ("raised", c.raised), ("control", c.control), ("hover", c.hover)] {
266 for (tname, t) in [("text", c.text), ("text_muted", c.text_muted)] {
267 let r = contrast(t, s);
268 assert!(r >= 4.5, "{} dark={}: {tname} on {sname} is {r:.2}:1", l.palette.name, l.dark);
269 }
270 }
271 for (name, t) in [("danger", c.danger), ("success", c.success), ("warning", c.warning)] {
272 let r = contrast(t, c.panel);
273 assert!(r >= 4.5, "dark={}: {name} on panel is {r:.2}:1", l.dark);
274 }
275 }
276 }
277
278 #[test]
279 fn accents_pass_contrast() {
280 for l in looks() {
281 let c = Colors::resolve(&l);
282 let name = format!("{} {:?} dark={}", l.palette.name, l.accent, l.dark);
283 for (fname, f) in [("fill", c.accent), ("fill_hover", c.accent_hover)] {
284 let r = contrast(c.on_accent, f);
285 assert!(r >= 4.5, "{name}: text on {fname} is {r:.2}:1");
286 }
287 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)] {
289 let r = contrast(c.accent_mark, s);
290 assert!(r >= min, "{name}: mark on {sname} is {r:.2}:1");
291 }
292 }
293 }
294
295 #[test]
296 fn diff_lines_pass_contrast() {
297 for dark in [true, false] {
298 let c = Colors::resolve(&Look::new(Palette::LIME).with_dark(dark));
299 for (bname, bg, mark) in [("added", c.diff_added, c.success), ("removed", c.diff_removed, c.danger)] {
300 for (tname, t) in [("text", c.text), ("text_muted", c.text_muted)] {
302 let r = contrast(t, bg);
303 assert!(r >= 4.5, "dark={dark}: {tname} on the {bname} line is {r:.2}:1");
304 }
305 let r = contrast(mark, bg);
307 assert!(r >= 3.0, "dark={dark}: the sign on the {bname} line is {r:.2}:1");
308 assert!(bg != c.panel, "dark={dark}: the {bname} line is not tinted");
310 }
311 }
312 }
313
314 #[test]
315 fn surfaces_step_apart() {
316 for dark in [true, false] {
317 let c = Colors::resolve(&Look { dark, ..Look::new(Palette::LIME) });
318 let order = [c.chrome, c.panel, c.raised];
319 let l: Vec<f32> = order.iter().map(|c| luminance(*c)).collect();
320 assert!(l[0] < l[1] && l[1] < l[2], "dark={dark}: chrome < panel < raised in lightness");
321 }
322 }
323
324 #[test]
325 fn contrast_of_known_pairs() {
326 assert!((contrast(rgb_hsla(0x000000), rgb_hsla(0xffffff)) - 21.0).abs() < 0.01);
327 let r = contrast(rgb_hsla(0x767676), rgb_hsla(0xffffff));
330 assert!((r - 4.54).abs() < 0.01, "{r}");
331 let r = contrast(rgb_hsla(0x777777), rgb_hsla(0xffffff));
332 assert!((r - 4.48).abs() < 0.01, "{r}");
333 assert_eq!(rgb8([0x12, 0x34, 0x56]), rgb_hsla(0x123456));
334 }
335}