1use crate::error::{IconFontError, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct Color {
10 pub r: u8,
11 pub g: u8,
12 pub b: u8,
13 pub a: u8,
14}
15
16impl Color {
17 #[inline]
18 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
19 Self { r, g, b, a: 255 }
20 }
21
22 #[inline]
23 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
24 Self { r, g, b, a }
25 }
26
27 #[inline]
28 pub const fn transparent() -> Self {
29 Self {
30 r: 0,
31 g: 0,
32 b: 0,
33 a: 0,
34 }
35 }
36
37 #[inline]
38 pub const fn black() -> Self {
39 Self::rgb(0, 0, 0)
40 }
41
42 #[inline]
43 pub const fn white() -> Self {
44 Self::rgb(255, 255, 255)
45 }
46
47 pub fn from_hex(hex: &str) -> Result<Self> {
58 let hex = hex.strip_prefix('#').unwrap_or(hex);
59
60 match hex.len() {
61 3 => {
62 let r = parse_hex_digit(hex.chars().next().unwrap())?;
63 let g = parse_hex_digit(hex.chars().nth(1).unwrap())?;
64 let b = parse_hex_digit(hex.chars().nth(2).unwrap())?;
65 Ok(Self::rgb(r * 17, g * 17, b * 17))
66 }
67 4 => {
68 let r = parse_hex_digit(hex.chars().next().unwrap())?;
69 let g = parse_hex_digit(hex.chars().nth(1).unwrap())?;
70 let b = parse_hex_digit(hex.chars().nth(2).unwrap())?;
71 let a = parse_hex_digit(hex.chars().nth(3).unwrap())?;
72 Ok(Self::rgba(r * 17, g * 17, b * 17, a * 17))
73 }
74 6 => {
75 let r = u8::from_str_radix(&hex[0..2], 16)
76 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
77 let g = u8::from_str_radix(&hex[2..4], 16)
78 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
79 let b = u8::from_str_radix(&hex[4..6], 16)
80 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
81 Ok(Self::rgb(r, g, b))
82 }
83 8 => {
84 let r = u8::from_str_radix(&hex[0..2], 16)
85 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
86 let g = u8::from_str_radix(&hex[2..4], 16)
87 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
88 let b = u8::from_str_radix(&hex[4..6], 16)
89 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
90 let a = u8::from_str_radix(&hex[6..8], 16)
91 .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
92 Ok(Self::rgba(r, g, b, a))
93 }
94 _ => Err(IconFontError::InvalidColor(format!(
95 "Invalid hex length: {}. Expected 3, 4, 6, or 8 characters.",
96 hex.len()
97 ))),
98 }
99 }
100
101 #[inline]
102 pub const fn to_rgba(&self) -> [u8; 4] {
103 [self.r, self.g, self.b, self.a]
104 }
105
106 #[inline]
107 pub const fn is_transparent(&self) -> bool {
108 self.a == 0
109 }
110}
111
112impl Default for Color {
113 fn default() -> Self {
114 Self::black()
115 }
116}
117
118fn parse_hex_digit(c: char) -> Result<u8> {
119 match c {
120 '0'..='9' => Ok(c as u8 - b'0'),
121 'a'..='f' => Ok(c as u8 - b'a' + 10),
122 'A'..='F' => Ok(c as u8 - b'A' + 10),
123 _ => Err(IconFontError::InvalidColor(format!(
124 "Invalid hex digit: {}",
125 c
126 ))),
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_hex_parsing() {
136 assert_eq!(Color::from_hex("#FF0000").unwrap(), Color::rgb(255, 0, 0));
137 assert_eq!(Color::from_hex("00FF00").unwrap(), Color::rgb(0, 255, 0));
138 assert_eq!(Color::from_hex("#F00").unwrap(), Color::rgb(255, 0, 0));
139 assert_eq!(
140 Color::from_hex("#FF000080").unwrap(),
141 Color::rgba(255, 0, 0, 128)
142 );
143 }
144
145 #[test]
146 fn test_rgb_constructor() {
147 let c = Color::rgb(100, 150, 200);
148 assert_eq!(c.r, 100);
149 assert_eq!(c.g, 150);
150 assert_eq!(c.b, 200);
151 assert_eq!(c.a, 255);
152 }
153}