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;