1use serde::de;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
42pub struct Rgba {
43 pub r: u8,
45 pub g: u8,
47 pub b: u8,
49 pub a: u8,
51}
52
53impl Rgba {
54 #[must_use]
56 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
57 Self { r, g, b, a: 255 }
58 }
59
60 #[must_use]
62 #[allow(clippy::self_named_constructors)]
63 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
64 Self { r, g, b, a }
65 }
66
67 #[must_use]
71 pub fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
72 Self {
73 r: (r.clamp(0.0, 1.0) * 255.0).round() as u8,
74 g: (g.clamp(0.0, 1.0) * 255.0).round() as u8,
75 b: (b.clamp(0.0, 1.0) * 255.0).round() as u8,
76 a: (a.clamp(0.0, 1.0) * 255.0).round() as u8,
77 }
78 }
79
80 #[must_use]
82 pub fn to_f32_array(&self) -> [f32; 4] {
83 [
84 self.r as f32 / 255.0,
85 self.g as f32 / 255.0,
86 self.b as f32 / 255.0,
87 self.a as f32 / 255.0,
88 ]
89 }
90
91 #[must_use]
93 pub fn to_f32_tuple(&self) -> (f32, f32, f32, f32) {
94 (
95 self.r as f32 / 255.0,
96 self.g as f32 / 255.0,
97 self.b as f32 / 255.0,
98 self.a as f32 / 255.0,
99 )
100 }
101}
102
103impl fmt::Display for Rgba {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 if self.a == 255 {
106 write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
107 } else {
108 write!(
109 f,
110 "#{:02x}{:02x}{:02x}{:02x}",
111 self.r, self.g, self.b, self.a
112 )
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
123pub struct ParseColorError(String);
124
125impl fmt::Display for ParseColorError {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.write_str(&self.0)
128 }
129}
130
131impl std::error::Error for ParseColorError {}
132
133impl FromStr for Rgba {
134 type Err = ParseColorError;
135
136 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 let hex = s.strip_prefix('#').unwrap_or(s);
138
139 if hex.is_empty() {
140 return Err(ParseColorError("empty hex color string".into()));
141 }
142
143 match hex.len() {
144 3 => {
146 let r = u8::from_str_radix(&hex[0..1], 16)
147 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
148 let g = u8::from_str_radix(&hex[1..2], 16)
149 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
150 let b = u8::from_str_radix(&hex[2..3], 16)
151 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
152 Ok(Rgba::rgb(r * 17, g * 17, b * 17))
153 }
154 4 => {
156 let r = u8::from_str_radix(&hex[0..1], 16)
157 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
158 let g = u8::from_str_radix(&hex[1..2], 16)
159 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
160 let b = u8::from_str_radix(&hex[2..3], 16)
161 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
162 let a = u8::from_str_radix(&hex[3..4], 16)
163 .map_err(|e| ParseColorError(format!("invalid alpha component: {e}")))?;
164 Ok(Rgba::rgba(r * 17, g * 17, b * 17, a * 17))
165 }
166 6 => {
168 let r = u8::from_str_radix(&hex[0..2], 16)
169 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
170 let g = u8::from_str_radix(&hex[2..4], 16)
171 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
172 let b = u8::from_str_radix(&hex[4..6], 16)
173 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
174 Ok(Rgba::rgb(r, g, b))
175 }
176 8 => {
178 let r = u8::from_str_radix(&hex[0..2], 16)
179 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
180 let g = u8::from_str_radix(&hex[2..4], 16)
181 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
182 let b = u8::from_str_radix(&hex[4..6], 16)
183 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
184 let a = u8::from_str_radix(&hex[6..8], 16)
185 .map_err(|e| ParseColorError(format!("invalid alpha component: {e}")))?;
186 Ok(Rgba::rgba(r, g, b, a))
187 }
188 other => Err(ParseColorError(format!(
189 "invalid hex color length {other}: expected 3, 4, 6, or 8 hex digits"
190 ))),
191 }
192 }
193}
194
195impl Serialize for Rgba {
196 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
197 serializer.serialize_str(&self.to_string())
198 }
199}
200
201impl<'de> Deserialize<'de> for Rgba {
202 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
203 let s = String::deserialize(deserializer)?;
204 Rgba::from_str(&s).map_err(de::Error::custom)
205 }
206}
207
208#[cfg(test)]
209#[allow(clippy::unwrap_used, clippy::expect_used)]
210mod tests {
211 use super::*;
212
213 #[test]
216 fn rgb_constructor_sets_alpha_255() {
217 let c = Rgba::rgb(61, 174, 233);
218 assert_eq!(
219 c,
220 Rgba {
221 r: 61,
222 g: 174,
223 b: 233,
224 a: 255
225 }
226 );
227 }
228
229 #[test]
230 fn rgba_constructor_sets_all_fields() {
231 let c = Rgba::rgba(61, 174, 233, 128);
232 assert_eq!(
233 c,
234 Rgba {
235 r: 61,
236 g: 174,
237 b: 233,
238 a: 128
239 }
240 );
241 }
242
243 #[test]
246 fn parse_6_digit_hex_with_hash() {
247 let c: Rgba = "#3daee9".parse().unwrap();
248 assert_eq!(c, Rgba::rgb(61, 174, 233));
249 }
250
251 #[test]
252 fn parse_8_digit_hex_with_hash() {
253 let c: Rgba = "#3daee980".parse().unwrap();
254 assert_eq!(c, Rgba::rgba(61, 174, 233, 128));
255 }
256
257 #[test]
258 fn parse_6_digit_hex_without_hash() {
259 let c: Rgba = "3daee9".parse().unwrap();
260 assert_eq!(c, Rgba::rgb(61, 174, 233));
261 }
262
263 #[test]
264 fn parse_3_digit_shorthand() {
265 let c: Rgba = "#abc".parse().unwrap();
266 assert_eq!(c, Rgba::rgb(0xaa, 0xbb, 0xcc));
267 }
268
269 #[test]
270 fn parse_4_digit_shorthand() {
271 let c: Rgba = "#abcd".parse().unwrap();
272 assert_eq!(c, Rgba::rgba(0xaa, 0xbb, 0xcc, 0xdd));
273 }
274
275 #[test]
276 fn parse_uppercase_hex() {
277 let c: Rgba = "#AABBCC".parse().unwrap();
278 assert_eq!(c, Rgba::rgb(0xaa, 0xbb, 0xcc));
279 }
280
281 #[test]
282 fn parse_empty_string_is_error() {
283 assert!("".parse::<Rgba>().is_err());
284 }
285
286 #[test]
287 fn parse_invalid_hex_chars_is_error() {
288 assert!("#gggggg".parse::<Rgba>().is_err());
289 }
290
291 #[test]
292 fn parse_invalid_length_5_chars_is_error() {
293 assert!("#12345".parse::<Rgba>().is_err());
294 }
295
296 #[test]
299 fn display_omits_alpha_when_255() {
300 assert_eq!(Rgba::rgb(61, 174, 233).to_string(), "#3daee9");
301 }
302
303 #[test]
304 fn display_includes_alpha_when_not_255() {
305 assert_eq!(Rgba::rgba(61, 174, 233, 128).to_string(), "#3daee980");
306 }
307
308 #[test]
311 fn serde_json_round_trip() {
312 let c = Rgba::rgb(61, 174, 233);
313 let json = serde_json::to_string(&c).unwrap();
314 assert_eq!(json, "\"#3daee9\"");
315 let deserialized: Rgba = serde_json::from_str(&json).unwrap();
316 assert_eq!(deserialized, c);
317 }
318
319 #[test]
320 fn serde_toml_round_trip() {
321 #[derive(Debug, PartialEq, Serialize, Deserialize)]
322 struct Wrapper {
323 color: Rgba,
324 }
325 let w = Wrapper {
326 color: Rgba::rgba(61, 174, 233, 128),
327 };
328 let toml_str = toml::to_string(&w).unwrap();
329 let deserialized: Wrapper = toml::from_str(&toml_str).unwrap();
330 assert_eq!(deserialized, w);
331 }
332
333 #[test]
336 fn to_f32_array_black() {
337 let arr = Rgba::rgb(0, 0, 0).to_f32_array();
338 assert_eq!(arr, [0.0, 0.0, 0.0, 1.0]);
339 }
340
341 #[test]
342 fn to_f32_array_white_transparent() {
343 let arr = Rgba::rgba(255, 255, 255, 0).to_f32_array();
344 assert_eq!(arr, [1.0, 1.0, 1.0, 0.0]);
345 }
346
347 #[test]
350 fn rgba_is_copy() {
351 let a = Rgba::rgb(1, 2, 3);
352 let b = a; assert_eq!(a, b); }
355
356 #[test]
357 fn rgba_default_is_transparent_black() {
358 let d = Rgba::default();
359 assert_eq!(
360 d,
361 Rgba {
362 r: 0,
363 g: 0,
364 b: 0,
365 a: 0
366 }
367 );
368 }
369
370 #[test]
371 fn rgba_is_hash() {
372 use std::collections::HashSet;
373 let mut set = HashSet::new();
374 set.insert(Rgba::rgb(1, 2, 3));
375 assert!(set.contains(&Rgba::rgb(1, 2, 3)));
376 }
377
378 #[test]
381 fn from_f32_basic() {
382 let c = Rgba::from_f32(1.0, 0.5, 0.0, 1.0);
383 assert_eq!(c.r, 255);
384 assert_eq!(c.g, 128); assert_eq!(c.b, 0);
386 assert_eq!(c.a, 255);
387 }
388
389 #[test]
390 fn from_f32_clamps_out_of_range() {
391 let c = Rgba::from_f32(-0.5, 1.5, 0.0, 0.0);
392 assert_eq!(c.r, 0);
393 assert_eq!(c.g, 255);
394 }
395
396 #[test]
399 fn to_f32_tuple_matches_array() {
400 let c = Rgba::rgb(128, 64, 32);
401 let arr = c.to_f32_array();
402 let tup = c.to_f32_tuple();
403 assert_eq!(tup, (arr[0], arr[1], arr[2], arr[3]));
404 }
405}