iced_core/
color.rs

1/// A color in the `sRGB` color space.
2#[derive(Debug, Clone, Copy, PartialEq, Default)]
3#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4pub struct Color {
5    /// Red component, 0.0 - 1.0
6    pub r: f32,
7    /// Green component, 0.0 - 1.0
8    pub g: f32,
9    /// Blue component, 0.0 - 1.0
10    pub b: f32,
11    /// Transparency, 0.0 - 1.0
12    pub a: f32,
13}
14
15impl Color {
16    /// The black color.
17    pub const BLACK: Color = Color {
18        r: 0.0,
19        g: 0.0,
20        b: 0.0,
21        a: 1.0,
22    };
23
24    /// The white color.
25    pub const WHITE: Color = Color {
26        r: 1.0,
27        g: 1.0,
28        b: 1.0,
29        a: 1.0,
30    };
31
32    /// A color with no opacity.
33    pub const TRANSPARENT: Color = Color {
34        r: 0.0,
35        g: 0.0,
36        b: 0.0,
37        a: 0.0,
38    };
39
40    /// Creates a new [`Color`].
41    ///
42    /// In debug mode, it will panic if the values are not in the correct
43    /// range: 0.0 - 1.0
44    const fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
45        debug_assert!(
46            r >= 0.0 && r <= 1.0,
47            "Red component must be in [0, 1] range."
48        );
49        debug_assert!(
50            g >= 0.0 && g <= 1.0,
51            "Green component must be in [0, 1] range."
52        );
53        debug_assert!(
54            b >= 0.0 && b <= 1.0,
55            "Blue component must be in [0, 1] range."
56        );
57
58        Color { r, g, b, a }
59    }
60
61    /// Creates a [`Color`] from its RGB components.
62    pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
63        Color::from_rgba(r, g, b, 1.0f32)
64    }
65
66    /// Creates a [`Color`] from its RGBA components.
67    pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
68        Color::new(r, g, b, a)
69    }
70
71    /// Creates a [`Color`] from its RGB8 components.
72    pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
73        Color::from_rgba8(r, g, b, 1.0)
74    }
75
76    /// Creates a [`Color`] from its RGB8 components and an alpha value.
77    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
78        Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
79    }
80
81    /// Creates a [`Color`] from its linear RGBA components.
82    pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
83        // As described in:
84        // https://en.wikipedia.org/wiki/SRGB
85        fn gamma_component(u: f32) -> f32 {
86            if u < 0.0031308 {
87                12.92 * u
88            } else {
89                1.055 * u.powf(1.0 / 2.4) - 0.055
90            }
91        }
92
93        Self::new(
94            gamma_component(r),
95            gamma_component(g),
96            gamma_component(b),
97            a,
98        )
99    }
100
101    /// Parses a [`Color`] from a hex string.
102    ///
103    /// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
104    /// The starting "#" is optional. Both uppercase and lowercase are supported.
105    ///
106    /// If you have a static color string, using the [`color!`] macro should be preferred
107    /// since it leverages hexadecimal literal notation and arithmetic directly.
108    ///
109    /// [`color!`]: crate::color!
110    pub fn parse(s: &str) -> Option<Color> {
111        let hex = s.strip_prefix('#').unwrap_or(s);
112
113        let parse_channel = |from: usize, to: usize| {
114            let num =
115                usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0;
116
117            // If we only got half a byte (one letter), expand it into a full byte (two letters)
118            Some(if from == to { num + num * 16.0 } else { num })
119        };
120
121        Some(match hex.len() {
122            3 => Color::from_rgb(
123                parse_channel(0, 0)?,
124                parse_channel(1, 1)?,
125                parse_channel(2, 2)?,
126            ),
127            4 => Color::from_rgba(
128                parse_channel(0, 0)?,
129                parse_channel(1, 1)?,
130                parse_channel(2, 2)?,
131                parse_channel(3, 3)?,
132            ),
133            6 => Color::from_rgb(
134                parse_channel(0, 1)?,
135                parse_channel(2, 3)?,
136                parse_channel(4, 5)?,
137            ),
138            8 => Color::from_rgba(
139                parse_channel(0, 1)?,
140                parse_channel(2, 3)?,
141                parse_channel(4, 5)?,
142                parse_channel(6, 7)?,
143            ),
144            _ => None?,
145        })
146    }
147
148    /// Converts the [`Color`] into its RGBA8 equivalent.
149    #[must_use]
150    pub fn into_rgba8(self) -> [u8; 4] {
151        [
152            (self.r * 255.0).round() as u8,
153            (self.g * 255.0).round() as u8,
154            (self.b * 255.0).round() as u8,
155            (self.a * 255.0).round() as u8,
156        ]
157    }
158
159    /// Converts the [`Color`] into its linear values.
160    pub fn into_linear(self) -> [f32; 4] {
161        // As described in:
162        // https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
163        fn linear_component(u: f32) -> f32 {
164            if u < 0.04045 {
165                u / 12.92
166            } else {
167                ((u + 0.055) / 1.055).powf(2.4)
168            }
169        }
170
171        [
172            linear_component(self.r),
173            linear_component(self.g),
174            linear_component(self.b),
175            self.a,
176        ]
177    }
178
179    /// Inverts the [`Color`] in-place.
180    pub fn invert(&mut self) {
181        self.r = 1.0f32 - self.r;
182        self.b = 1.0f32 - self.g;
183        self.g = 1.0f32 - self.b;
184    }
185
186    /// Returns the inverted [`Color`].
187    pub fn inverse(self) -> Color {
188        Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
189    }
190
191    /// Scales the alpha channel of the [`Color`] by the given factor.
192    pub fn scale_alpha(self, factor: f32) -> Color {
193        Self {
194            a: self.a * factor,
195            ..self
196        }
197    }
198
199    /// Returns the relative luminance of the [`Color`].
200    /// <https://www.w3.org/TR/WCAG21/#dfn-relative-luminance>
201    pub fn relative_luminance(self) -> f32 {
202        let linear = self.into_linear();
203        0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
204    }
205}
206
207impl From<[f32; 3]> for Color {
208    fn from([r, g, b]: [f32; 3]) -> Self {
209        Color::new(r, g, b, 1.0)
210    }
211}
212
213impl From<[f32; 4]> for Color {
214    fn from([r, g, b, a]: [f32; 4]) -> Self {
215        Color::new(r, g, b, a)
216    }
217}
218
219/// Creates a [`Color`] with shorter and cleaner syntax.
220///
221/// # Examples
222///
223/// ```
224/// # use iced_core::{Color, color};
225/// assert_eq!(color!(0, 0, 0), Color::BLACK);
226/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
227/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
228/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
229/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0));
230/// ```
231#[macro_export]
232macro_rules! color {
233    ($r:expr, $g:expr, $b:expr) => {
234        $crate::Color::from_rgb8($r, $g, $b)
235    };
236    ($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
237    ($hex:expr) => {{ $crate::color!($hex, 1.0) }};
238    ($hex:expr, $a:expr) => {{
239        let hex = $hex as u32;
240
241        debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
242
243        let r = (hex & 0xff0000) >> 16;
244        let g = (hex & 0xff00) >> 8;
245        let b = (hex & 0xff);
246
247        $crate::color!(r as u8, g as u8, b as u8, $a)
248    }};
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn parse() {
257        let tests = [
258            ("#ff0000", [255, 0, 0, 255]),
259            ("00ff0080", [0, 255, 0, 128]),
260            ("#F80", [255, 136, 0, 255]),
261            ("#00f1", [0, 0, 255, 17]),
262        ];
263
264        for (arg, expected) in tests {
265            assert_eq!(
266                Color::parse(arg).expect("color must parse").into_rgba8(),
267                expected
268            );
269        }
270
271        assert!(Color::parse("invalid").is_none());
272    }
273}