Skip to main content

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