Skip to main content

bc_mur/
color.rs

1use crate::{Error, Result};
2
3/// RGBA color with 8-bit channels.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct Color {
6    pub r: u8,
7    pub g: u8,
8    pub b: u8,
9    pub a: u8,
10}
11
12impl Color {
13    pub const BLACK: Self = Self { r: 0, g: 0, b: 0, a: 255 };
14    pub const WHITE: Self = Self { r: 255, g: 255, b: 255, a: 255 };
15    pub const TRANSPARENT: Self = Self { r: 0, g: 0, b: 0, a: 0 };
16
17    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } }
18
19    /// Parse hex color: `#RGB`, `#RRGGBB`, or `#RRGGBBAA`.
20    pub fn from_hex(s: &str) -> Result<Self> {
21        let s = s.strip_prefix('#').unwrap_or(s);
22        match s.len() {
23            3 => {
24                let r = hex_nibble(s.as_bytes()[0])?;
25                let g = hex_nibble(s.as_bytes()[1])?;
26                let b = hex_nibble(s.as_bytes()[2])?;
27                Ok(Self::new(r << 4 | r, g << 4 | g, b << 4 | b, 255))
28            }
29            6 => {
30                let r = hex_byte(&s[0..2])?;
31                let g = hex_byte(&s[2..4])?;
32                let b = hex_byte(&s[4..6])?;
33                Ok(Self::new(r, g, b, 255))
34            }
35            8 => {
36                let r = hex_byte(&s[0..2])?;
37                let g = hex_byte(&s[2..4])?;
38                let b = hex_byte(&s[4..6])?;
39                let a = hex_byte(&s[6..8])?;
40                Ok(Self::new(r, g, b, a))
41            }
42            _ => Err(Error::InvalidColor(format!(
43                "expected #RGB, #RRGGBB, or #RRGGBBAA, got: #{s}"
44            ))),
45        }
46    }
47
48    /// True if alpha < 3 (effectively transparent).
49    pub fn is_transparent(self) -> bool { self.a < 3 }
50}
51
52impl std::fmt::Display for Color {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        if self.a == 255 {
55            write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
56        } else {
57            write!(
58                f,
59                "#{:02X}{:02X}{:02X}{:02X}",
60                self.r, self.g, self.b, self.a
61            )
62        }
63    }
64}
65
66fn hex_nibble(b: u8) -> Result<u8> {
67    match b {
68        b'0'..=b'9' => Ok(b - b'0'),
69        b'a'..=b'f' => Ok(b - b'a' + 10),
70        b'A'..=b'F' => Ok(b - b'A' + 10),
71        _ => Err(Error::InvalidColor(format!("invalid hex digit: {b}"))),
72    }
73}
74
75fn hex_byte(s: &str) -> Result<u8> {
76    let hi = hex_nibble(s.as_bytes()[0])?;
77    let lo = hex_nibble(s.as_bytes()[1])?;
78    Ok(hi << 4 | lo)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn parse_hex_6() {
87        let c = Color::from_hex("#FF8000").unwrap();
88        assert_eq!(c, Color::new(255, 128, 0, 255));
89    }
90
91    #[test]
92    fn parse_hex_8() {
93        let c = Color::from_hex("#FF800080").unwrap();
94        assert_eq!(c, Color::new(255, 128, 0, 128));
95    }
96
97    #[test]
98    fn parse_hex_3() {
99        let c = Color::from_hex("#F80").unwrap();
100        assert_eq!(c, Color::new(0xFF, 0x88, 0x00, 255));
101    }
102
103    #[test]
104    fn display_rgb() {
105        assert_eq!(Color::BLACK.to_string(), "#000000");
106    }
107
108    #[test]
109    fn display_rgba() {
110        assert_eq!(Color::new(255, 128, 0, 128).to_string(), "#FF800080");
111    }
112}