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