1use thiserror::Error;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct Color {
8 pub r: u8,
9 pub g: u8,
10 pub b: u8,
11 pub a: u8,
12}
13
14impl Color {
15 pub const RED: Color = Color::rgb(255, 0, 0);
16 pub const GREEN: Color = Color::rgb(0, 200, 0);
17 pub const BLUE: Color = Color::rgb(0, 100, 255);
18 pub const YELLOW: Color = Color::rgb(255, 220, 0);
19 pub const CYAN: Color = Color::rgb(0, 220, 220);
20 pub const MAGENTA: Color = Color::rgb(220, 0, 220);
21 pub const WHITE: Color = Color::rgb(255, 255, 255);
22 pub const BLACK: Color = Color::rgb(0, 0, 0);
23 pub const ORANGE: Color = Color::rgb(255, 140, 0);
24 pub const PURPLE: Color = Color::rgb(160, 32, 240);
25 pub const PINK: Color = Color::rgb(255, 105, 180);
26 pub const GRAY: Color = Color::rgb(128, 128, 128);
27
28 pub const EXPECTED: Color = Color::GREEN;
30 pub const ACTUAL: Color = Color::RED;
32 pub const HINT: Color = Color::YELLOW;
34 pub const ERROR: Color = Color::RED;
36 pub const SUCCESS: Color = Color::GREEN;
38
39 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
40 Self { r, g, b, a: 255 }
41 }
42
43 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
44 Self { r, g, b, a }
45 }
46
47 pub fn to_rgba(self) -> [u8; 4] {
48 [self.r, self.g, self.b, self.a]
49 }
50
51 pub fn parse(s: &str) -> Result<Self, ColorParseError> {
54 let s = s.trim();
55 if let Some(hex) = s.strip_prefix('#') {
56 return parse_hex(hex);
57 }
58 if s.starts_with("rgb(") && s.ends_with(')') {
59 return parse_rgb_fn(&s[4..s.len() - 1]);
60 }
61 if s.starts_with("rgba(") && s.ends_with(')') {
62 return parse_rgba_fn(&s[5..s.len() - 1]);
63 }
64 parse_named(s)
65 }
66}
67
68fn parse_hex(hex: &str) -> Result<Color, ColorParseError> {
69 if !hex.len().is_multiple_of(2) || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
77 return Err(ColorParseError::BadHex(hex.to_string()));
78 }
79 let bytes: Result<Vec<u8>, _> = (0..hex.len())
80 .step_by(2)
81 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16))
82 .collect();
83 let bytes = bytes.map_err(|_| ColorParseError::BadHex(hex.to_string()))?;
84 match bytes.len() {
85 3 => Ok(Color::rgb(bytes[0], bytes[1], bytes[2])),
86 4 => Ok(Color::rgba(bytes[0], bytes[1], bytes[2], bytes[3])),
87 _ => Err(ColorParseError::BadHex(hex.to_string())),
88 }
89}
90
91fn parse_rgb_fn(inner: &str) -> Result<Color, ColorParseError> {
92 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
93 if parts.len() != 3 {
94 return Err(ColorParseError::BadRgb(inner.to_string()));
95 }
96 let r = parts[0]
97 .parse()
98 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
99 let g = parts[1]
100 .parse()
101 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
102 let b = parts[2]
103 .parse()
104 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
105 Ok(Color::rgb(r, g, b))
106}
107
108fn parse_rgba_fn(inner: &str) -> Result<Color, ColorParseError> {
109 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
110 if parts.len() != 4 {
111 return Err(ColorParseError::BadRgb(inner.to_string()));
112 }
113 let r = parts[0]
114 .parse()
115 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
116 let g = parts[1]
117 .parse()
118 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
119 let b = parts[2]
120 .parse()
121 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
122 let a_f: f32 = parts[3]
123 .parse()
124 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
125 let a = (a_f.clamp(0.0, 1.0) * 255.0) as u8;
126 Ok(Color::rgba(r, g, b, a))
127}
128
129fn parse_named(s: &str) -> Result<Color, ColorParseError> {
130 match s.to_lowercase().as_str() {
131 "red" => Ok(Color::RED),
132 "green" => Ok(Color::GREEN),
133 "blue" => Ok(Color::BLUE),
134 "yellow" => Ok(Color::YELLOW),
135 "cyan" => Ok(Color::CYAN),
136 "magenta" => Ok(Color::MAGENTA),
137 "white" => Ok(Color::WHITE),
138 "black" => Ok(Color::BLACK),
139 "orange" => Ok(Color::ORANGE),
140 "purple" => Ok(Color::PURPLE),
141 "pink" => Ok(Color::PINK),
142 "gray" | "grey" => Ok(Color::GRAY),
143 "expected" | "success" => Ok(Color::GREEN),
144 "actual" | "error" => Ok(Color::RED),
145 "hint" => Ok(Color::YELLOW),
146 other => Err(ColorParseError::UnknownName(other.to_string())),
147 }
148}
149
150#[derive(Debug, Error)]
151pub enum ColorParseError {
152 #[error("unknown color name: {0}")]
153 UnknownName(String),
154 #[error("bad hex color: {0}")]
155 BadHex(String),
156 #[error("bad rgb/rgba color: {0}")]
157 BadRgb(String),
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn named() {
166 assert_eq!(Color::parse("red").unwrap(), Color::RED);
167 assert_eq!(Color::parse("EXPECTED").unwrap(), Color::GREEN);
168 }
169
170 #[test]
171 fn hex_short() {
172 assert_eq!(Color::parse("#FF0000").unwrap(), Color::RED);
173 assert_eq!(Color::parse("#00CC00").unwrap(), Color::rgb(0, 204, 0));
174 }
175
176 #[test]
177 fn hex_with_alpha() {
178 assert_eq!(
179 Color::parse("#FF000080").unwrap(),
180 Color::rgba(255, 0, 0, 128)
181 );
182 }
183
184 #[test]
185 fn rgb_fn() {
186 assert_eq!(Color::parse("rgb(255, 0, 0)").unwrap(), Color::RED);
187 }
188
189 #[test]
190 fn rgba_fn() {
191 let c = Color::parse("rgba(255, 0, 0, 0.5)").unwrap();
192 assert_eq!(c.r, 255);
193 assert_eq!(c.a, 127);
194 }
195
196 #[test]
197 fn unknown_name_errors() {
198 assert!(matches!(
199 Color::parse("neverheard"),
200 Err(ColorParseError::UnknownName(_))
201 ));
202 }
203}