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