1use std::fmt;
4use std::str::FromStr;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct ThemeColor {
12 pub r: u8,
13 pub g: u8,
14 pub b: u8,
15}
16
17impl ThemeColor {
18 #[must_use]
20 pub const fn new(r: u8, g: u8, b: u8) -> Self {
21 Self { r, g, b }
22 }
23
24 pub fn from_hex(hex: &str) -> Result<Self, ColorParseError> {
29 let hex = hex.trim_start_matches('#');
30
31 if hex.len() != 6 {
32 return Err(ColorParseError::InvalidLength(hex.len()));
33 }
34
35 let r = u8::from_str_radix(&hex[0..2], 16)
36 .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
37 let g = u8::from_str_radix(&hex[2..4], 16)
38 .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
39 let b = u8::from_str_radix(&hex[4..6], 16)
40 .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
41
42 Ok(Self { r, g, b })
43 }
44
45 #[must_use]
47 pub fn to_hex(&self) -> String {
48 format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
49 }
50
51 #[must_use]
53 pub const fn to_rgb_tuple(&self) -> (u8, u8, u8) {
54 (self.r, self.g, self.b)
55 }
56
57 #[must_use]
61 #[allow(
62 clippy::cast_possible_truncation,
63 clippy::cast_sign_loss,
64 clippy::as_conversions
65 )]
66 pub fn lerp(&self, other: &Self, t: f32) -> Self {
67 let t = t.clamp(0.0, 1.0);
68 Self {
69 r: (f32::from(self.r) + (f32::from(other.r) - f32::from(self.r)) * t) as u8,
70 g: (f32::from(self.g) + (f32::from(other.g) - f32::from(self.g)) * t) as u8,
71 b: (f32::from(self.b) + (f32::from(other.b) - f32::from(self.b)) * t) as u8,
72 }
73 }
74
75 pub const FALLBACK: Self = Self::new(128, 128, 128);
78}
79
80impl Default for ThemeColor {
81 fn default() -> Self {
82 Self::FALLBACK
83 }
84}
85
86impl fmt::Display for ThemeColor {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(f, "{}", self.to_hex())
89 }
90}
91
92impl FromStr for ThemeColor {
93 type Err = ColorParseError;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 Self::from_hex(s)
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum ColorParseError {
103 InvalidLength(usize),
105 InvalidHex(String),
107}
108
109impl fmt::Display for ColorParseError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Self::InvalidLength(len) => {
113 write!(f, "invalid hex color length: {len} (expected 6)")
114 }
115 Self::InvalidHex(hex) => {
116 write!(f, "invalid hex color: {hex}")
117 }
118 }
119 }
120}
121
122impl std::error::Error for ColorParseError {}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_from_hex() {
130 assert_eq!(
131 ThemeColor::from_hex("#e135ff").unwrap(),
132 ThemeColor::new(225, 53, 255)
133 );
134 assert_eq!(
135 ThemeColor::from_hex("80ffea").unwrap(),
136 ThemeColor::new(128, 255, 234)
137 );
138 }
139
140 #[test]
141 fn test_to_hex() {
142 assert_eq!(ThemeColor::new(225, 53, 255).to_hex(), "#e135ff");
143 }
144
145 #[test]
146 fn test_lerp() {
147 let black = ThemeColor::new(0, 0, 0);
148 let white = ThemeColor::new(255, 255, 255);
149 let mid = black.lerp(&white, 0.5);
150 assert_eq!(mid, ThemeColor::new(127, 127, 127));
151 }
152}