1use palette::{rgb::Rgb, FromColor, GetHue, Hsl, IntoColor};
2use serde::{Deserialize, Serialize, Serializer};
3use std::fmt;
4use std::str::FromStr;
5
6use crate::error::TintedBuilderError;
7
8#[derive(Debug, Clone, Deserialize)]
14pub struct Color {
15 pub hex: (String, String, String),
16 pub rgb: (u8, u8, u8),
17 pub dec: (f32, f32, f32),
18 pub name: ColorName,
19 pub variant: ColorVariant,
20}
21
22impl Color {
23 pub fn new(
39 hex_color: &str,
40 name: Option<ColorName>,
41 variant: Option<ColorVariant>,
42 ) -> Result<Self, TintedBuilderError> {
43 let hex_full = process_hex_input(hex_color).ok_or(TintedBuilderError::HexInputFormat)?;
44 let hex: (String, String, String) = (
45 hex_full[0..2].to_lowercase(),
46 hex_full[2..4].to_lowercase(),
47 hex_full[4..6].to_lowercase(),
48 );
49 let rgb = hex_to_rgb(&hex)?;
50 let inv_255: f32 = 1.0 / 255.0;
52 let dec: (f32, f32, f32) = (
53 f32::from(rgb.0) * inv_255,
54 f32::from(rgb.1) * inv_255,
55 f32::from(rgb.2) * inv_255,
56 );
57
58 Ok(Self {
59 hex,
60 rgb,
61 dec,
62 name: name.unwrap_or(ColorName::Other),
63 variant: variant.unwrap_or(ColorVariant::Normal),
64 })
65 }
66
67 #[must_use]
68 pub fn to_hex(&self) -> String {
70 format!("{}{}{}", &self.hex.0, &self.hex.1, &self.hex.2)
71 }
72
73 #[allow(
74 clippy::cast_possible_truncation,
75 clippy::cast_sign_loss,
76 clippy::missing_errors_doc
77 )]
78 pub fn try_to_variant(&self, color_variant: &ColorVariant) -> Result<Self, TintedBuilderError> {
84 let rgb = Rgb::new(self.rgb.0, self.rgb.1, self.rgb.2);
85 let hsl: Hsl = Hsl::from_color(rgb.into_format::<f32>());
86 let updated_hsl = adjust_normal_hsl_for_variant(hsl, color_variant);
87 let updated_rgb: Rgb = updated_hsl.into_color();
88 let updated_rgb_r: u8 = (updated_rgb.red.clamp(0.0, 1.0) * 255.0).round() as u8;
89 let updated_rgb_g: u8 = (updated_rgb.green.clamp(0.0, 1.0) * 255.0).round() as u8;
90 let updated_rgb_b: u8 = (updated_rgb.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
91 let updated_hex = format!("{updated_rgb_r:02X}{updated_rgb_g:02X}{updated_rgb_b:02X}");
92
93 Self::new(
94 &updated_hex,
95 Some(self.name.clone()),
96 Some(color_variant.clone()),
97 )
98 }
99
100 #[allow(
101 clippy::missing_errors_doc,
102 clippy::cast_possible_truncation,
103 clippy::cast_sign_loss
104 )]
105 pub fn try_to_color(&self, target_color_name: &ColorName) -> Result<Self, TintedBuilderError> {
111 let from_target_color_name = &self.name.clone();
112 let to_target_color_name = target_color_name.clone();
113 let to_color_variant = &self.variant.clone();
114
115 match (&from_target_color_name, &to_target_color_name) {
116 (ColorName::Yellow, ColorName::Orange) => {
117 let from_rgb = Rgb::new(self.rgb.0, self.rgb.1, self.rgb.2);
118 let from_hsl: Hsl = Hsl::from_color(from_rgb.into_format::<f32>());
119 let from_hsl_h = from_hsl.get_hue().into_degrees();
120 let from_hsl_s = from_hsl.saturation;
121 let from_hsl_l = from_hsl.lightness;
122 let h_prime = (from_hsl_h - 10.0 + 360.0) % 360.0;
124 let to_hsl: Hsl = Hsl::new(h_prime, from_hsl_s, from_hsl_l);
125 let to_rgb: Rgb = to_hsl.into_color();
126 let [to_rgb_r, to_rgb_g, to_rgb_b]: [u8; 3] =
127 [to_rgb.red, to_rgb.green, to_rgb.blue]
128 .map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8);
129 let to_hex = format!("{to_rgb_r:02X}{to_rgb_g:02X}{to_rgb_b:02X}");
130
131 Self::new(
132 &to_hex,
133 Some(to_target_color_name.clone()),
134 Some(to_color_variant.clone()),
135 )
136 }
137 (ColorName::Yellow, ColorName::Brown) => {
138 let from_rgb = Rgb::new(self.rgb.0, self.rgb.1, self.rgb.2);
139 let from_hsl: Hsl = Hsl::from_color(from_rgb.into_format::<f32>());
140 let from_hsl_h = from_hsl.get_hue().into_degrees();
141 let from_hsl_s = from_hsl.saturation;
142 let from_hsl_l = from_hsl.lightness;
143 let h_difference = 15.0;
144 let l_difference = 0.3;
145 let s_perc_difference = 0.65;
146 let s_prime = (from_hsl_s * s_perc_difference).clamp(0.0, 1.0);
148 let l_prime = (from_hsl_l - l_difference).clamp(0.0, 1.0);
149 let to_hsl: Hsl = Hsl::new(from_hsl_h - h_difference, s_prime, l_prime);
150 let to_rgb: Rgb = to_hsl.into_color();
151 let [to_rgb_r, to_rgb_g, to_rgb_b]: [u8; 3] =
152 [to_rgb.red, to_rgb.green, to_rgb.blue]
153 .map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8);
154 let to_hex = format!("{to_rgb_r:02X}{to_rgb_g:02X}{to_rgb_b:02X}");
155
156 Self::new(
157 &to_hex,
158 Some(to_target_color_name.clone()),
159 Some(to_color_variant.clone()),
160 )
161 }
162 _ => Err(TintedBuilderError::UnsupportedColorDerivation {
163 from_color: self.name.to_string(),
164 target: target_color_name.to_string(),
165 supported_derivations: "yellow→orange, yellow→brown".to_string(),
166 }),
167 }
168 }
169}
170
171impl fmt::Display for Color {
172 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173 write!(f, "#{}", &self.to_hex())
174 }
175}
176
177#[derive(Clone, Debug, Deserialize, Serialize)]
179#[non_exhaustive]
180pub enum ColorVariant {
181 Dim,
182 Normal,
183 Bright,
184}
185
186impl fmt::Display for ColorVariant {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::Dim => write!(f, "dim"),
190 Self::Normal => write!(f, "normal"),
191 Self::Bright => write!(f, "bright"),
192 }
193 }
194}
195
196impl FromStr for ColorVariant {
197 type Err = TintedBuilderError;
198
199 fn from_str(variant_str: &str) -> Result<Self, Self::Err> {
206 match variant_str {
207 "dim" => Ok(Self::Dim),
208 "normal" => Ok(Self::Normal),
209 "bright" => Ok(Self::Bright),
210 _ => Err(TintedBuilderError::InvalidColorVariant(
211 variant_str.to_string(),
212 )),
213 }
214 }
215}
216
217impl ColorVariant {
218 #[must_use]
219 pub const fn get_list<'a>() -> &'a [Self] {
220 &[Self::Dim, Self::Normal, Self::Bright]
221 }
222}
223
224#[derive(Clone, Debug, Deserialize, Serialize)]
226#[non_exhaustive]
227pub enum ColorName {
228 Black,
229 Red,
230 Green,
231 Yellow,
232 Blue,
233 Magenta,
234 Cyan,
235 White,
236 Orange,
237 Gray,
238 Brown,
239 Other, }
241
242impl fmt::Display for ColorName {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 Self::Black => write!(f, "black"),
246 Self::Red => write!(f, "red"),
247 Self::Green => write!(f, "green"),
248 Self::Yellow => write!(f, "yellow"),
249 Self::Blue => write!(f, "blue"),
250 Self::Magenta => write!(f, "magenta"),
251 Self::Cyan => write!(f, "cyan"),
252 Self::White => write!(f, "white"),
253 Self::Orange => write!(f, "orange"),
254 Self::Gray => write!(f, "gray"),
255 Self::Brown => write!(f, "brown"),
256 Self::Other => write!(f, "other"),
257 }
258 }
259}
260
261impl ColorName {
262 #[must_use]
263 pub const fn get_list<'a>() -> &'a [Self] {
264 &[
265 Self::Black,
266 Self::Red,
267 Self::Green,
268 Self::Yellow,
269 Self::Blue,
270 Self::Magenta,
271 Self::Cyan,
272 Self::White,
273 Self::Orange,
274 Self::Gray,
275 Self::Brown,
276 ]
277 }
278}
279
280pub struct ColorType(pub ColorName, pub ColorVariant);
281
282impl FromStr for ColorName {
283 type Err = TintedBuilderError;
284
285 fn from_str(name_str: &str) -> Result<Self, Self::Err> {
292 match name_str {
293 "black" => Ok(Self::Black),
294 "red" => Ok(Self::Red),
295 "green" => Ok(Self::Green),
296 "yellow" => Ok(Self::Yellow),
297 "blue" => Ok(Self::Blue),
298 "magenta" => Ok(Self::Magenta),
299 "cyan" => Ok(Self::Cyan),
300 "white" => Ok(Self::White),
301 "orange" => Ok(Self::Orange),
302 "gray" => Ok(Self::Gray),
303 "brown" => Ok(Self::Brown),
304 "other" => Ok(Self::Other),
305 _ => Err(TintedBuilderError::InvalidColorName(name_str.to_string())),
306 }
307 }
308}
309
310impl FromStr for ColorType {
311 type Err = TintedBuilderError;
312
313 fn from_str(color_str: &str) -> Result<Self, Self::Err> {
320 let trimmed = color_str.trim();
321 let lower = trimmed.to_lowercase();
322
323 let (name, variant) = lower
324 .split_once('_')
325 .or_else(|| lower.split_once('-'))
326 .ok_or_else(|| TintedBuilderError::InvalidColorType(trimmed.to_string()))?;
327
328 Ok(Self(
329 ColorName::from_str(name)?,
330 ColorVariant::from_str(variant)?,
331 ))
332 }
333}
334
335fn hex_to_rgb(hex: &(String, String, String)) -> Result<(u8, u8, u8), TintedBuilderError> {
337 let r = u8::from_str_radix(hex.0.as_str(), 16)?;
338 let g = u8::from_str_radix(hex.1.as_str(), 16)?;
339 let b = u8::from_str_radix(hex.2.as_str(), 16)?;
340
341 Ok((r, g, b))
342}
343
344fn process_hex_input(input: &str) -> Option<String> {
346 let hex_str = input.strip_prefix('#').unwrap_or(input);
348
349 match hex_str.len() {
350 3 => {
352 if hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
353 Some(
354 hex_str
355 .chars()
356 .flat_map(|c| std::iter::repeat(c).take(2))
357 .collect(),
358 )
359 } else {
360 None }
362 }
363 6 => {
365 if hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
366 Some(hex_str.to_string())
367 } else {
368 None }
370 }
371 _ => None,
373 }
374}
375
376const DL: f32 = 0.12;
377
378fn adjust_normal_hsl_for_variant(hsl: Hsl, color_variant: &ColorVariant) -> Hsl {
380 let mut updated_s = hsl.saturation;
381 let mut updated_l = hsl.lightness;
382
383 match color_variant {
384 ColorVariant::Dim => {
385 let k: f32 = if hsl.lightness < 0.4 {
386 1.04
387 } else if hsl.lightness < 0.7 {
388 1.07
389 } else {
390 1.1
391 };
392 let delta_l = DL.min(hsl.lightness);
393
394 updated_l = (hsl.lightness - delta_l).clamp(0.0, 1.0);
395 updated_s = (hsl.saturation * k).clamp(0.0, 1.0);
396 }
397 ColorVariant::Bright => {
398 let k: f32 = if hsl.lightness < 0.5 {
399 1.08
400 } else if hsl.lightness < 0.8 {
401 1.00
402 } else {
403 0.9
404 };
405 let delta_l = DL.min(1.0 - hsl.lightness);
406
407 updated_l = (hsl.lightness + delta_l).clamp(0.0, 1.0);
408 updated_s = (hsl.saturation * k).clamp(0.0, 1.0);
409 }
410 _ => {}
411 }
412
413 Hsl::new(hsl.hue, updated_s, updated_l)
414}
415
416#[derive(Serialize)]
417struct RgbSer {
418 r: u8,
419 g: u8,
420 b: u8,
421}
422#[derive(Serialize)]
423struct Rgb16Ser {
424 r: u16,
425 g: u16,
426 b: u16,
427}
428#[derive(Serialize)]
429struct DecSer {
430 r: String,
431 g: String,
432 b: String,
433}
434
435impl Serialize for Color {
436 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
437 where
438 S: Serializer,
439 {
440 use serde::ser::SerializeMap;
441
442 let mut map = serializer.serialize_map(Some(8))?;
443 map.serialize_entry("hex", &self.to_hex())?;
444 map.serialize_entry("hex-r", &self.hex.0)?;
445 map.serialize_entry("hex-g", &self.hex.1)?;
446 map.serialize_entry("hex-b", &self.hex.2)?;
447 let hex_bgr = format!("{}{}{}", self.hex.2, self.hex.1, self.hex.0);
448 map.serialize_entry("hex-bgr", &hex_bgr)?;
449
450 let rgb = RgbSer {
451 r: self.rgb.0,
452 g: self.rgb.1,
453 b: self.rgb.2,
454 };
455 map.serialize_entry("rgb", &rgb)?;
456
457 let rgb16 = Rgb16Ser {
458 r: u16::from(self.rgb.0) * 257,
459 g: u16::from(self.rgb.1) * 257,
460 b: u16::from(self.rgb.2) * 257,
461 };
462 map.serialize_entry("rgb16", &rgb16)?;
463
464 let dec = DecSer {
465 r: format!("{:.8}", f64::from(self.dec.0)),
466 g: format!("{:.8}", f64::from(self.dec.1)),
467 b: format!("{:.8}", f64::from(self.dec.2)),
468 };
469 map.serialize_entry("dec", &dec)?;
470
471 map.end()
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn serializes_to_color_object() {
481 let color = Color::new("#AABBCC", Some(ColorName::Blue), Some(ColorVariant::Normal))
482 .expect("unable to create new color");
483 let yaml = serde_yaml::to_string(&color).expect("unable to serialize color");
484 assert!(yaml.contains("hex: aabbcc"));
485 assert!(yaml.contains("hex-r: aa"));
486 assert!(yaml.contains("hex-g: bb"));
487 assert!(yaml.contains("hex-b: cc"));
488 assert!(yaml.contains("hex-bgr: ccbbaa"));
489 assert!(yaml.contains(
490 "rgb:
491 r: 170
492 g: 187
493 b: 204"
494 ));
495 assert!(yaml.contains(
496 "rgb16:
497 r: 43690
498 g: 48059
499 b: 52428"
500 ));
501 assert!(yaml.contains(
502 "dec:
503 r: '0.66666669'
504 g: '0.73333335'
505 b: '0.80000007'"
506 ));
507 }
508
509 #[test]
510 fn color_object_field_types() {
511 let color = Color::new("#112233", Some(ColorName::Blue), Some(ColorVariant::Normal))
512 .expect("unable to create color");
513 let val = serde_yaml::to_value(&color).expect("unable to deserialize color");
514 let map = val.as_mapping().expect("unable to create mapping");
515 assert!(map
517 .get(serde_yaml::Value::String("hex".into()))
518 .expect("unable to get 'hex' property")
519 .as_str()
520 .is_some());
521 let rgb = map
523 .get(serde_yaml::Value::String("rgb".into()))
524 .expect("unable to get 'rgb' property")
525 .as_mapping()
526 .expect("unable to create mapping");
527 assert!(rgb
528 .get(serde_yaml::Value::String("r".into()))
529 .expect("unable to get 'rgb.r' property")
530 .as_i64()
531 .is_some());
532 let rgb16 = map
534 .get(serde_yaml::Value::String("rgb16".into()))
535 .expect("unable to get 'rgb16' property")
536 .as_mapping()
537 .expect("unable to create mapping");
538 assert!(rgb16
539 .get(serde_yaml::Value::String("r".into()))
540 .expect("unable to get 'rgb16.r' property")
541 .as_i64()
542 .is_some());
543 let dec = map
545 .get(serde_yaml::Value::String("dec".into()))
546 .expect("unable to get 'dec' property")
547 .as_mapping()
548 .expect("unable to create mapping");
549 assert!(dec
550 .get(serde_yaml::Value::String("r".into()))
551 .expect("unable to get 'dec.r' property")
552 .as_str()
553 .is_some());
554 }
555}