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}
81
82impl Colors {
83 pub fn resolve(look: &Look) -> Colors {
85 let dark = look.dark;
86 let n = |pair: (u32, u32)| rgb_hsla(if dark { pair.0 } else { pair.1 });
87 let a = look.active_palette().accent(dark);
88 Colors {
89 chrome: n(neutrals::CHROME),
90 panel: n(neutrals::PANEL),
91 raised: n(neutrals::RAISED),
92 control: n(neutrals::CONTROL),
93 hover: n(neutrals::HOVER),
94 border: n(neutrals::BORDER),
95 text: n(neutrals::TEXT),
96 text_muted: n(neutrals::TEXT_MUTED),
97 text_disabled: n(neutrals::TEXT_DISABLED),
98 accent: rgb_hsla(a.fill),
99 accent_hover: rgb_hsla(a.fill_hover),
100 on_accent: rgb_hsla(a.on_fill),
101 accent_mark: rgb_hsla(a.mark),
102 danger: n(neutrals::DANGER),
103 success: n(neutrals::SUCCESS),
104 warning: n(neutrals::WARNING),
105 }
106 }
107}
108
109pub fn colors() -> Colors {
111 Colors::resolve(&look())
112}
113
114pub fn chrome() -> Hsla {
116 colors().chrome
117}
118pub fn panel() -> Hsla {
120 colors().panel
121}
122pub fn raised() -> Hsla {
124 colors().raised
125}
126pub fn control() -> Hsla {
128 colors().control
129}
130pub fn hover() -> Hsla {
132 colors().hover
133}
134pub fn border() -> Hsla {
136 colors().border
137}
138pub fn text() -> Hsla {
140 colors().text
141}
142pub fn text_muted() -> Hsla {
144 colors().text_muted
145}
146pub fn text_disabled() -> Hsla {
148 colors().text_disabled
149}
150pub fn accent() -> Hsla {
152 colors().accent
153}
154pub fn accent_hover() -> Hsla {
156 colors().accent_hover
157}
158pub fn on_accent() -> Hsla {
160 colors().on_accent
161}
162pub fn accent_mark() -> Hsla {
164 colors().accent_mark
165}
166pub fn danger() -> Hsla {
168 colors().danger
169}
170pub fn success() -> Hsla {
172 colors().success
173}
174pub fn warning() -> Hsla {
176 colors().warning
177}
178
179pub fn rgb_hsla(c: u32) -> Hsla {
181 rgb(c).into()
182}
183
184pub fn rgb8(c: [u8; 3]) -> Hsla {
186 rgb_hsla(((c[0] as u32) << 16) | ((c[1] as u32) << 8) | c[2] as u32)
187}
188
189pub fn luminance(c: Hsla) -> f32 {
191 let Rgba { r, g, b, .. } = c.to_rgb();
192 let lin = |v: f32| if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) };
193 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
194}
195
196pub fn contrast(a: Hsla, b: Hsla) -> f32 {
198 let (la, lb) = (luminance(a), luminance(b));
199 (la.max(lb) + 0.05) / (la.min(lb) + 0.05)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::theme::Palette;
206
207 use crate::theme::Rgb;
208
209 fn looks() -> Vec<Look> {
211 let mut accents: Vec<Option<Rgb>> = vec![None];
212 accents.extend(Rgb::SUGGESTED.map(Some));
213 accents.extend([0xfffacd, 0x0a1030, 0xff0000, 0xffffff, 0x000000].map(|c| Some(Rgb(c))));
214 let mut out = Vec::new();
215 for p in Palette::APPS {
216 for a in &accents {
217 for dark in [true, false] {
218 out.push(Look::new(p).with_accent(*a).with_dark(dark));
219 }
220 }
221 }
222 out
223 }
224
225 #[test]
226 fn neutrals_are_plain_greys() {
227 use neutrals::*;
228 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)] {
229 for c in [pair.0, pair.1] {
230 let (r, g, b) = (c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff);
231 assert!(r == g && g == b, "{name}: #{c:06x} is tinted");
232 }
233 }
234 }
235
236 #[test]
237 fn text_passes_aa_on_every_surface() {
238 for l in looks() {
239 let c = Colors::resolve(&l);
240 for (sname, s) in [("chrome", c.chrome), ("panel", c.panel), ("raised", c.raised), ("control", c.control), ("hover", c.hover)] {
241 for (tname, t) in [("text", c.text), ("text_muted", c.text_muted)] {
242 let r = contrast(t, s);
243 assert!(r >= 4.5, "{} dark={}: {tname} on {sname} is {r:.2}:1", l.palette.name, l.dark);
244 }
245 }
246 for (name, t) in [("danger", c.danger), ("success", c.success), ("warning", c.warning)] {
247 let r = contrast(t, c.panel);
248 assert!(r >= 4.5, "dark={}: {name} on panel is {r:.2}:1", l.dark);
249 }
250 }
251 }
252
253 #[test]
254 fn accents_pass_contrast() {
255 for l in looks() {
256 let c = Colors::resolve(&l);
257 let name = format!("{} {:?} dark={}", l.palette.name, l.accent, l.dark);
258 for (fname, f) in [("fill", c.accent), ("fill_hover", c.accent_hover)] {
259 let r = contrast(c.on_accent, f);
260 assert!(r >= 4.5, "{name}: text on {fname} is {r:.2}:1");
261 }
262 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)] {
264 let r = contrast(c.accent_mark, s);
265 assert!(r >= min, "{name}: mark on {sname} is {r:.2}:1");
266 }
267 }
268 }
269
270 #[test]
271 fn surfaces_step_apart() {
272 for dark in [true, false] {
273 let c = Colors::resolve(&Look { dark, ..Look::new(Palette::LIME) });
274 let order = [c.chrome, c.panel, c.raised];
275 let l: Vec<f32> = order.iter().map(|c| luminance(*c)).collect();
276 assert!(l[0] < l[1] && l[1] < l[2], "dark={dark}: chrome < panel < raised in lightness");
277 }
278 }
279
280 #[test]
281 fn contrast_of_known_pairs() {
282 assert!((contrast(rgb_hsla(0x000000), rgb_hsla(0xffffff)) - 21.0).abs() < 0.01);
283 let r = contrast(rgb_hsla(0x767676), rgb_hsla(0xffffff));
286 assert!((r - 4.54).abs() < 0.01, "{r}");
287 let r = contrast(rgb_hsla(0x777777), rgb_hsla(0xffffff));
288 assert!((r - 4.48).abs() < 0.01, "{r}");
289 assert_eq!(rgb8([0x12, 0x34, 0x56]), rgb_hsla(0x123456));
290 }
291}