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