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 {
94            r: gamma_component(r),
95            g: gamma_component(g),
96            b: 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
200impl From<[f32; 3]> for Color {
201    fn from([r, g, b]: [f32; 3]) -> Self {
202        Color::new(r, g, b, 1.0)
203    }
204}
205
206impl From<[f32; 4]> for Color {
207    fn from([r, g, b, a]: [f32; 4]) -> Self {
208        Color::new(r, g, b, a)
209    }
210}
211
212/// Creates a [`Color`] with shorter and cleaner syntax.
213///
214/// # Examples
215///
216/// ```
217/// # use iced_core::{Color, color};
218/// assert_eq!(color!(0, 0, 0), Color::BLACK);
219/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
220/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
221/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
222/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0));
223/// ```
224#[macro_export]
225macro_rules! color {
226    ($r:expr, $g:expr, $b:expr) => {
227        $crate::Color::from_rgb8($r, $g, $b)
228    };
229    ($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
230    ($hex:expr) => {{ $crate::color!($hex, 1.0) }};
231    ($hex:expr, $a:expr) => {{
232        let hex = $hex as u32;
233
234        debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
235
236        let r = (hex & 0xff0000) >> 16;
237        let g = (hex & 0xff00) >> 8;
238        let b = (hex & 0xff);
239
240        $crate::color!(r as u8, g as u8, b as u8, $a)
241    }};
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn parse() {
250        let tests = [
251            ("#ff0000", [255, 0, 0, 255]),
252            ("00ff0080", [0, 255, 0, 128]),
253            ("#F80", [255, 136, 0, 255]),
254            ("#00f1", [0, 0, 255, 17]),
255        ];
256
257        for (arg, expected) in tests {
258            assert_eq!(
259                Color::parse(arg).expect("color must parse").into_rgba8(),
260                expected
261            );
262        }
263
264        assert!(Color::parse("invalid").is_none());
265    }
266}