iced_core/
color.rs

1use palette::rgb::{Srgb, Srgba};
2
3/// A color in the `sRGB` color space.
4#[derive(Debug, Clone, Copy, PartialEq, Default)]
5pub struct Color {
6    /// Red component, 0.0 - 1.0
7    pub r: f32,
8    /// Green component, 0.0 - 1.0
9    pub g: f32,
10    /// Blue component, 0.0 - 1.0
11    pub b: f32,
12    /// Transparency, 0.0 - 1.0
13    pub a: f32,
14}
15
16impl Color {
17    /// The black color.
18    pub const BLACK: Color = Color {
19        r: 0.0,
20        g: 0.0,
21        b: 0.0,
22        a: 1.0,
23    };
24
25    /// The white color.
26    pub const WHITE: Color = Color {
27        r: 1.0,
28        g: 1.0,
29        b: 1.0,
30        a: 1.0,
31    };
32
33    /// A color with no opacity.
34    pub const TRANSPARENT: Color = Color {
35        r: 0.0,
36        g: 0.0,
37        b: 0.0,
38        a: 0.0,
39    };
40
41    /// Creates a new [`Color`].
42    ///
43    /// In debug mode, it will panic if the values are not in the correct
44    /// range: 0.0 - 1.0
45    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
46        debug_assert!(
47            (0.0..=1.0).contains(&r),
48            "Red component must be on [0, 1]"
49        );
50        debug_assert!(
51            (0.0..=1.0).contains(&g),
52            "Green component must be on [0, 1]"
53        );
54        debug_assert!(
55            (0.0..=1.0).contains(&b),
56            "Blue component must be on [0, 1]"
57        );
58        debug_assert!(
59            (0.0..=1.0).contains(&a),
60            "Alpha component must be on [0, 1]"
61        );
62
63        Color { r, g, b, a }
64    }
65
66    /// Creates a [`Color`] from its RGB components.
67    pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
68        Color::from_rgba(r, g, b, 1.0f32)
69    }
70
71    /// Creates a [`Color`] from its RGBA components.
72    pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
73        Color { r, g, b, a }
74    }
75
76    /// Creates a [`Color`] from its RGB8 components.
77    pub fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
78        Color::from_rgba8(r, g, b, 1.0)
79    }
80
81    /// Creates a [`Color`] from its RGB8 components and an alpha value.
82    pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
83        Color {
84            r: f32::from(r) / 255.0,
85            g: f32::from(g) / 255.0,
86            b: f32::from(b) / 255.0,
87            a,
88        }
89    }
90
91    /// Creates a [`Color`] from its linear RGBA components.
92    pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
93        // As described in:
94        // https://en.wikipedia.org/wiki/SRGB
95        fn gamma_component(u: f32) -> f32 {
96            if u < 0.0031308 {
97                12.92 * u
98            } else {
99                1.055 * u.powf(1.0 / 2.4) - 0.055
100            }
101        }
102
103        Self {
104            r: gamma_component(r),
105            g: gamma_component(g),
106            b: gamma_component(b),
107            a,
108        }
109    }
110
111    /// Parses a [`Color`] from a hex string.
112    ///
113    /// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
114    /// The starting "#" is optional. Both uppercase and lowercase are supported.
115    ///
116    /// If you have a static color string, using the [`color!`] macro should be preferred
117    /// since it leverages hexadecimal literal notation and arithmetic directly.
118    ///
119    /// [`color!`]: crate::color!
120    pub fn parse(s: &str) -> Option<Color> {
121        let hex = s.strip_prefix('#').unwrap_or(s);
122
123        let parse_channel = |from: usize, to: usize| {
124            let num =
125                usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0;
126
127            // If we only got half a byte (one letter), expand it into a full byte (two letters)
128            Some(if from == to { num + num * 16.0 } else { num })
129        };
130
131        Some(match hex.len() {
132            3 => Color::from_rgb(
133                parse_channel(0, 0)?,
134                parse_channel(1, 1)?,
135                parse_channel(2, 2)?,
136            ),
137            4 => Color::from_rgba(
138                parse_channel(0, 0)?,
139                parse_channel(1, 1)?,
140                parse_channel(2, 2)?,
141                parse_channel(3, 3)?,
142            ),
143            6 => Color::from_rgb(
144                parse_channel(0, 1)?,
145                parse_channel(2, 3)?,
146                parse_channel(4, 5)?,
147            ),
148            8 => Color::from_rgba(
149                parse_channel(0, 1)?,
150                parse_channel(2, 3)?,
151                parse_channel(4, 5)?,
152                parse_channel(6, 7)?,
153            ),
154            _ => None?,
155        })
156    }
157
158    /// Converts the [`Color`] into its RGBA8 equivalent.
159    #[must_use]
160    pub fn into_rgba8(self) -> [u8; 4] {
161        [
162            (self.r * 255.0).round() as u8,
163            (self.g * 255.0).round() as u8,
164            (self.b * 255.0).round() as u8,
165            (self.a * 255.0).round() as u8,
166        ]
167    }
168
169    /// Converts the [`Color`] into its linear values.
170    pub fn into_linear(self) -> [f32; 4] {
171        // As described in:
172        // https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
173        fn linear_component(u: f32) -> f32 {
174            if u < 0.04045 {
175                u / 12.92
176            } else {
177                ((u + 0.055) / 1.055).powf(2.4)
178            }
179        }
180
181        [
182            linear_component(self.r),
183            linear_component(self.g),
184            linear_component(self.b),
185            self.a,
186        ]
187    }
188
189    /// Inverts the [`Color`] in-place.
190    pub fn invert(&mut self) {
191        self.r = 1.0f32 - self.r;
192        self.b = 1.0f32 - self.g;
193        self.g = 1.0f32 - self.b;
194    }
195
196    /// Returns the inverted [`Color`].
197    pub fn inverse(self) -> Color {
198        Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
199    }
200
201    /// Scales the alpha channel of the [`Color`] by the given factor.
202    pub fn scale_alpha(self, factor: f32) -> Color {
203        Self {
204            a: self.a * factor,
205            ..self
206        }
207    }
208}
209
210impl From<[f32; 3]> for Color {
211    fn from([r, g, b]: [f32; 3]) -> Self {
212        Color::new(r, g, b, 1.0)
213    }
214}
215
216impl From<[f32; 4]> for Color {
217    fn from([r, g, b, a]: [f32; 4]) -> Self {
218        Color::new(r, g, b, a)
219    }
220}
221
222/// Creates a [`Color`] with shorter and cleaner syntax.
223///
224/// # Examples
225///
226/// ```
227/// # use iced_core::{Color, color};
228/// assert_eq!(color!(0, 0, 0), Color::BLACK);
229/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
230/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
231/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
232/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0));
233/// ```
234#[macro_export]
235macro_rules! color {
236    ($r:expr, $g:expr, $b:expr) => {
237        $crate::color!($r, $g, $b, 1.0)
238    };
239    ($r:expr, $g:expr, $b:expr, $a:expr) => {{
240        let r = $r as f32 / 255.0;
241        let g = $g as f32 / 255.0;
242        let b = $b as f32 / 255.0;
243
244        #[allow(clippy::manual_range_contains)]
245        {
246            debug_assert!(
247                r >= 0.0 && r <= 1.0,
248                "R channel must be in [0, 255] range."
249            );
250            debug_assert!(
251                g >= 0.0 && g <= 1.0,
252                "G channel must be in [0, 255] range."
253            );
254            debug_assert!(
255                b >= 0.0 && b <= 1.0,
256                "B channel must be in [0, 255] range."
257            );
258        }
259
260        $crate::Color { r, g, b, a: $a }
261    }};
262    ($hex:expr) => {{
263        $crate::color!($hex, 1.0)
264    }};
265    ($hex:expr, $a:expr) => {{
266        let hex = $hex as u32;
267
268        debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
269
270        let r = (hex & 0xff0000) >> 16;
271        let g = (hex & 0xff00) >> 8;
272        let b = (hex & 0xff);
273
274        $crate::color!(r, g, b, $a)
275    }};
276}
277
278/// Converts from palette's `Rgba` type to a [`Color`].
279impl From<Srgba> for Color {
280    fn from(rgba: Srgba) -> Self {
281        Color::new(rgba.red, rgba.green, rgba.blue, rgba.alpha)
282    }
283}
284
285/// Converts from [`Color`] to palette's `Rgba` type.
286impl From<Color> for Srgba {
287    fn from(c: Color) -> Self {
288        Srgba::new(c.r, c.g, c.b, c.a)
289    }
290}
291
292/// Converts from palette's `Rgb` type to a [`Color`].
293impl From<Srgb> for Color {
294    fn from(rgb: Srgb) -> Self {
295        Color::new(rgb.red, rgb.green, rgb.blue, 1.0)
296    }
297}
298
299/// Converts from [`Color`] to palette's `Rgb` type.
300impl From<Color> for Srgb {
301    fn from(c: Color) -> Self {
302        Srgb::new(c.r, c.g, c.b)
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use palette::blend::Blend;
310
311    #[test]
312    fn srgba_traits() {
313        let c = Color::from_rgb(0.5, 0.4, 0.3);
314        // Round-trip conversion to the palette::Srgba type
315        let s: Srgba = c.into();
316        let r: Color = s.into();
317        assert_eq!(c, r);
318    }
319
320    #[test]
321    fn color_manipulation() {
322        use approx::assert_relative_eq;
323
324        let c1 = Color::from_rgb(0.5, 0.4, 0.3);
325        let c2 = Color::from_rgb(0.2, 0.5, 0.3);
326
327        // Convert to linear color for manipulation
328        let l1 = Srgba::from(c1).into_linear();
329        let l2 = Srgba::from(c2).into_linear();
330
331        // Take the lighter of each of the sRGB components
332        let lighter = l1.lighten(l2);
333
334        // Convert back to our Color
335        let result: Color = Srgba::from_linear(lighter).into();
336
337        assert_relative_eq!(result.r, 0.5);
338        assert_relative_eq!(result.g, 0.5);
339        assert_relative_eq!(result.b, 0.3);
340        assert_relative_eq!(result.a, 1.0);
341    }
342
343    #[test]
344    fn parse() {
345        let tests = [
346            ("#ff0000", [255, 0, 0, 255]),
347            ("00ff0080", [0, 255, 0, 128]),
348            ("#F80", [255, 136, 0, 255]),
349            ("#00f1", [0, 0, 255, 17]),
350        ];
351
352        for (arg, expected) in tests {
353            assert_eq!(
354                Color::parse(arg).expect("color must parse").into_rgba8(),
355                expected
356            );
357        }
358
359        assert!(Color::parse("invalid").is_none());
360    }
361}