i_slint_core/graphics/
color.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5This module contains color related types for the run-time library.
6*/
7
8use crate::properties::InterpolatedPropertyValue;
9
10#[cfg(not(feature = "std"))]
11use num_traits::float::Float;
12
13/// RgbaColor stores the red, green, blue and alpha components of a color
14/// with the precision of the generic parameter T. For example if T is f32,
15/// the values are normalized between 0 and 1. If T is u8, they values range
16/// is 0 to 255.
17/// This is merely a helper class for use with [`Color`].
18#[derive(Copy, Clone, PartialEq, Debug, Default)]
19pub struct RgbaColor<T> {
20    /// The alpha component.
21    pub alpha: T,
22    /// The red channel.
23    pub red: T,
24    /// The green channel.
25    pub green: T,
26    /// The blue channel.
27    pub blue: T,
28}
29
30/// Color represents a color in the Slint run-time, represented using 8-bit channels for
31/// red, green, blue and the alpha (opacity).
32/// It can be conveniently converted using the `to_` and `from_` (a)rgb helper functions:
33/// ```
34/// # fn do_something_with_red_and_green(_:f32, _:f32) {}
35/// # fn do_something_with_red(_:u8) {}
36/// # use i_slint_core::graphics::{Color, RgbaColor};
37/// # let some_color = Color::from_rgb_u8(0, 0, 0);
38/// let col = some_color.to_argb_f32();
39/// do_something_with_red_and_green(col.red, col.green);
40///
41/// let RgbaColor { red, blue, green, .. } = some_color.to_argb_u8();
42/// do_something_with_red(red);
43///
44/// let new_col = Color::from(RgbaColor{ red: 0.5, green: 0.65, blue: 0.32, alpha: 1.});
45/// ```
46#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48#[repr(C)]
49pub struct Color {
50    red: u8,
51    green: u8,
52    blue: u8,
53    alpha: u8,
54}
55
56impl From<RgbaColor<u8>> for Color {
57    fn from(col: RgbaColor<u8>) -> Self {
58        Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
59    }
60}
61
62impl From<Color> for RgbaColor<u8> {
63    fn from(col: Color) -> Self {
64        RgbaColor { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
65    }
66}
67
68impl From<RgbaColor<u8>> for RgbaColor<f32> {
69    fn from(col: RgbaColor<u8>) -> Self {
70        Self {
71            red: (col.red as f32) / 255.0,
72            green: (col.green as f32) / 255.0,
73            blue: (col.blue as f32) / 255.0,
74            alpha: (col.alpha as f32) / 255.0,
75        }
76    }
77}
78
79impl From<Color> for RgbaColor<f32> {
80    fn from(col: Color) -> Self {
81        let u8col: RgbaColor<u8> = col.into();
82        u8col.into()
83    }
84}
85
86impl From<RgbaColor<f32>> for Color {
87    fn from(col: RgbaColor<f32>) -> Self {
88        Self {
89            red: (col.red * 255.).round() as u8,
90            green: (col.green * 255.).round() as u8,
91            blue: (col.blue * 255.).round() as u8,
92            alpha: (col.alpha * 255.).round() as u8,
93        }
94    }
95}
96
97impl Color {
98    /// Construct a color from an integer encoded as `0xAARRGGBB`
99    pub const fn from_argb_encoded(encoded: u32) -> Color {
100        Self {
101            red: (encoded >> 16) as u8,
102            green: (encoded >> 8) as u8,
103            blue: encoded as u8,
104            alpha: (encoded >> 24) as u8,
105        }
106    }
107
108    /// Returns `(alpha, red, green, blue)` encoded as u32
109    pub fn as_argb_encoded(&self) -> u32 {
110        ((self.red as u32) << 16)
111            | ((self.green as u32) << 8)
112            | (self.blue as u32)
113            | ((self.alpha as u32) << 24)
114    }
115
116    /// Construct a color from the alpha, red, green and blue color channel parameters.
117    pub const fn from_argb_u8(alpha: u8, red: u8, green: u8, blue: u8) -> Self {
118        Self { red, green, blue, alpha }
119    }
120
121    /// Construct a color from the red, green and blue color channel parameters. The alpha
122    /// channel will have the value 255.
123    pub const fn from_rgb_u8(red: u8, green: u8, blue: u8) -> Self {
124        Self::from_argb_u8(255, red, green, blue)
125    }
126
127    /// Construct a color from the alpha, red, green and blue color channel parameters.
128    pub fn from_argb_f32(alpha: f32, red: f32, green: f32, blue: f32) -> Self {
129        RgbaColor { alpha, red, green, blue }.into()
130    }
131
132    /// Construct a color from the red, green and blue color channel parameters. The alpha
133    /// channel will have the value 255.
134    pub fn from_rgb_f32(red: f32, green: f32, blue: f32) -> Self {
135        Self::from_argb_f32(1.0, red, green, blue)
136    }
137
138    /// Converts this color to an RgbaColor struct for easy destructuring.
139    pub fn to_argb_u8(&self) -> RgbaColor<u8> {
140        RgbaColor::from(*self)
141    }
142
143    /// Converts this color to an RgbaColor struct for easy destructuring.
144    pub fn to_argb_f32(&self) -> RgbaColor<f32> {
145        RgbaColor::from(*self)
146    }
147
148    /// Converts this color to the HSV color space.
149    pub fn to_hsva(&self) -> HsvaColor {
150        let rgba: RgbaColor<f32> = (*self).into();
151        rgba.into()
152    }
153
154    /// Construct a color from the hue, saturation, and value HSV color space parameters.
155    ///
156    /// Hue is between 0 and 360, the others parameters between 0 and 1.
157    pub fn from_hsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self {
158        let hsva = HsvaColor { hue, saturation, value, alpha };
159        <RgbaColor<f32>>::from(hsva).into()
160    }
161
162    /// Returns the red channel of the color as u8 in the range 0..255.
163    #[inline(always)]
164    pub fn red(self) -> u8 {
165        self.red
166    }
167
168    /// Returns the green channel of the color as u8 in the range 0..255.
169    #[inline(always)]
170    pub fn green(self) -> u8 {
171        self.green
172    }
173
174    /// Returns the blue channel of the color as u8 in the range 0..255.
175    #[inline(always)]
176    pub fn blue(self) -> u8 {
177        self.blue
178    }
179
180    /// Returns the alpha channel of the color as u8 in the range 0..255.
181    #[inline(always)]
182    pub fn alpha(self) -> u8 {
183        self.alpha
184    }
185
186    /// Returns a new version of this color that has the brightness increased
187    /// by the specified factor. This is done by converting the color to the HSV
188    /// color space and multiplying the brightness (value) with (1 + factor).
189    /// The result is converted back to RGB and the alpha channel is unchanged.
190    /// So for example `brighter(0.2)` will increase the brightness by 20%, and
191    /// calling `brighter(-0.5)` will return a color that's 50% darker.
192    #[must_use]
193    pub fn brighter(&self, factor: f32) -> Self {
194        let rgba: RgbaColor<f32> = (*self).into();
195        let mut hsva: HsvaColor = rgba.into();
196        hsva.value *= 1. + factor;
197        let rgba: RgbaColor<f32> = hsva.into();
198        rgba.into()
199    }
200
201    /// Returns a new version of this color that has the brightness decreased
202    /// by the specified factor. This is done by converting the color to the HSV
203    /// color space and dividing the brightness (value) by (1 + factor). The
204    /// result is converted back to RGB and the alpha channel is unchanged.
205    /// So for example `darker(0.3)` will decrease the brightness by 30%.
206    #[must_use]
207    pub fn darker(&self, factor: f32) -> Self {
208        let rgba: RgbaColor<f32> = (*self).into();
209        let mut hsva: HsvaColor = rgba.into();
210        hsva.value /= 1. + factor;
211        let rgba: RgbaColor<f32> = hsva.into();
212        rgba.into()
213    }
214
215    /// Returns a new version of this color with the opacity decreased by `factor`.
216    ///
217    /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
218    ///
219    /// # Examples
220    /// Decreasing the opacity of a red color by half:
221    /// ```
222    /// # use i_slint_core::graphics::Color;
223    /// let red = Color::from_argb_u8(255, 255, 0, 0);
224    /// assert_eq!(red.transparentize(0.5), Color::from_argb_u8(128, 255, 0, 0));
225    /// ```
226    ///
227    /// Decreasing the opacity of a blue color by 20%:
228    /// ```
229    /// # use i_slint_core::graphics::Color;
230    /// let blue = Color::from_argb_u8(200, 0, 0, 255);
231    /// assert_eq!(blue.transparentize(0.2), Color::from_argb_u8(160, 0, 0, 255));
232    /// ```
233    ///
234    /// Negative values increase the opacity
235    ///
236    /// ```
237    /// # use i_slint_core::graphics::Color;
238    /// let blue = Color::from_argb_u8(200, 0, 0, 255);
239    /// assert_eq!(blue.transparentize(-0.1), Color::from_argb_u8(220, 0, 0, 255));
240    /// ```
241    #[must_use]
242    pub fn transparentize(&self, factor: f32) -> Self {
243        let mut color = *self;
244        color.alpha = ((self.alpha as f32) * (1.0 - factor))
245            .round()
246            .clamp(u8::MIN as f32, u8::MAX as f32) as u8;
247        color
248    }
249
250    /// Returns a new color that is a mix of this color and `other`. The specified factor is
251    /// clamped to be between `0.0` and `1.0` and then applied to this color, while `1.0 - factor`
252    /// is applied to `other`.
253    ///
254    /// # Examples
255    /// Mix red with black half-and-half:
256    /// ```
257    /// # use i_slint_core::graphics::Color;
258    /// let red = Color::from_rgb_u8(255, 0, 0);
259    /// let black = Color::from_rgb_u8(0, 0, 0);
260    /// assert_eq!(red.mix(&black, 0.5), Color::from_rgb_u8(128, 0, 0));
261    /// ```
262    ///
263    /// Mix Purple with OrangeRed,  with `75%` purpe and `25%` orange red ratio:
264    /// ```
265    /// # use i_slint_core::graphics::Color;
266    /// let purple = Color::from_rgb_u8(128, 0, 128);
267    /// let orange_red = Color::from_rgb_u8(255, 69, 0);
268    /// assert_eq!(purple.mix(&orange_red, 0.75), Color::from_rgb_u8(160, 17, 96));
269    /// ```
270    #[must_use]
271    pub fn mix(&self, other: &Self, factor: f32) -> Self {
272        // * NOTE: The opacity (`alpha` as a "percentage") of each color involved
273        // *       must be taken into account when mixing them. Because of this,
274        // *       we cannot just interpolate between them.
275        // * NOTE: Considering the spec (textual):
276        // *       <https://github.com/sass/sass/blob/47d30713765b975c86fa32ec359ed16e83ad1ecc/spec/built-in-modules/color.md#mix>
277
278        fn lerp(v1: u8, v2: u8, f: f32) -> u8 {
279            (v1 as f32 * f + v2 as f32 * (1.0 - f)).clamp(u8::MIN as f32, u8::MAX as f32).round()
280                as u8
281        }
282
283        let original_factor = factor.clamp(0.0, 1.0);
284
285        let self_opacity = RgbaColor::<f32>::from(*self).alpha;
286        let other_opacity = RgbaColor::<f32>::from(*other).alpha;
287
288        let normal_weight = 2.0 * original_factor - 1.0;
289        let alpha_distance = self_opacity - other_opacity;
290        let weight_by_distance = normal_weight * alpha_distance;
291
292        // As to not divide by 0.0
293        let combined_weight = if weight_by_distance == -1.0 {
294            normal_weight
295        } else {
296            (normal_weight + alpha_distance) / (1.0 + weight_by_distance)
297        };
298
299        let channels_factor = (combined_weight + 1.0) / 2.0;
300
301        let red = lerp(self.red, other.red, channels_factor);
302        let green = lerp(self.green, other.green, channels_factor);
303        let blue = lerp(self.blue, other.blue, channels_factor);
304
305        let alpha = lerp(self.alpha, other.alpha, original_factor);
306
307        Self { red, green, blue, alpha }
308    }
309
310    /// Returns a new version of this color with the opacity set to `alpha`.
311    #[must_use]
312    pub fn with_alpha(&self, alpha: f32) -> Self {
313        let mut rgba: RgbaColor<f32> = (*self).into();
314        rgba.alpha = alpha.clamp(0.0, 1.0);
315        rgba.into()
316    }
317}
318
319impl InterpolatedPropertyValue for Color {
320    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
321        target_value.mix(self, t)
322    }
323}
324
325impl core::fmt::Display for Color {
326    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327        write!(f, "argb({}, {}, {}, {})", self.alpha, self.red, self.green, self.blue)
328    }
329}
330
331/// HsvaColor stores the hue, saturation, value and alpha components of a color
332/// in the HSV color space as `f32 ` fields.
333/// This is merely a helper struct for use with [`Color`].
334#[derive(Copy, Clone, PartialOrd, Debug, Default)]
335#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
336pub struct HsvaColor {
337    /// The hue component in degrees between 0 and 360.
338    pub hue: f32,
339    /// The saturation component, between 0 and 1.
340    pub saturation: f32,
341    /// The value component, between 0 and 1.
342    pub value: f32,
343    /// The alpha component, between 0 and 1.
344    pub alpha: f32,
345}
346
347impl PartialEq for HsvaColor {
348    fn eq(&self, other: &Self) -> bool {
349        (self.hue - other.hue).abs() < 0.00001
350            && (self.saturation - other.saturation).abs() < 0.00001
351            && (self.value - other.value).abs() < 0.00001
352            && (self.alpha - other.alpha).abs() < 0.00001
353    }
354}
355
356impl From<RgbaColor<f32>> for HsvaColor {
357    fn from(col: RgbaColor<f32>) -> Self {
358        // RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
359
360        let red = col.red;
361        let green = col.green;
362        let blue = col.blue;
363
364        let min = red.min(green).min(blue);
365        let max = red.max(green).max(blue);
366        let chroma = max - min;
367
368        #[allow(clippy::float_cmp)] // `max` is either `red`, `green` or `blue`
369        let hue = num_traits::Euclid::rem_euclid(
370            &(60.
371                * if chroma == 0.0 {
372                    0.0
373                } else if max == red {
374                    ((green - blue) / chroma) % 6.0
375                } else if max == green {
376                    2. + (blue - red) / chroma
377                } else {
378                    4. + (red - green) / chroma
379                }),
380            &360.0,
381        );
382        let saturation = if max == 0. { 0. } else { chroma / max };
383
384        Self { hue, saturation, value: max, alpha: col.alpha }
385    }
386}
387
388impl From<HsvaColor> for RgbaColor<f32> {
389    fn from(col: HsvaColor) -> Self {
390        // RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
391
392        let chroma = col.saturation * col.value;
393
394        let hue = num_traits::Euclid::rem_euclid(&col.hue, &360.0);
395
396        let x = chroma * (1. - ((hue / 60.) % 2. - 1.).abs());
397
398        let (red, green, blue) = match (hue / 60.0) as usize {
399            0 => (chroma, x, 0.),
400            1 => (x, chroma, 0.),
401            2 => (0., chroma, x),
402            3 => (0., x, chroma),
403            4 => (x, 0., chroma),
404            5 => (chroma, 0., x),
405            _ => (0., 0., 0.),
406        };
407
408        let m = col.value - chroma;
409
410        Self { red: red + m, green: green + m, blue: blue + m, alpha: col.alpha }
411    }
412}
413
414impl From<HsvaColor> for Color {
415    fn from(value: HsvaColor) -> Self {
416        RgbaColor::from(value).into()
417    }
418}
419
420impl From<Color> for HsvaColor {
421    fn from(value: Color) -> Self {
422        value.to_hsva()
423    }
424}
425
426#[test]
427fn test_rgb_to_hsv() {
428    // White
429    assert_eq!(
430        HsvaColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 }),
431        HsvaColor { hue: 0., saturation: 0., value: 1., alpha: 0.5 }
432    );
433    assert_eq!(
434        RgbaColor::<f32>::from(HsvaColor { hue: 0., saturation: 0., value: 1., alpha: 0.3 }),
435        RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.3 }
436    );
437
438    // #8a0c77ff ensure the hue ends up positive
439    assert_eq!(
440        HsvaColor::from(Color::from_argb_u8(0xff, 0x8a, 0xc, 0x77,).to_argb_f32()),
441        HsvaColor { hue: 309.0476, saturation: 0.9130435, value: 0.5411765, alpha: 1.0 }
442    );
443
444    let received = RgbaColor::<f32>::from(HsvaColor {
445        hue: 309.0476,
446        saturation: 0.9130435,
447        value: 0.5411765,
448        alpha: 1.0,
449    });
450    let expected = Color::from_argb_u8(0xff, 0x8a, 0xc, 0x77).to_argb_f32();
451
452    assert!(
453        (received.alpha - expected.alpha).abs() < 0.00001
454            && (received.red - expected.red).abs() < 0.00001
455            && (received.green - expected.green).abs() < 0.00001
456            && (received.blue - expected.blue).abs() < 0.00001
457    );
458
459    // Bright greenish, verified via colorizer.org
460    assert_eq!(
461        HsvaColor::from(RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }),
462        HsvaColor { hue: 120., saturation: 1., value: 0.9, alpha: 1.0 }
463    );
464    assert_eq!(
465        RgbaColor::<f32>::from(HsvaColor { hue: 120., saturation: 1., value: 0.9, alpha: 1.0 }),
466        RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }
467    );
468
469    // Hue should wrap around 360deg i.e. 480 == 120 && -240 == 240
470    assert_eq!(
471        RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 },
472        RgbaColor::<f32>::from(HsvaColor { hue: 480., saturation: 1., value: 0.9, alpha: 1.0 }),
473    );
474    assert_eq!(
475        RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 },
476        RgbaColor::<f32>::from(HsvaColor { hue: -240., saturation: 1., value: 0.9, alpha: 1.0 }),
477    );
478}
479
480#[test]
481fn test_brighter_darker() {
482    let blue = Color::from_rgb_u8(0, 0, 128);
483    assert_eq!(blue.brighter(0.5), Color::from_rgb_u8(0, 0, 192));
484    assert_eq!(blue.darker(0.5), Color::from_rgb_u8(0, 0, 85));
485}
486
487#[test]
488fn test_transparent_transition() {
489    let color = Color::from_argb_u8(0, 0, 0, 0);
490    let interpolated = color.interpolate(&Color::from_rgb_u8(211, 211, 211), 0.25);
491    assert_eq!(interpolated, Color::from_argb_u8(64, 211, 211, 211));
492    let interpolated = color.interpolate(&Color::from_rgb_u8(211, 211, 211), 0.5);
493    assert_eq!(interpolated, Color::from_argb_u8(128, 211, 211, 211));
494    let interpolated = color.interpolate(&Color::from_rgb_u8(211, 211, 211), 0.75);
495    assert_eq!(interpolated, Color::from_argb_u8(191, 211, 211, 211));
496}
497
498#[cfg(feature = "ffi")]
499pub(crate) mod ffi {
500    #![allow(unsafe_code)]
501    use super::*;
502
503    #[unsafe(no_mangle)]
504    pub unsafe extern "C" fn slint_color_brighter(col: &Color, factor: f32, out: *mut Color) {
505        core::ptr::write(out, col.brighter(factor))
506    }
507
508    #[unsafe(no_mangle)]
509    pub unsafe extern "C" fn slint_color_darker(col: &Color, factor: f32, out: *mut Color) {
510        core::ptr::write(out, col.darker(factor))
511    }
512
513    #[unsafe(no_mangle)]
514    pub unsafe extern "C" fn slint_color_transparentize(col: &Color, factor: f32, out: *mut Color) {
515        core::ptr::write(out, col.transparentize(factor))
516    }
517
518    #[unsafe(no_mangle)]
519    pub unsafe extern "C" fn slint_color_mix(
520        col1: &Color,
521        col2: &Color,
522        factor: f32,
523        out: *mut Color,
524    ) {
525        core::ptr::write(out, col1.mix(col2, factor))
526    }
527
528    #[unsafe(no_mangle)]
529    pub unsafe extern "C" fn slint_color_with_alpha(col: &Color, alpha: f32, out: *mut Color) {
530        core::ptr::write(out, col.with_alpha(alpha))
531    }
532
533    #[unsafe(no_mangle)]
534    pub extern "C" fn slint_color_to_hsva(
535        col: &Color,
536        h: &mut f32,
537        s: &mut f32,
538        v: &mut f32,
539        a: &mut f32,
540    ) {
541        let hsv = col.to_hsva();
542        *h = hsv.hue;
543        *s = hsv.saturation;
544        *v = hsv.value;
545        *a = hsv.alpha;
546    }
547
548    #[unsafe(no_mangle)]
549    pub extern "C" fn slint_color_from_hsva(h: f32, s: f32, v: f32, a: f32) -> Color {
550        Color::from_hsva(h, s, v, a)
551    }
552}