1use std::fmt;
24
25use serde::{Deserialize, Serialize};
26
27#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[repr(transparent)]
50pub struct Color(u32);
51
52const _: () = assert!(std::mem::size_of::<Color>() == 4);
54
55impl Color {
56 pub const BLACK: Self = Self(0x00000000);
60 pub const WHITE: Self = Self(0x00FFFFFF);
62 pub const RED: Self = Self(0x000000FF);
64 pub const GREEN: Self = Self(0x0000FF00);
66 pub const BLUE: Self = Self(0x00FF0000);
68
69 pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
84 Self((b as u32) << 16 | (g as u32) << 8 | (r as u32))
85 }
86
87 pub const fn to_rgb(self) -> (u8, u8, u8) {
89 (self.red(), self.green(), self.blue())
90 }
91
92 pub const fn from_raw(bgr: u32) -> Self {
107 Self(bgr)
108 }
109
110 pub const fn to_raw(self) -> u32 {
112 self.0
113 }
114
115 pub const fn red(self) -> u8 {
117 (self.0 & 0xFF) as u8
118 }
119
120 pub const fn green(self) -> u8 {
122 ((self.0 >> 8) & 0xFF) as u8
123 }
124
125 pub const fn blue(self) -> u8 {
127 ((self.0 >> 16) & 0xFF) as u8
128 }
129
130 #[allow(clippy::wrong_self_convention)]
142 pub fn to_hex_rgb(&self) -> String {
143 format!("#{:02X}{:02X}{:02X}", self.red(), self.green(), self.blue())
144 }
145}
146
147impl Default for Color {
148 fn default() -> Self {
149 Self::BLACK
150 }
151}
152
153impl fmt::Debug for Color {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "Color(#{:02X}{:02X}{:02X})", self.red(), self.green(), self.blue())
156 }
157}
158
159impl fmt::Display for Color {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "#{:02X}{:02X}{:02X}", self.red(), self.green(), self.blue())
162 }
163}
164
165impl schemars::JsonSchema for Color {
166 fn schema_name() -> std::borrow::Cow<'static, str> {
167 "Color".into()
168 }
169
170 fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
171 gen.subschema_for::<u32>()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
185 fn color_pure_red_bgr_order() {
186 let c = Color::from_rgb(255, 0, 0);
187 assert_eq!(c.to_raw(), 0x000000FF);
188 assert_eq!(c.red(), 255);
189 assert_eq!(c.green(), 0);
190 assert_eq!(c.blue(), 0);
191 }
192
193 #[test]
195 fn color_pure_blue_bgr_order() {
196 let c = Color::from_rgb(0, 0, 255);
197 assert_eq!(c.to_raw(), 0x00FF0000);
198 assert_eq!(c.blue(), 255);
199 assert_eq!(c.red(), 0);
200 }
201
202 #[test]
204 fn color_black() {
205 let c = Color::from_rgb(0, 0, 0);
206 assert_eq!(c, Color::BLACK);
207 assert_eq!(c.to_raw(), 0x00000000);
208 }
209
210 #[test]
212 fn color_white() {
213 let c = Color::from_rgb(255, 255, 255);
214 assert_eq!(c, Color::WHITE);
215 assert_eq!(c.to_raw(), 0x00FFFFFF);
216 }
217
218 #[test]
220 fn color_rgb_roundtrip() {
221 let (r, g, b) = (0x11, 0x22, 0x33);
222 let c = Color::from_rgb(r, g, b);
223 assert_eq!(c.to_rgb(), (r, g, b));
224 }
225
226 #[test]
228 fn color_from_raw_u32_max() {
229 let c = Color::from_raw(u32::MAX);
230 assert_eq!(c.red(), 255);
231 assert_eq!(c.green(), 255);
232 assert_eq!(c.blue(), 255);
233 assert_eq!(c.to_raw(), u32::MAX);
234 }
235
236 #[test]
238 fn color_from_raw_zero() {
239 let c = Color::from_raw(0);
240 assert_eq!(c, Color::BLACK);
241 }
242
243 #[test]
245 fn color_named_constants() {
246 assert_eq!(Color::RED, Color::from_rgb(255, 0, 0));
247 assert_eq!(Color::GREEN, Color::from_rgb(0, 255, 0));
248 assert_eq!(Color::BLUE, Color::from_rgb(0, 0, 255));
249 assert_eq!(Color::BLACK, Color::from_rgb(0, 0, 0));
250 assert_eq!(Color::WHITE, Color::from_rgb(255, 255, 255));
251 }
252
253 #[test]
255 fn color_display_hex() {
256 let c = Color::from_rgb(0xAB, 0xCD, 0xEF);
257 assert_eq!(c.to_string(), "#ABCDEF");
258 }
259
260 #[test]
262 fn color_default_is_black() {
263 assert_eq!(Color::default(), Color::BLACK);
264 }
265
266 #[test]
269 fn color_to_hex_rgb_red() {
270 assert_eq!(Color::from_rgb(255, 0, 0).to_hex_rgb(), "#FF0000");
271 }
272
273 #[test]
274 fn color_to_hex_rgb_green() {
275 assert_eq!(Color::from_rgb(0, 255, 0).to_hex_rgb(), "#00FF00");
276 }
277
278 #[test]
279 fn color_to_hex_rgb_blue() {
280 assert_eq!(Color::from_rgb(0, 0, 255).to_hex_rgb(), "#0000FF");
281 }
282
283 #[test]
284 fn color_to_hex_rgb_black() {
285 assert_eq!(Color::from_rgb(0, 0, 0).to_hex_rgb(), "#000000");
286 }
287
288 #[test]
289 fn color_to_hex_rgb_white() {
290 assert_eq!(Color::from_rgb(255, 255, 255).to_hex_rgb(), "#FFFFFF");
291 }
292
293 #[test]
294 fn color_to_hex_rgb_arbitrary() {
295 assert_eq!(Color::from_rgb(18, 52, 86).to_hex_rgb(), "#123456");
296 }
297
298 #[test]
299 fn color_debug_format() {
300 let c = Color::from_rgb(0x11, 0x22, 0x33);
301 assert_eq!(format!("{c:?}"), "Color(#112233)");
302 }
303
304 #[test]
305 fn color_copy_and_hash() {
306 use std::collections::HashSet;
307 let c = Color::RED;
308 let c2 = c; assert_eq!(c, c2);
310
311 let mut set = HashSet::new();
312 set.insert(Color::RED);
313 set.insert(Color::GREEN);
314 set.insert(Color::RED); assert_eq!(set.len(), 2);
316 }
317
318 #[test]
319 fn color_serde_roundtrip() {
320 let c = Color::from_rgb(0xAA, 0xBB, 0xCC);
321 let json = serde_json::to_string(&c).unwrap();
322 let back: Color = serde_json::from_str(&json).unwrap();
323 assert_eq!(back, c);
324 }
325
326 #[test]
327 fn color_individual_components_isolated() {
328 let c = Color::from_rgb(0x01, 0x00, 0x00);
330 assert_eq!(c.red(), 1);
331 assert_eq!(c.green(), 0);
332 assert_eq!(c.blue(), 0);
333
334 let c = Color::from_rgb(0x00, 0x01, 0x00);
335 assert_eq!(c.red(), 0);
336 assert_eq!(c.green(), 1);
337 assert_eq!(c.blue(), 0);
338
339 let c = Color::from_rgb(0x00, 0x00, 0x01);
340 assert_eq!(c.red(), 0);
341 assert_eq!(c.green(), 0);
342 assert_eq!(c.blue(), 1);
343 }
344
345 use proptest::prelude::*;
350
351 proptest! {
352 #[test]
353 fn prop_color_rgb_roundtrip(r in 0u8..=255, g in 0u8..=255, b in 0u8..=255) {
354 let c = Color::from_rgb(r, g, b);
355 prop_assert_eq!(c.to_rgb(), (r, g, b));
356 }
357
358 #[test]
359 fn prop_color_raw_preserves_bits(raw in 0u32..=0x00FFFFFF) {
360 let c = Color::from_raw(raw);
361 prop_assert_eq!(c.to_raw(), raw);
362 let r = c.red() as u32;
364 let g = (c.green() as u32) << 8;
365 let b = (c.blue() as u32) << 16;
366 prop_assert_eq!(r | g | b, raw);
367 }
368 }
369}