Skip to main content

cranpose_ui_graphics/
color.rs

1//! Color representation and color space utilities
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct Color(pub f32, pub f32, pub f32, pub f32);
5
6impl Color {
7    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
8        Self(r, g, b, 1.0)
9    }
10
11    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
12        Self(r, g, b, a)
13    }
14
15    pub const fn from_rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
16        Self(
17            r as f32 / 255.0,
18            g as f32 / 255.0,
19            b as f32 / 255.0,
20            a as f32 / 255.0,
21        )
22    }
23
24    pub const fn from_rgb_u8(r: u8, g: u8, b: u8) -> Self {
25        Self::from_rgba_u8(r, g, b, 255)
26    }
27
28    pub fn r(&self) -> f32 {
29        self.0
30    }
31
32    pub fn g(&self) -> f32 {
33        self.1
34    }
35
36    pub fn b(&self) -> f32 {
37        self.2
38    }
39
40    pub fn a(&self) -> f32 {
41        self.3
42    }
43
44    pub fn with_alpha(&self, alpha: f32) -> Self {
45        Self(self.0, self.1, self.2, alpha)
46    }
47
48    /// This colour as the platform's colour type actually holds it: **eight
49    /// bits per channel**.
50    ///
51    /// `androidx.compose.ui.graphics.Color` is a float-shaped API over an
52    /// 8-bit value. Building one in the sRGB space snaps every channel on the
53    /// spot — the bytecode of `ColorKt.Color(float, float, float, float,
54    /// ColorSpace)` is `coerceIn(0f, 1f)`, `* 255.0f`, `+ 0.5f`, `f2i`, packed
55    /// into an ARGB int — so a colour an app computes in float is already a
56    /// whole channel value before anything paints with it, and the rasterizer
57    /// never sees the fraction.
58    ///
59    /// A renderer that carries the fraction to the framebuffer instead leaves
60    /// the rounding to whatever converts float to unorm there. That agrees
61    /// nearly everywhere and disagrees on an exact half. It is not a rare
62    /// shape: a theme that lerps between two byte colours lands on one
63    /// routinely — `mix(rail, background, 0.55)` puts a Wear settings capsule
64    /// at exactly 22.5/255 on green, where this rule gives 23 and a converter
65    /// that breaks ties to even gives 22. One level, and then the row's layer
66    /// alpha multiplies it and keeps it.
67    ///
68    /// Ties go **up**, which is what `(int)(x + 0.5f)`, Skia's
69    /// `SkScalarRoundToInt` and Rust's `f32::round` all give for a channel
70    /// clamped into 0..=1.
71    pub fn srgb_8bit(self) -> Self {
72        Self(
73            srgb_channel_8bit(self.0),
74            srgb_channel_8bit(self.1),
75            srgb_channel_8bit(self.2),
76            srgb_channel_8bit(self.3),
77        )
78    }
79
80    pub const BLACK: Color = Color(0.0, 0.0, 0.0, 1.0);
81    pub const WHITE: Color = Color(1.0, 1.0, 1.0, 1.0);
82    pub const RED: Color = Color(1.0, 0.0, 0.0, 1.0);
83    pub const GREEN: Color = Color(0.0, 1.0, 0.0, 1.0);
84    pub const BLUE: Color = Color(0.0, 0.0, 1.0, 1.0);
85    pub const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
86}
87
88/// One channel snapped to the 8-bit value an sRGB colour holds.
89fn srgb_channel_8bit(channel: f32) -> f32 {
90    (channel.clamp(0.0, 1.0) * 255.0).round() / 255.0
91}
92
93#[cfg(test)]
94#[path = "tests/color_tests.rs"]
95mod tests;