Skip to main content

gpui_kit_material_color/
color.rs

1/// An sRGB color with straight alpha, each channel in `0.0..=1.0`.
2#[derive(Clone, Copy, Debug, Default, PartialEq)]
3pub struct Color {
4    pub r: f32,
5    pub g: f32,
6    pub b: f32,
7    pub a: f32,
8}
9
10impl Color {
11    /// An opaque color from `0xRRGGBB`.
12    pub const fn hex(rgb: u32) -> Self {
13        Self::hexa((rgb << 8) | 0xff)
14    }
15
16    /// A color from `0xRRGGBBAA`.
17    pub const fn hexa(rgba: u32) -> Self {
18        Self {
19            r: ((rgba >> 24) & 0xff) as f32 / 255.,
20            g: ((rgba >> 16) & 0xff) as f32 / 255.,
21            b: ((rgba >> 8) & 0xff) as f32 / 255.,
22            a: (rgba & 0xff) as f32 / 255.,
23        }
24    }
25
26    /// The same color at `alpha` opacity.
27    pub const fn alpha(self, alpha: f32) -> Self {
28        Self { a: alpha, ..self }
29    }
30
31    /// Linear interpolation toward `to`; `t` is clamped to `0.0..=1.0`, and the ends are exact.
32    ///
33    /// State layers, tonal containers and palette transitions all use this
34    /// one blend, so a hovered selected control resolves over its current
35    /// container instead of stacking translucent fills.
36    pub fn blend(self, to: Self, t: f32) -> Self {
37        // The ends are exact, so a finished transition paints its target.
38        if t >= 1. {
39            return to;
40        }
41        let t = t.max(0.);
42        Self {
43            r: self.r + (to.r - self.r) * t,
44            g: self.g + (to.g - self.g) * t,
45            b: self.b + (to.b - self.b) * t,
46            a: self.a + (to.a - self.a) * t,
47        }
48    }
49
50    /// Relative luminance (WCAG 2.x / sRGB): Rec. 709 weights on the
51    /// linearized channels, so mid gray `#808080` is about 0.216, not 0.5.
52    pub fn luminance(self) -> f32 {
53        fn linear(channel: f32) -> f32 {
54            if channel <= 0.04045 {
55                channel / 12.92
56            } else {
57                ((channel + 0.055) / 1.055).powf(2.4)
58            }
59        }
60        0.2126 * linear(self.r) + 0.7152 * linear(self.g) + 0.0722 * linear(self.b)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::Color;
67
68    #[test]
69    fn hex_and_blend() {
70        let black = Color::hex(0x000000);
71        let white = Color::hex(0xffffff);
72        assert_eq!(white.a, 1.);
73        assert_eq!(black.blend(white, 0.5).r, 0.5);
74        assert_eq!(black.blend(white, 2.), white, "t is clamped");
75        assert_eq!(Color::hexa(0x10141680).a, 128. / 255.);
76        assert!(white.luminance() > black.luminance());
77    }
78
79    #[test]
80    fn luminance_linearizes_srgb() {
81        assert_eq!(Color::hex(0xffffff).luminance(), 1.);
82        assert_eq!(Color::hex(0x000000).luminance(), 0.);
83        // WCAG: #808080 is 0.2159, far darker than its stored 0.502.
84        let gray = Color::hex(0x808080).luminance();
85        assert!((gray - 0.2159).abs() < 0.0005, "{gray}");
86        // The low segment is linear: #0A0A0A (0.0392) / 12.92.
87        let dark = Color::hex(0x0a0a0a).luminance();
88        assert!((dark - 10. / 255. / 12.92).abs() < 1e-6, "{dark}");
89        // Pure green carries the largest weight.
90        assert!((Color::hex(0x00ff00).luminance() - 0.7152).abs() < 1e-6);
91    }
92}