use csscolorparser::Color as CssColor;
use ls_types::{Color, ColorPresentation, Range, TextEdit};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct NormalizedColorKey {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
pub fn parse_color(value: &str) -> Option<Color> {
parse_csscolorparser(value.trim())
}
pub fn normalized_color_key(value: &str) -> Option<NormalizedColorKey> {
parse_color(value).map(normalize_color)
}
pub fn normalize_color(color: Color) -> NormalizedColorKey {
NormalizedColorKey {
red: color_channel_to_u8(color.red),
green: color_channel_to_u8(color.green),
blue: color_channel_to_u8(color.blue),
alpha: color_channel_to_u8(color.alpha),
}
}
pub fn color_from_key(key: NormalizedColorKey) -> Color {
Color {
red: key.red as f32 / 255.0,
green: key.green as f32 / 255.0,
blue: key.blue as f32 / 255.0,
alpha: key.alpha as f32 / 255.0,
}
}
fn parse_csscolorparser(value: &str) -> Option<Color> {
let parsed: CssColor = value.parse().ok()?;
Some(Color {
red: parsed.r as f32,
green: parsed.g as f32,
blue: parsed.b as f32,
alpha: parsed.a as f32,
})
}
pub fn generate_color_presentations(color: Color, range: Range) -> Vec<ColorPresentation> {
let mut presentations = Vec::new();
let hex_str = format_color_as_hex(color);
presentations.push(ColorPresentation {
label: hex_str.clone(),
text_edit: Some(TextEdit {
range,
new_text: hex_str,
}),
additional_text_edits: None,
});
let rgb_str = format_color_as_rgb(color);
presentations.push(ColorPresentation {
label: rgb_str.clone(),
text_edit: Some(TextEdit {
range,
new_text: rgb_str,
}),
additional_text_edits: None,
});
let hsl_str = format_color_as_hsl(color);
presentations.push(ColorPresentation {
label: hsl_str.clone(),
text_edit: Some(TextEdit {
range,
new_text: hsl_str,
}),
additional_text_edits: None,
});
presentations
}
pub fn format_color_as_hex(color: Color) -> String {
let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
let a = (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
if a == 255 {
format!("#{:02x}{:02x}{:02x}", r, g, b)
} else {
format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
}
}
pub fn format_color_as_rgb(color: Color) -> String {
let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
let a = color.alpha.clamp(0.0, 1.0);
if a >= 1.0 {
format!("rgb({}, {}, {})", r, g, b)
} else {
format!("rgba({}, {}, {}, {:.2})", r, g, b, a)
}
}
pub fn format_color_as_hsl(color: Color) -> String {
let (h, s, l) = rgb_to_hsl(color.red, color.green, color.blue);
let a = color.alpha.clamp(0.0, 1.0);
let h_deg = (h * 360.0).round();
let s_pct = (s * 100.0).round();
let l_pct = (l * 100.0).round();
if a >= 1.0 {
format!("hsl({}, {}%, {}%)", h_deg, s_pct, l_pct)
} else {
format!("hsla({}, {}%, {}%, {:.2})", h_deg, s_pct, l_pct, a)
}
}
fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) / 2.0;
if max == min {
return (0.0, 0.0, l);
}
let d = max - min;
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if max == r {
((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
} else if max == g {
((b - r) / d + 2.0) / 6.0
} else {
((r - g) / d + 4.0) / 6.0
};
(h, s, l)
}
fn color_channel_to_u8(channel: f32) -> u8 {
(channel.clamp(0.0, 1.0) * 255.0).round() as u8
}
#[cfg(test)]
mod tests {
use super::*;
use ls_types::Position;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 0.01
}
#[test]
fn parse_color_hex_and_named() {
let color = parse_color("#abc").expect("hex");
assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
assert!(approx_eq(color.green, 0xBB as f32 / 255.0));
assert!(approx_eq(color.blue, 0xCC as f32 / 255.0));
assert!(approx_eq(color.alpha, 1.0));
let color = parse_color("#abcd").expect("hex with alpha");
assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
assert!(approx_eq(color.alpha, 0xDD as f32 / 255.0));
let color = parse_color("blue").expect("named");
assert!(approx_eq(color.blue, 1.0));
assert!(approx_eq(color.red, 0.0));
}
#[test]
fn parse_color_rgb_variants() {
let color = parse_color("rgb(255, 0, 128)").expect("rgb");
assert!(approx_eq(color.red, 1.0));
assert!(approx_eq(color.green, 0.0));
assert!(approx_eq(color.blue, 128.0 / 255.0));
let color = parse_color("rgba(255, 0, 0, 0.5)").expect("rgba");
assert!(approx_eq(color.red, 1.0));
assert!(approx_eq(color.alpha, 0.5));
let color = parse_color("rgb(100%, 0%, 50%)").expect("rgb percent");
assert!(approx_eq(color.red, 1.0));
assert!(approx_eq(color.blue, 0.5));
let color = parse_color("rgba(255, 0, 0, 50%)").expect("rgba percent");
assert!(approx_eq(color.alpha, 0.5));
}
#[test]
fn normalized_color_key_matches_equivalent_inputs() {
let white = normalized_color_key("white").expect("white");
assert_eq!(white, normalized_color_key("#fff").expect("#fff"));
assert_eq!(white, normalized_color_key("#ffffff").expect("#ffffff"));
assert_eq!(
white,
normalized_color_key("rgb(255 255 255)").expect("rgb")
);
assert_eq!(white, normalized_color_key("hsl(0 0% 100%)").expect("hsl"));
}
#[test]
fn normalized_color_key_preserves_alpha() {
let translucent = normalized_color_key("rgba(255, 255, 255, 0.5)").expect("rgba");
assert_eq!(
translucent,
normalized_color_key("#ffffff80").expect("hex alpha")
);
assert_ne!(translucent, normalized_color_key("white").expect("white"));
}
#[test]
fn generate_color_presentations_formats_output() {
let range = Range::new(Position::new(0, 0), Position::new(0, 4));
let color = Color {
red: 1.0,
green: 0.0,
blue: 0.5,
alpha: 1.0,
};
let presentations = generate_color_presentations(color, range);
assert_eq!(presentations.len(), 3);
assert!(presentations[0].label.starts_with('#'));
assert!(presentations[1].label.starts_with("rgb("));
assert!(presentations[2].label.starts_with("hsl("));
let color = Color {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 0.5,
};
let presentations = generate_color_presentations(color, range);
assert_eq!(presentations.len(), 3);
assert!(presentations[0].label.starts_with('#'));
assert!(presentations[1].label.starts_with("rgba("));
assert!(presentations[2].label.starts_with("hsla("));
}
#[test]
fn format_color_hex_opaque_and_transparent() {
let color = Color {
red: 0.0,
green: 0.5,
blue: 1.0,
alpha: 1.0,
};
let hex = format_color_as_hex(color);
assert_eq!(hex, "#0080ff");
let color = Color {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 0.5,
};
let hex = format_color_as_hex(color);
assert_eq!(hex, "#ff000080");
}
#[test]
fn format_color_rgb_with_alpha() {
let color = Color {
red: 0.5,
green: 0.5,
blue: 0.5,
alpha: 1.0,
};
let rgb = format_color_as_rgb(color);
assert_eq!(rgb, "rgb(128, 128, 128)");
let color = Color {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 0.75,
};
let rgba = format_color_as_rgb(color);
assert_eq!(rgba, "rgba(255, 0, 0, 0.75)");
}
#[test]
fn format_color_hsl_with_alpha() {
let color = Color {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 1.0,
};
let hsl = format_color_as_hsl(color);
assert!(hsl.starts_with("hsl("));
assert!(hsl.contains("0,") || hsl.contains("360,"));
let color = Color {
red: 0.0,
green: 0.5,
blue: 1.0,
alpha: 0.5,
};
let hsla = format_color_as_hsl(color);
assert!(hsla.starts_with("hsla("));
assert!(hsla.contains("0.50"));
}
#[test]
fn rgb_to_hsl_conversion() {
let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
assert!(approx_eq(h, 0.0));
assert!(approx_eq(s, 1.0));
assert!(approx_eq(l, 0.5));
let (h, s, l) = rgb_to_hsl(0.0, 1.0, 0.0);
assert!(approx_eq(h, 1.0 / 3.0));
assert!(approx_eq(s, 1.0));
assert!(approx_eq(l, 0.5));
let (_h, s, l) = rgb_to_hsl(0.5, 0.5, 0.5);
assert!(approx_eq(s, 0.0));
assert!(approx_eq(l, 0.5));
}
#[test]
fn parse_color_edge_cases() {
assert!(parse_color("not-a-color").is_none());
assert!(parse_color("").is_none());
assert!(parse_color("rgb(999, 999, 999)").is_some());
assert!(parse_color("rebeccapurple").is_some());
assert!(parse_color("aliceblue").is_some());
let color = parse_color("transparent").expect("transparent");
assert!(approx_eq(color.alpha, 0.0));
}
#[test]
fn color_clamping() {
let color = Color {
red: 1.5,
green: -0.5,
blue: 0.5,
alpha: 2.0,
};
let hex = format_color_as_hex(color);
assert!(hex.starts_with('#'));
let rgb = format_color_as_rgb(color);
assert!(rgb.contains("255")); assert!(rgb.contains("0")); }
}