tailor_api/
color.rs

1use atoi::FromRadix16;
2use std::{fmt::Display, io, str::FromStr};
3
4#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5pub struct ColorPoint {
6    pub color: Color,
7    pub transition: ColorTransition,
8    /// Transition time in ms.
9    pub transition_time: u32,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub enum ColorTransition {
14    None,
15    Linear,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub enum ColorProfile {
20    None,
21    Single(Color),
22    Multiple(Vec<ColorPoint>),
23}
24
25impl Default for ColorProfile {
26    fn default() -> Self {
27        Self::None
28    }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct Color {
33    pub r: u8,
34    pub g: u8,
35    pub b: u8,
36}
37
38impl Color {
39    pub fn sysfs_rgb_string(&self, max_brightness: u32) -> String {
40        let Color { r, g, b } = *self;
41        if max_brightness == 255 {
42            format!("{r} {g} {b}")
43        } else {
44            let max = max_brightness as f32;
45            let scale = |value: u8| -> u32 { (value as f32 / 255.0 * max).clamp(0.0, max) as u32 };
46            format!("{} {} {}", scale(r), scale(g), scale(b))
47        }
48    }
49
50    pub fn sysfs_monochrome_string(&self, max_brightness: u32) -> String {
51        let Color { r, g, b } = *self;
52        let average = [r, g, b].into_iter().map(u16::from).sum::<u16>() / 3;
53        if max_brightness == 255 {
54            average.to_string()
55        } else {
56            let max = max_brightness as f32;
57            let value = (average as f32 / 255.0 * max).clamp(0.0, max) as u32;
58            value.to_string()
59        }
60    }
61
62    pub fn from_sysfs_rgb_value(values: [u32; 3], max_brightness: u32) -> Self {
63        if max_brightness == 255 {
64            let values: Vec<u8> = values
65                .into_iter()
66                .map(u8::try_from)
67                .map(Result::unwrap_or_default)
68                .collect();
69            Self {
70                r: values[0],
71                g: values[1],
72                b: values[2],
73            }
74        } else {
75            let max = max_brightness as f32;
76            let scale = |value: u32| -> u8 { (value as f32 / max * 255.0).clamp(0.0, 255.0) as u8 };
77            Self {
78                r: scale(values[0]),
79                g: scale(values[1]),
80                b: scale(values[2]),
81            }
82        }
83    }
84}
85
86impl Display for Color {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        write!(f, "0x{:02X}{:02X}{:02X}", self.r, self.g, self.b)
89    }
90}
91
92impl FromStr for Color {
93    type Err = io::Error;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        if s.len() != 6 {
97            Err(io::Error::new(
98                io::ErrorKind::InvalidData,
99                "Incorrect length for 3x8-bit hexadecimal value",
100            ))
101        } else {
102            let r = u8::from_radix_16(s[0..2].as_bytes());
103            let g = u8::from_radix_16(s[2..4].as_bytes());
104            let b = u8::from_radix_16(s[4..6].as_bytes());
105
106            if r.1 == 2 && g.1 == 2 && b.1 == 2 {
107                Ok(Self {
108                    r: r.0,
109                    g: g.0,
110                    b: b.0,
111                })
112            } else {
113                Err(io::Error::new(
114                    io::ErrorKind::InvalidData,
115                    "Incorrect value (not [0-9] or [A-F]) in hexadecimal value",
116                ))
117            }
118        }
119    }
120}
121
122#[cfg(test)]
123mod test {
124    use crate::color::Color;
125    use std::str::FromStr;
126
127    #[test]
128    fn color_from_string() {
129        let string = "000Fac";
130        let color = Color::from_str(string).unwrap();
131        assert_eq!(
132            color,
133            Color {
134                r: 0x00,
135                g: 0x0F,
136                b: 0xAC,
137            }
138        );
139        assert_eq!(color.to_string()[2..], string.to_ascii_uppercase());
140
141        Color::from_str("F00FF").unwrap_err();
142        Color::from_str("F").unwrap_err();
143        Color::from_str("INVLD!").unwrap_err();
144    }
145}