use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(transparent)]
pub struct Color(u32);
const _: () = assert!(std::mem::size_of::<Color>() == 4);
impl Color {
pub const BLACK: Self = Self(0x00000000);
pub const WHITE: Self = Self(0x00FFFFFF);
pub const RED: Self = Self(0x000000FF);
pub const GREEN: Self = Self(0x0000FF00);
pub const BLUE: Self = Self(0x00FF0000);
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self((b as u32) << 16 | (g as u32) << 8 | (r as u32))
}
pub const fn to_rgb(self) -> (u8, u8, u8) {
(self.red(), self.green(), self.blue())
}
pub const fn from_raw(bgr: u32) -> Self {
Self(bgr)
}
pub const fn to_raw(self) -> u32 {
self.0
}
pub const fn red(self) -> u8 {
(self.0 & 0xFF) as u8
}
pub const fn green(self) -> u8 {
((self.0 >> 8) & 0xFF) as u8
}
pub const fn blue(self) -> u8 {
((self.0 >> 16) & 0xFF) as u8
}
#[allow(clippy::wrong_self_convention)]
pub fn to_hex_rgb(&self) -> String {
format!("#{:02X}{:02X}{:02X}", self.red(), self.green(), self.blue())
}
}
impl Default for Color {
fn default() -> Self {
Self::BLACK
}
}
impl fmt::Debug for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Color(#{:02X}{:02X}{:02X})", self.red(), self.green(), self.blue())
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.red(), self.green(), self.blue())
}
}
impl schemars::JsonSchema for Color {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Color".into()
}
fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
gen.subschema_for::<u32>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_pure_red_bgr_order() {
let c = Color::from_rgb(255, 0, 0);
assert_eq!(c.to_raw(), 0x000000FF);
assert_eq!(c.red(), 255);
assert_eq!(c.green(), 0);
assert_eq!(c.blue(), 0);
}
#[test]
fn color_pure_blue_bgr_order() {
let c = Color::from_rgb(0, 0, 255);
assert_eq!(c.to_raw(), 0x00FF0000);
assert_eq!(c.blue(), 255);
assert_eq!(c.red(), 0);
}
#[test]
fn color_black() {
let c = Color::from_rgb(0, 0, 0);
assert_eq!(c, Color::BLACK);
assert_eq!(c.to_raw(), 0x00000000);
}
#[test]
fn color_white() {
let c = Color::from_rgb(255, 255, 255);
assert_eq!(c, Color::WHITE);
assert_eq!(c.to_raw(), 0x00FFFFFF);
}
#[test]
fn color_rgb_roundtrip() {
let (r, g, b) = (0x11, 0x22, 0x33);
let c = Color::from_rgb(r, g, b);
assert_eq!(c.to_rgb(), (r, g, b));
}
#[test]
fn color_from_raw_u32_max() {
let c = Color::from_raw(u32::MAX);
assert_eq!(c.red(), 255);
assert_eq!(c.green(), 255);
assert_eq!(c.blue(), 255);
assert_eq!(c.to_raw(), u32::MAX);
}
#[test]
fn color_from_raw_zero() {
let c = Color::from_raw(0);
assert_eq!(c, Color::BLACK);
}
#[test]
fn color_named_constants() {
assert_eq!(Color::RED, Color::from_rgb(255, 0, 0));
assert_eq!(Color::GREEN, Color::from_rgb(0, 255, 0));
assert_eq!(Color::BLUE, Color::from_rgb(0, 0, 255));
assert_eq!(Color::BLACK, Color::from_rgb(0, 0, 0));
assert_eq!(Color::WHITE, Color::from_rgb(255, 255, 255));
}
#[test]
fn color_display_hex() {
let c = Color::from_rgb(0xAB, 0xCD, 0xEF);
assert_eq!(c.to_string(), "#ABCDEF");
}
#[test]
fn color_default_is_black() {
assert_eq!(Color::default(), Color::BLACK);
}
#[test]
fn color_to_hex_rgb_red() {
assert_eq!(Color::from_rgb(255, 0, 0).to_hex_rgb(), "#FF0000");
}
#[test]
fn color_to_hex_rgb_green() {
assert_eq!(Color::from_rgb(0, 255, 0).to_hex_rgb(), "#00FF00");
}
#[test]
fn color_to_hex_rgb_blue() {
assert_eq!(Color::from_rgb(0, 0, 255).to_hex_rgb(), "#0000FF");
}
#[test]
fn color_to_hex_rgb_black() {
assert_eq!(Color::from_rgb(0, 0, 0).to_hex_rgb(), "#000000");
}
#[test]
fn color_to_hex_rgb_white() {
assert_eq!(Color::from_rgb(255, 255, 255).to_hex_rgb(), "#FFFFFF");
}
#[test]
fn color_to_hex_rgb_arbitrary() {
assert_eq!(Color::from_rgb(18, 52, 86).to_hex_rgb(), "#123456");
}
#[test]
fn color_debug_format() {
let c = Color::from_rgb(0x11, 0x22, 0x33);
assert_eq!(format!("{c:?}"), "Color(#112233)");
}
#[test]
fn color_copy_and_hash() {
use std::collections::HashSet;
let c = Color::RED;
let c2 = c; assert_eq!(c, c2);
let mut set = HashSet::new();
set.insert(Color::RED);
set.insert(Color::GREEN);
set.insert(Color::RED); assert_eq!(set.len(), 2);
}
#[test]
fn color_serde_roundtrip() {
let c = Color::from_rgb(0xAA, 0xBB, 0xCC);
let json = serde_json::to_string(&c).unwrap();
let back: Color = serde_json::from_str(&json).unwrap();
assert_eq!(back, c);
}
#[test]
fn color_individual_components_isolated() {
let c = Color::from_rgb(0x01, 0x00, 0x00);
assert_eq!(c.red(), 1);
assert_eq!(c.green(), 0);
assert_eq!(c.blue(), 0);
let c = Color::from_rgb(0x00, 0x01, 0x00);
assert_eq!(c.red(), 0);
assert_eq!(c.green(), 1);
assert_eq!(c.blue(), 0);
let c = Color::from_rgb(0x00, 0x00, 0x01);
assert_eq!(c.red(), 0);
assert_eq!(c.green(), 0);
assert_eq!(c.blue(), 1);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_color_rgb_roundtrip(r in 0u8..=255, g in 0u8..=255, b in 0u8..=255) {
let c = Color::from_rgb(r, g, b);
prop_assert_eq!(c.to_rgb(), (r, g, b));
}
#[test]
fn prop_color_raw_preserves_bits(raw in 0u32..=0x00FFFFFF) {
let c = Color::from_raw(raw);
prop_assert_eq!(c.to_raw(), raw);
let r = c.red() as u32;
let g = (c.green() as u32) << 8;
let b = (c.blue() as u32) << 16;
prop_assert_eq!(r | g | b, raw);
}
}
}