1use csscolorparser::Color as CssColor;
2use ls_types::{Color, ColorPresentation, Range, TextEdit};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
5pub struct NormalizedColorKey {
6 pub red: u8,
7 pub green: u8,
8 pub blue: u8,
9 pub alpha: u8,
10}
11
12pub fn parse_color(value: &str) -> Option<Color> {
14 parse_csscolorparser(value.trim())
15}
16
17pub fn normalized_color_key(value: &str) -> Option<NormalizedColorKey> {
18 parse_color(value).map(normalize_color)
19}
20
21pub fn normalize_color(color: Color) -> NormalizedColorKey {
22 NormalizedColorKey {
23 red: color_channel_to_u8(color.red),
24 green: color_channel_to_u8(color.green),
25 blue: color_channel_to_u8(color.blue),
26 alpha: color_channel_to_u8(color.alpha),
27 }
28}
29
30pub fn color_from_key(key: NormalizedColorKey) -> Color {
31 Color {
32 red: key.red as f32 / 255.0,
33 green: key.green as f32 / 255.0,
34 blue: key.blue as f32 / 255.0,
35 alpha: key.alpha as f32 / 255.0,
36 }
37}
38
39fn parse_csscolorparser(value: &str) -> Option<Color> {
40 let parsed: CssColor = value.parse().ok()?;
41 Some(Color {
42 red: parsed.r as f32,
43 green: parsed.g as f32,
44 blue: parsed.b as f32,
45 alpha: parsed.a as f32,
46 })
47}
48
49pub fn generate_color_presentations(color: Color, range: Range) -> Vec<ColorPresentation> {
51 let mut presentations = Vec::new();
52
53 let hex_str = format_color_as_hex(color);
54 presentations.push(ColorPresentation {
55 label: hex_str.clone(),
56 text_edit: Some(TextEdit {
57 range,
58 new_text: hex_str,
59 }),
60 additional_text_edits: None,
61 });
62
63 let rgb_str = format_color_as_rgb(color);
64 presentations.push(ColorPresentation {
65 label: rgb_str.clone(),
66 text_edit: Some(TextEdit {
67 range,
68 new_text: rgb_str,
69 }),
70 additional_text_edits: None,
71 });
72
73 let hsl_str = format_color_as_hsl(color);
74 presentations.push(ColorPresentation {
75 label: hsl_str.clone(),
76 text_edit: Some(TextEdit {
77 range,
78 new_text: hsl_str,
79 }),
80 additional_text_edits: None,
81 });
82
83 presentations
84}
85
86pub fn format_color_as_hex(color: Color) -> String {
87 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
88 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
89 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
90 let a = (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
91
92 if a == 255 {
93 format!("#{:02x}{:02x}{:02x}", r, g, b)
94 } else {
95 format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
96 }
97}
98
99pub fn format_color_as_rgb(color: Color) -> String {
100 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
101 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
102 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
103 let a = color.alpha.clamp(0.0, 1.0);
104
105 if a >= 1.0 {
106 format!("rgb({}, {}, {})", r, g, b)
107 } else {
108 format!("rgba({}, {}, {}, {:.2})", r, g, b, a)
109 }
110}
111
112pub fn format_color_as_hsl(color: Color) -> String {
113 let (h, s, l) = rgb_to_hsl(color.red, color.green, color.blue);
114 let a = color.alpha.clamp(0.0, 1.0);
115
116 let h_deg = (h * 360.0).round();
117 let s_pct = (s * 100.0).round();
118 let l_pct = (l * 100.0).round();
119
120 if a >= 1.0 {
121 format!("hsl({}, {}%, {}%)", h_deg, s_pct, l_pct)
122 } else {
123 format!("hsla({}, {}%, {}%, {:.2})", h_deg, s_pct, l_pct, a)
124 }
125}
126
127fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
128 let max = r.max(g).max(b);
129 let min = r.min(g).min(b);
130 let l = (max + min) / 2.0;
131
132 if max == min {
133 return (0.0, 0.0, l);
134 }
135
136 let d = max - min;
137 let s = if l > 0.5 {
138 d / (2.0 - max - min)
139 } else {
140 d / (max + min)
141 };
142
143 let h = if max == r {
144 ((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
145 } else if max == g {
146 ((b - r) / d + 2.0) / 6.0
147 } else {
148 ((r - g) / d + 4.0) / 6.0
149 };
150
151 (h, s, l)
152}
153
154fn color_channel_to_u8(channel: f32) -> u8 {
155 (channel.clamp(0.0, 1.0) * 255.0).round() as u8
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use ls_types::Position;
162
163 fn approx_eq(a: f32, b: f32) -> bool {
164 (a - b).abs() < 0.01
165 }
166
167 #[test]
168 fn parse_color_hex_and_named() {
169 let color = parse_color("#abc").expect("hex");
170 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
171 assert!(approx_eq(color.green, 0xBB as f32 / 255.0));
172 assert!(approx_eq(color.blue, 0xCC as f32 / 255.0));
173 assert!(approx_eq(color.alpha, 1.0));
174
175 let color = parse_color("#abcd").expect("hex with alpha");
176 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
177 assert!(approx_eq(color.alpha, 0xDD as f32 / 255.0));
178
179 let color = parse_color("blue").expect("named");
180 assert!(approx_eq(color.blue, 1.0));
181 assert!(approx_eq(color.red, 0.0));
182 }
183
184 #[test]
185 fn parse_color_rgb_variants() {
186 let color = parse_color("rgb(255, 0, 128)").expect("rgb");
187 assert!(approx_eq(color.red, 1.0));
188 assert!(approx_eq(color.green, 0.0));
189 assert!(approx_eq(color.blue, 128.0 / 255.0));
190
191 let color = parse_color("rgba(255, 0, 0, 0.5)").expect("rgba");
192 assert!(approx_eq(color.red, 1.0));
193 assert!(approx_eq(color.alpha, 0.5));
194
195 let color = parse_color("rgb(100%, 0%, 50%)").expect("rgb percent");
196 assert!(approx_eq(color.red, 1.0));
197 assert!(approx_eq(color.blue, 0.5));
198
199 let color = parse_color("rgba(255, 0, 0, 50%)").expect("rgba percent");
200 assert!(approx_eq(color.alpha, 0.5));
201 }
202
203 #[test]
204 fn normalized_color_key_matches_equivalent_inputs() {
205 let white = normalized_color_key("white").expect("white");
206 assert_eq!(white, normalized_color_key("#fff").expect("#fff"));
207 assert_eq!(white, normalized_color_key("#ffffff").expect("#ffffff"));
208 assert_eq!(
209 white,
210 normalized_color_key("rgb(255 255 255)").expect("rgb")
211 );
212 assert_eq!(white, normalized_color_key("hsl(0 0% 100%)").expect("hsl"));
213 }
214
215 #[test]
216 fn normalized_color_key_preserves_alpha() {
217 let translucent = normalized_color_key("rgba(255, 255, 255, 0.5)").expect("rgba");
218 assert_eq!(
219 translucent,
220 normalized_color_key("#ffffff80").expect("hex alpha")
221 );
222 assert_ne!(translucent, normalized_color_key("white").expect("white"));
223 }
224
225 #[test]
226 fn generate_color_presentations_formats_output() {
227 let range = Range::new(Position::new(0, 0), Position::new(0, 4));
228 let color = Color {
229 red: 1.0,
230 green: 0.0,
231 blue: 0.5,
232 alpha: 1.0,
233 };
234 let presentations = generate_color_presentations(color, range);
235 assert_eq!(presentations.len(), 3);
236 assert!(presentations[0].label.starts_with('#'));
237 assert!(presentations[1].label.starts_with("rgb("));
238 assert!(presentations[2].label.starts_with("hsl("));
239
240 let color = Color {
241 red: 1.0,
242 green: 0.0,
243 blue: 0.0,
244 alpha: 0.5,
245 };
246 let presentations = generate_color_presentations(color, range);
247 assert_eq!(presentations.len(), 3);
248 assert!(presentations[0].label.starts_with('#'));
249 assert!(presentations[1].label.starts_with("rgba("));
250 assert!(presentations[2].label.starts_with("hsla("));
251 }
252
253 #[test]
254 fn format_color_hex_opaque_and_transparent() {
255 let color = Color {
257 red: 0.0,
258 green: 0.5,
259 blue: 1.0,
260 alpha: 1.0,
261 };
262 let hex = format_color_as_hex(color);
263 assert_eq!(hex, "#0080ff");
264
265 let color = Color {
267 red: 1.0,
268 green: 0.0,
269 blue: 0.0,
270 alpha: 0.5,
271 };
272 let hex = format_color_as_hex(color);
273 assert_eq!(hex, "#ff000080");
274 }
275
276 #[test]
277 fn format_color_rgb_with_alpha() {
278 let color = Color {
279 red: 0.5,
280 green: 0.5,
281 blue: 0.5,
282 alpha: 1.0,
283 };
284 let rgb = format_color_as_rgb(color);
285 assert_eq!(rgb, "rgb(128, 128, 128)");
286
287 let color = Color {
288 red: 1.0,
289 green: 0.0,
290 blue: 0.0,
291 alpha: 0.75,
292 };
293 let rgba = format_color_as_rgb(color);
294 assert_eq!(rgba, "rgba(255, 0, 0, 0.75)");
295 }
296
297 #[test]
298 fn format_color_hsl_with_alpha() {
299 let color = Color {
300 red: 1.0,
301 green: 0.0,
302 blue: 0.0,
303 alpha: 1.0,
304 };
305 let hsl = format_color_as_hsl(color);
306 assert!(hsl.starts_with("hsl("));
307 assert!(hsl.contains("0,") || hsl.contains("360,")); let color = Color {
310 red: 0.0,
311 green: 0.5,
312 blue: 1.0,
313 alpha: 0.5,
314 };
315 let hsla = format_color_as_hsl(color);
316 assert!(hsla.starts_with("hsla("));
317 assert!(hsla.contains("0.50"));
318 }
319
320 #[test]
321 fn rgb_to_hsl_conversion() {
322 let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
324 assert!(approx_eq(h, 0.0));
325 assert!(approx_eq(s, 1.0));
326 assert!(approx_eq(l, 0.5));
327
328 let (h, s, l) = rgb_to_hsl(0.0, 1.0, 0.0);
330 assert!(approx_eq(h, 1.0 / 3.0));
331 assert!(approx_eq(s, 1.0));
332 assert!(approx_eq(l, 0.5));
333
334 let (_h, s, l) = rgb_to_hsl(0.5, 0.5, 0.5);
336 assert!(approx_eq(s, 0.0));
337 assert!(approx_eq(l, 0.5));
338 }
339
340 #[test]
341 fn parse_color_edge_cases() {
342 assert!(parse_color("not-a-color").is_none());
344 assert!(parse_color("").is_none());
345 assert!(parse_color("rgb(999, 999, 999)").is_some()); assert!(parse_color("rebeccapurple").is_some());
349 assert!(parse_color("aliceblue").is_some());
350
351 let color = parse_color("transparent").expect("transparent");
353 assert!(approx_eq(color.alpha, 0.0));
354 }
355
356 #[test]
357 fn color_clamping() {
358 let color = Color {
360 red: 1.5,
361 green: -0.5,
362 blue: 0.5,
363 alpha: 2.0,
364 };
365
366 let hex = format_color_as_hex(color);
367 assert!(hex.starts_with('#'));
368
369 let rgb = format_color_as_rgb(color);
370 assert!(rgb.contains("255")); assert!(rgb.contains("0")); }
373}