iced_core/
color.rs

1/// A color in the `sRGB` color space.
2///
3/// # String Representation
4///
5/// A color can be represented in either of the following valid formats: `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
6/// Where `rgba` represent hexadecimal digits. Both uppercase and lowercase letters are supported.
7///
8/// If `a` (transparency) is not specified, `1.0` (completely opaque) would be used by default.
9///
10/// If you have a static color string, using the [`color!`] macro should be preferred
11/// since it leverages hexadecimal literal notation and arithmetic directly.
12///
13/// [`color!`]: crate::color!
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Color {
17    /// Red component, 0.0 - 1.0
18    pub r: f32,
19    /// Green component, 0.0 - 1.0
20    pub g: f32,
21    /// Blue component, 0.0 - 1.0
22    pub b: f32,
23    /// Transparency, 0.0 - 1.0
24    pub a: f32,
25}
26
27impl Color {
28    /// The black color.
29    pub const BLACK: Color = Color {
30        r: 0.0,
31        g: 0.0,
32        b: 0.0,
33        a: 1.0,
34    };
35
36    /// The white color.
37    pub const WHITE: Color = Color {
38        r: 1.0,
39        g: 1.0,
40        b: 1.0,
41        a: 1.0,
42    };
43
44    /// A color with no opacity.
45    pub const TRANSPARENT: Color = Color {
46        r: 0.0,
47        g: 0.0,
48        b: 0.0,
49        a: 0.0,
50    };
51
52    /// Creates a new [`Color`].
53    ///
54    /// In debug mode, it will panic if the values are not in the correct
55    /// range: 0.0 - 1.0
56    const fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
57        debug_assert!(
58            r >= 0.0 && r <= 1.0,
59            "Red component must be in [0, 1] range."
60        );
61        debug_assert!(
62            g >= 0.0 && g <= 1.0,
63            "Green component must be in [0, 1] range."
64        );
65        debug_assert!(
66            b >= 0.0 && b <= 1.0,
67            "Blue component must be in [0, 1] range."
68        );
69
70        Color { r, g, b, a }
71    }
72
73    /// Creates a [`Color`] from its RGB components.
74    pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
75        Color::from_rgba(r, g, b, 1.0f32)
76    }
77
78    /// Creates a [`Color`] from its RGBA components.
79    pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
80        Color::new(r, g, b, a)
81    }
82
83    /// Creates a [`Color`] from its RGB8 components.
84    pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
85        Color::from_rgba8(r, g, b, 1.0)
86    }
87
88    /// Creates a [`Color`] from its RGB8 components and an alpha value.
89    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
90        Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
91    }
92
93    /// Creates a [`Color`] from its linear RGBA components.
94    pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
95        // As described in:
96        // https://en.wikipedia.org/wiki/SRGB
97        fn gamma_component(u: f32) -> f32 {
98            if u < 0.0031308 {
99                12.92 * u
100            } else {
101                1.055 * u.powf(1.0 / 2.4) - 0.055
102            }
103        }
104
105        Self::new(
106            gamma_component(r),
107            gamma_component(g),
108            gamma_component(b),
109            a,
110        )
111    }
112
113    /// Converts the [`Color`] into its RGBA8 equivalent.
114    #[must_use]
115    pub fn into_rgba8(self) -> [u8; 4] {
116        [
117            (self.r * 255.0).round() as u8,
118            (self.g * 255.0).round() as u8,
119            (self.b * 255.0).round() as u8,
120            (self.a * 255.0).round() as u8,
121        ]
122    }
123
124    /// Converts the [`Color`] into its linear values.
125    pub fn into_linear(self) -> [f32; 4] {
126        // As described in:
127        // https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
128        fn linear_component(u: f32) -> f32 {
129            if u < 0.04045 {
130                u / 12.92
131            } else {
132                ((u + 0.055) / 1.055).powf(2.4)
133            }
134        }
135
136        [
137            linear_component(self.r),
138            linear_component(self.g),
139            linear_component(self.b),
140            self.a,
141        ]
142    }
143
144    /// Inverts the [`Color`] in-place.
145    pub fn invert(&mut self) {
146        self.r = 1.0f32 - self.r;
147        self.b = 1.0f32 - self.g;
148        self.g = 1.0f32 - self.b;
149    }
150
151    /// Returns the inverted [`Color`].
152    pub fn inverse(self) -> Color {
153        Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
154    }
155
156    /// Scales the alpha channel of the [`Color`] by the given factor.
157    pub fn scale_alpha(self, factor: f32) -> Color {
158        Self {
159            a: self.a * factor,
160            ..self
161        }
162    }
163
164    /// Returns the relative luminance of the [`Color`].
165    /// <https://www.w3.org/TR/WCAG21/#dfn-relative-luminance>
166    pub fn relative_luminance(self) -> f32 {
167        let linear = self.into_linear();
168        0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
169    }
170
171    /// Returns the [relative contrast ratio] of the [`Color`] against another one.
172    ///
173    /// [relative contrast ratio]: https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
174    pub fn relative_contrast(self, b: Color) -> f32 {
175        let lum_a = self.relative_luminance();
176        let lum_b = b.relative_luminance();
177
178        (lum_a.max(lum_b) + 0.05) / (lum_a.min(lum_b) + 0.05)
179    }
180
181    /// Returns true if the current [`Color`] is readable on top
182    /// of the given background [`Color`].
183    pub fn is_readable_on(self, background: Color) -> bool {
184        background.relative_contrast(self) >= 6.0
185    }
186}
187
188impl From<[f32; 3]> for Color {
189    fn from([r, g, b]: [f32; 3]) -> Self {
190        Color::new(r, g, b, 1.0)
191    }
192}
193
194impl From<[f32; 4]> for Color {
195    fn from([r, g, b, a]: [f32; 4]) -> Self {
196        Color::new(r, g, b, a)
197    }
198}
199
200/// An error which can be returned when parsing color from an RGB hexadecimal string.
201///
202/// See [`Color`] for specifications for the string.
203#[derive(Debug, thiserror::Error)]
204pub enum ParseError {
205    /// The string could not be parsed to valid integers.
206    #[error(transparent)]
207    ParseIntError(#[from] std::num::ParseIntError),
208    /// The string is of invalid length.
209    #[error(
210        "expected hex string of length 3, 4, 6 or 8 excluding optional prefix '#', found {0}"
211    )]
212    InvalidLength(usize),
213}
214
215impl std::str::FromStr for Color {
216    type Err = ParseError;
217
218    fn from_str(s: &str) -> Result<Self, Self::Err> {
219        let hex = s.strip_prefix('#').unwrap_or(s);
220
221        let parse_channel =
222            |from: usize, to: usize| -> Result<f32, std::num::ParseIntError> {
223                let num =
224                    usize::from_str_radix(&hex[from..=to], 16)? as f32 / 255.0;
225
226                // If we only got half a byte (one letter), expand it into a full byte (two letters)
227                Ok(if from == to { num + num * 16.0 } else { num })
228            };
229
230        let val = match hex.len() {
231            3 => Color::from_rgb(
232                parse_channel(0, 0)?,
233                parse_channel(1, 1)?,
234                parse_channel(2, 2)?,
235            ),
236            4 => Color::from_rgba(
237                parse_channel(0, 0)?,
238                parse_channel(1, 1)?,
239                parse_channel(2, 2)?,
240                parse_channel(3, 3)?,
241            ),
242            6 => Color::from_rgb(
243                parse_channel(0, 1)?,
244                parse_channel(2, 3)?,
245                parse_channel(4, 5)?,
246            ),
247            8 => Color::from_rgba(
248                parse_channel(0, 1)?,
249                parse_channel(2, 3)?,
250                parse_channel(4, 5)?,
251                parse_channel(6, 7)?,
252            ),
253            _ => return Err(ParseError::InvalidLength(hex.len())),
254        };
255
256        Ok(val)
257    }
258}
259
260impl std::fmt::Display for Color {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        let [r, g, b, a] = self.into_rgba8();
263
264        if self.a == 1.0 {
265            return write!(f, "#{r:02x}{g:02x}{b:02x}");
266        }
267
268        write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}")
269    }
270}
271
272/// Creates a [`Color`] with shorter and cleaner syntax.
273///
274/// # Examples
275///
276/// ```
277/// # use iced_core::{Color, color};
278/// assert_eq!(color!(0, 0, 0), Color::BLACK);
279/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
280/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
281/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
282/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0));
283/// ```
284#[macro_export]
285macro_rules! color {
286    ($r:expr, $g:expr, $b:expr) => {
287        $crate::Color::from_rgb8($r, $g, $b)
288    };
289    ($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
290    ($hex:literal) => {{ $crate::color!($hex, 1.0) }};
291    ($hex:literal, $a:expr) => {{
292        let mut hex = $hex as u32;
293
294        // Shorthand notation: 0x123
295        if stringify!($hex).len() == 5 {
296            let r = hex & 0xF00;
297            let g = hex & 0xF0;
298            let b = hex & 0xF;
299
300            hex = (r << 12) | (r << 8) | (g << 8) | (g << 4) | (b << 4) | b;
301        }
302
303        debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
304
305        let r = (hex & 0xff0000) >> 16;
306        let g = (hex & 0xff00) >> 8;
307        let b = (hex & 0xff);
308
309        $crate::color!(r as u8, g as u8, b as u8, $a)
310    }};
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn parse() {
319        let tests = [
320            ("#ff0000", [255, 0, 0, 255], "#ff0000"),
321            ("00ff0080", [0, 255, 0, 128], "#00ff0080"),
322            ("#F80", [255, 136, 0, 255], "#ff8800"),
323            ("#00f1", [0, 0, 255, 17], "#0000ff11"),
324            ("#00ff", [0, 0, 255, 255], "#0000ff"),
325        ];
326
327        for (arg, expected_rgba8, expected_str) in tests {
328            let color = arg.parse::<Color>().expect("color must parse");
329
330            assert_eq!(color.into_rgba8(), expected_rgba8);
331            assert_eq!(color.to_string(), expected_str);
332        }
333
334        assert!("invalid".parse::<Color>().is_err());
335    }
336
337    const SHORTHAND: Color = color!(0x123);
338
339    #[test]
340    fn shorthand_notation() {
341        assert_eq!(SHORTHAND, Color::from_rgb8(0x11, 0x22, 0x33));
342    }
343}