1use gpui::Hsla;
14use gpui_component::Colorize;
15
16pub fn hover_color(base: Hsla, bg: Hsla) -> Hsla {
24 bg.blend(base.opacity(0.9))
25}
26
27pub fn active_color(base: Hsla, is_dark: bool) -> Hsla {
40 let factor = if is_dark { 0.2 } else { 0.1 };
41 if base.l < 0.15 {
44 base.lighten(factor)
45 } else {
46 base.darken(factor)
47 }
48}
49
50pub fn contrast_ratio(a: Hsla, b: Hsla) -> f32 {
55 let la = relative_luminance(a);
56 let lb = relative_luminance(b);
57 let (lighter, darker) = if la > lb { (la, lb) } else { (lb, la) };
58 (lighter + 0.05) / (darker + 0.05)
59}
60
61fn relative_luminance(c: Hsla) -> f32 {
63 let rgba: gpui::Rgba = c.into();
64 let linearize = |v: f32| -> f32 {
65 let v = v.clamp(0.0, 1.0);
66 if v <= 0.04045 {
67 v / 12.92
68 } else {
69 ((v + 0.055) / 1.055).powf(2.4)
70 }
71 };
72 0.2126 * linearize(rgba.r) + 0.7152 * linearize(rgba.g) + 0.0722 * linearize(rgba.b)
73}
74
75pub fn light_variant(bg: Hsla, color: Hsla, is_dark: bool) -> Hsla {
81 if is_dark {
82 Hsla {
83 l: (color.l + 0.15).min(0.95),
84 ..color
85 }
86 } else {
87 bg.blend(color.opacity(0.8))
88 }
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used, clippy::expect_used)]
93mod tests {
94 use super::*;
95 use gpui::hsla;
96
97 #[test]
98 fn hover_color_differs_from_base() {
99 let base = hsla(0.6, 0.7, 0.5, 1.0);
100 let bg = hsla(0.0, 0.0, 1.0, 1.0); let result = hover_color(base, bg);
102 assert_ne!(result, base, "hover should differ from base");
104 }
105
106 #[test]
107 fn active_color_light_theme_darkens() {
108 let base = hsla(0.6, 0.7, 0.5, 1.0);
109 let result = active_color(base, false);
110 assert!(
111 result.l < base.l,
112 "active (light) l={} should be < base l={}",
113 result.l,
114 base.l
115 );
116 }
117
118 #[test]
119 fn active_color_dark_theme_darkens_more() {
120 let base = hsla(0.6, 0.7, 0.5, 1.0);
121 let light_result = active_color(base, false);
122 let dark_result = active_color(base, true);
123 assert!(
124 dark_result.l < light_result.l,
125 "dark active l={} should darken more than light active l={}",
126 dark_result.l,
127 light_result.l
128 );
129 }
130
131 #[test]
133 fn active_color_near_black_lightens() {
134 let near_black = hsla(0.6, 0.7, 0.05, 1.0);
135 let result = active_color(near_black, true);
136 assert!(
137 result.l > near_black.l,
138 "near-black active l={} should be > base l={} (lighten, not darken)",
139 result.l,
140 near_black.l
141 );
142 }
143
144 #[test]
145 fn active_color_near_black_light_mode_also_lightens() {
146 let near_black = hsla(0.3, 0.5, 0.10, 1.0);
147 let result = active_color(near_black, false);
148 assert!(
149 result.l > near_black.l,
150 "near-black active (light) l={} should be > base l={}",
151 result.l,
152 near_black.l
153 );
154 }
155
156 #[test]
157 fn contrast_ratio_black_white() {
158 let black = hsla(0.0, 0.0, 0.0, 1.0);
159 let white = hsla(0.0, 0.0, 1.0, 1.0);
160 let ratio = contrast_ratio(black, white);
161 assert!(
162 ratio > 20.0,
163 "black/white contrast should be ~21, got {}",
164 ratio
165 );
166 }
167
168 #[test]
169 fn contrast_ratio_same_color_is_one() {
170 let c = hsla(0.5, 0.5, 0.5, 1.0);
171 let ratio = contrast_ratio(c, c);
172 assert!(
173 (ratio - 1.0).abs() < 0.01,
174 "same-color contrast should be 1.0, got {}",
175 ratio
176 );
177 }
178
179 #[test]
180 fn light_variant_dark_theme_increases_lightness() {
181 let bg = hsla(0.0, 0.0, 0.1, 1.0);
182 let color = hsla(0.0, 0.8, 0.4, 1.0);
183 let result = light_variant(bg, color, true);
184 assert!(
185 result.l > color.l,
186 "dark theme light_variant l={} should be > base l={}",
187 result.l,
188 color.l
189 );
190 }
191
192 #[test]
193 fn light_variant_light_theme_blends_toward_bg() {
194 let bg = hsla(0.0, 0.0, 0.95, 1.0);
195 let color = hsla(0.0, 0.8, 0.4, 1.0);
196 let result = light_variant(bg, color, false);
197 assert!(
199 result.l > color.l,
200 "light theme light_variant l={} should be > base l={}",
201 result.l,
202 color.l
203 );
204 }
205
206 #[test]
208 fn hover_color_near_white() {
209 let near_white = hsla(0.6, 0.5, 0.95, 1.0);
210 let bg = hsla(0.0, 0.0, 1.0, 1.0); let result = hover_color(near_white, bg);
212 assert_ne!(
213 result, near_white,
214 "hover of near-white color should still differ from input"
215 );
216 }
217
218 #[test]
220 fn active_color_near_boundary() {
221 let base = hsla(0.6, 0.7, 0.16, 1.0);
223 let result = active_color(base, true);
224 assert!(
225 result.l < base.l,
226 "l=0.16 (above 0.15 threshold) active l={} should darken (< base l={})",
227 result.l,
228 base.l
229 );
230 }
231
232 #[test]
234 fn hover_color_zero_saturation() {
235 let gray = hsla(0.0, 0.0, 0.5, 1.0); let bg = hsla(0.0, 0.0, 0.1, 1.0); let result = hover_color(gray, bg);
238 assert_ne!(
239 result, gray,
240 "hover of zero-saturation gray should still differ from input"
241 );
242 }
243
244 #[test]
246 fn hover_color_transparent_base() {
247 let transparent = hsla(0.5, 0.5, 0.5, 0.0);
248 let bg = hsla(0.0, 0.0, 1.0, 1.0);
249 let result = hover_color(transparent, bg);
250 assert!(
253 (result.l - bg.l).abs() < 0.05,
254 "hover of transparent base l={} should be close to bg l={}",
255 result.l,
256 bg.l
257 );
258 }
259
260 #[test]
266 fn active_color_pure_black_stays_black() {
267 let pure_black = hsla(0.0, 0.0, 0.0, 1.0);
268 let result = active_color(pure_black, true);
269 assert!(
272 (result.l - 0.0).abs() < f32::EPSILON,
273 "pure black active l={} stays at 0.0 (Colorize limitation)",
274 result.l,
275 );
276 }
277
278 #[test]
279 fn light_variant_clamped_to_095() {
280 let bg = hsla(0.0, 0.0, 0.1, 1.0);
281 let bright = hsla(0.5, 0.5, 0.9, 1.0);
282 let result = light_variant(bg, bright, true);
283 assert!(
284 result.l <= 0.95,
285 "light_variant should clamp to 0.95, got {}",
286 result.l
287 );
288 }
289}