terminal_colorsaurus/
color.rs

1/// An RGB color with 16 bits per channel.
2/// You can use [`Color::scale_to_8bit`] to convert to an 8bit RGB color.
3#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
4#[non_exhaustive]
5pub struct Color {
6    /// Red
7    pub r: u16,
8    /// Green
9    pub g: u16,
10    /// Blue
11    pub b: u16,
12    // Alpha field is omitted because only rxvt-unicode supports it.
13    // If you need it, open a PR. Thanks to #[non_exhaustive] this would be a non-breaking change.
14}
15
16impl Color {
17    /// Creates a RGB color from its three channels: Red, Green and Blue.
18    pub fn rgb(r: u16, g: u16, b: u16) -> Self {
19        Self { r, g, b }
20    }
21
22    /// Perceptual lightness (L*) as a value between 0.0 (black) and 1.0 (white)
23    /// where 0.5 is the perceptual middle gray.
24    ///
25    /// Note that the color's alpha is ignored.
26    /// ```
27    /// # use terminal_colorsaurus::Color;
28    /// # let color = Color::default();
29    /// let is_dark = color.perceived_lightness() <= 0.5;
30    /// ```
31    pub fn perceived_lightness(&self) -> f32 {
32        let color = xterm_color::Color::rgb(self.r, self.g, self.b);
33        color.perceived_lightness()
34    }
35
36    /// Converts the color to 8 bit precision per channel by scaling each channel.
37    ///
38    /// ```
39    /// # use terminal_colorsaurus::Color;
40    /// let white = Color::rgb(u16::MAX, u16::MAX, u16::MAX);
41    /// assert_eq!((u8::MAX, u8::MAX, u8::MAX), white.scale_to_8bit());
42    ///
43    /// let black = Color::rgb(0, 0, 0);
44    /// assert_eq!((0, 0, 0), black.scale_to_8bit());
45    /// ```
46    pub fn scale_to_8bit(&self) -> (u8, u8, u8) {
47        (
48            scale_to_u8(self.r),
49            scale_to_u8(self.g),
50            scale_to_u8(self.b),
51        )
52    }
53}
54
55fn scale_to_u8(channel: u16) -> u8 {
56    (channel as u32 * (u8::MAX as u32) / (u16::MAX as u32)) as u8
57}
58
59#[cfg(feature = "rgb")]
60impl From<Color> for rgb::RGB16 {
61    fn from(value: Color) -> Self {
62        rgb::RGB16 {
63            r: value.r,
64            g: value.g,
65            b: value.b,
66        }
67    }
68}
69
70#[cfg(feature = "rgb")]
71impl From<Color> for rgb::RGB8 {
72    fn from(value: Color) -> Self {
73        let (r, g, b) = value.scale_to_8bit();
74        rgb::RGB8 { r, g, b }
75    }
76}
77
78#[cfg(feature = "rgb")]
79impl From<rgb::RGB16> for Color {
80    fn from(value: rgb::RGB16) -> Self {
81        Color {
82            r: value.r,
83            g: value.g,
84            b: value.b,
85        }
86    }
87}
88
89#[cfg(feature = "anstyle")]
90impl From<Color> for anstyle::RgbColor {
91    fn from(value: Color) -> Self {
92        let (r, g, b) = value.scale_to_8bit();
93        anstyle::RgbColor(r, g, b)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn black_has_perceived_lightness_zero() {
103        let black = Color::default();
104        assert_eq!(0.0, black.perceived_lightness())
105    }
106
107    #[test]
108    fn white_has_perceived_lightness_100() {
109        let white = Color {
110            r: u16::MAX,
111            g: u16::MAX,
112            b: u16::MAX,
113        };
114        assert_eq!(1.0, white.perceived_lightness())
115    }
116}