1use gpui::{Hsla, Rgba};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct Color {
12 pub r: u8,
13 pub g: u8,
14 pub b: u8,
15}
16
17impl Color {
18 pub const fn new(r: u8, g: u8, b: u8) -> Self {
19 Color { r, g, b }
20 }
21
22 pub const fn hex(s: &str) -> Self {
27 let bytes = s.as_bytes();
28 let hex = if !bytes.is_empty() && bytes[0] == b'#' {
29 split_at(bytes, 1)
30 } else {
31 bytes
32 };
33 match hex.len() {
34 3 => Color::new(
35 nibble(hex[0]) * 17,
36 nibble(hex[1]) * 17,
37 nibble(hex[2]) * 17,
38 ),
39 6 => Color::new(
40 nibble(hex[0]) * 16 + nibble(hex[1]),
41 nibble(hex[2]) * 16 + nibble(hex[3]),
42 nibble(hex[4]) * 16 + nibble(hex[5]),
43 ),
44 _ => panic!("guise: color hex must be 3 or 6 digits"),
45 }
46 }
47
48 pub fn from_hsla(c: Hsla) -> Color {
51 let (r, g, b) = hsl_to_rgb(c.h, c.s, c.l);
52 Color::new(r, g, b)
53 }
54
55 pub fn rgba(self) -> Rgba {
57 Rgba {
58 r: self.r as f32 / 255.0,
59 g: self.g as f32 / 255.0,
60 b: self.b as f32 / 255.0,
61 a: 1.0,
62 }
63 }
64
65 pub fn hsla(self) -> Hsla {
67 self.rgba().into()
68 }
69
70 pub fn alpha(self, a: f32) -> Hsla {
72 let mut hsla: Hsla = self.rgba().into();
73 hsla.a = a.clamp(0.0, 1.0);
74 hsla
75 }
76
77 pub fn luminance(self) -> f32 {
80 (0.2126 * self.r as f32 + 0.7152 * self.g as f32 + 0.0722 * self.b as f32) / 255.0
81 }
82
83 pub fn contrasting(self) -> Color {
85 if self.luminance() > 0.55 {
86 Color::new(0, 0, 0)
87 } else {
88 Color::new(255, 255, 255)
89 }
90 }
91}
92
93impl From<Color> for Hsla {
94 fn from(c: Color) -> Hsla {
95 c.hsla()
96 }
97}
98
99fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
101 let to_u8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
102 if s <= 0.0 {
103 let v = to_u8(l);
104 return (v, v, v);
105 }
106 let q = if l < 0.5 {
107 l * (1.0 + s)
108 } else {
109 l + s - l * s
110 };
111 let p = 2.0 * l - q;
112 let hue = |mut t: f32| {
113 if t < 0.0 {
114 t += 1.0;
115 }
116 if t > 1.0 {
117 t -= 1.0;
118 }
119 if t < 1.0 / 6.0 {
120 p + (q - p) * 6.0 * t
121 } else if t < 1.0 / 2.0 {
122 q
123 } else if t < 2.0 / 3.0 {
124 p + (q - p) * (2.0 / 3.0 - t) * 6.0
125 } else {
126 p
127 }
128 };
129 (
130 to_u8(hue(h + 1.0 / 3.0)),
131 to_u8(hue(h)),
132 to_u8(hue(h - 1.0 / 3.0)),
133 )
134}
135
136const fn split_at(bytes: &[u8], at: usize) -> &[u8] {
137 bytes.split_at(at).1
139}
140
141const fn nibble(b: u8) -> u8 {
142 match b {
143 b'0'..=b'9' => b - b'0',
144 b'a'..=b'f' => b - b'a' + 10,
145 b'A'..=b'F' => b - b'A' + 10,
146 _ => panic!("guise: invalid hex digit in color"),
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn parses_long_and_short_forms() {
156 assert_eq!(Color::hex("#228be6"), Color::new(0x22, 0x8b, 0xe6));
157 assert_eq!(Color::hex("228be6"), Color::new(0x22, 0x8b, 0xe6));
158 assert_eq!(Color::hex("#fff"), Color::new(255, 255, 255));
159 assert_eq!(Color::hex("000"), Color::new(0, 0, 0));
160 }
161
162 #[test]
163 fn contrasting_picks_readable_foreground() {
164 assert_eq!(Color::hex("#ffffff").contrasting(), Color::new(0, 0, 0));
165 assert_eq!(
166 Color::hex("#1864ab").contrasting(),
167 Color::new(255, 255, 255)
168 );
169 }
170}