Skip to main content

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#[cfg(not(feature = "std"))]
10#[allow(unused_imports)]
11use num_traits::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#[cfg(feature = "32-bit-color")]
31type Channel = f32;
32#[cfg(not(feature = "32-bit-color"))]
33type Channel = u8;
34
35/// Color represents a color in the Slint run-time, represented using 8-bit channels for
36/// red, green, blue and the alpha (opacity).
37/// It can be conveniently converted using the `to_` and `from_` (a)rgb helper functions:
38/// ```
39/// # fn do_something_with_red_and_green(_:f32, _:f32) {}
40/// # fn do_something_with_red(_:u8) {}
41/// # use i_slint_core::graphics::{Color, RgbaColor};
42/// # let some_color = Color::from_rgb_u8(0, 0, 0);
43/// let col = some_color.to_argb_f32();
44/// do_something_with_red_and_green(col.red, col.green);
45///
46/// let RgbaColor { red, blue, green, .. } = some_color.to_argb_u8();
47/// do_something_with_red(red);
48///
49/// let new_col = Color::from(RgbaColor{ red: 0.5, green: 0.65, blue: 0.32, alpha: 1.});
50/// ```
51#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53#[repr(C)]
54pub struct Color {
55    red: Channel,
56    green: Channel,
57    blue: Channel,
58    alpha: Channel,
59}
60
61// until slint uses rust 1.90 as MSRV.
62const fn round(mut value: f32) -> u8 {
63    if value % 1.0 > 0.5 {
64        value += 0.5;
65    }
66
67    value as _
68}
69
70const fn quantize(value: f32) -> u8 {
71    round(value * 255.0)
72}
73
74const fn unquantize(value: u8) -> f32 {
75    (value as f32) / 255.0
76}
77
78#[test]
79fn unquantize_roundtrip() {
80    for v in 0..=255 {
81        assert_eq!(v, quantize(unquantize(v)));
82    }
83}
84
85impl From<RgbaColor<u8>> for RgbaColor<f32> {
86    #[inline]
87    fn from(col: RgbaColor<u8>) -> Self {
88        Self {
89            red: unquantize(col.red),
90            green: unquantize(col.green),
91            blue: unquantize(col.blue),
92            alpha: unquantize(col.alpha),
93        }
94    }
95}
96
97impl From<RgbaColor<f32>> for RgbaColor<u8> {
98    #[inline]
99    fn from(col: RgbaColor<f32>) -> Self {
100        Self {
101            red: quantize(col.red),
102            green: quantize(col.green),
103            blue: quantize(col.blue),
104            alpha: quantize(col.alpha),
105        }
106    }
107}
108
109impl From<Color> for RgbaColor<f32> {
110    #[inline]
111    fn from(col: Color) -> Self {
112        #[cfg(feature = "32-bit-color")]
113        {
114            Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
115        }
116        #[cfg(not(feature = "32-bit-color"))]
117        {
118            let col: RgbaColor<u8> = col.into();
119            col.into()
120        }
121    }
122}
123
124impl From<RgbaColor<f32>> for Color {
125    #[inline]
126    fn from(col: RgbaColor<f32>) -> Self {
127        #[cfg(feature = "32-bit-color")]
128        {
129            Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
130        }
131        #[cfg(not(feature = "32-bit-color"))]
132        {
133            let col: RgbaColor<u8> = col.into();
134            col.into()
135        }
136    }
137}
138
139impl From<RgbaColor<u8>> for Color {
140    #[inline]
141    fn from(col: RgbaColor<u8>) -> Self {
142        #[cfg(feature = "32-bit-color")]
143        {
144            let col: RgbaColor<f32> = col.into();
145            col.into()
146        }
147        #[cfg(not(feature = "32-bit-color"))]
148        {
149            Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
150        }
151    }
152}
153
154impl From<Color> for RgbaColor<u8> {
155    #[inline]
156    fn from(col: Color) -> Self {
157        #[cfg(feature = "32-bit-color")]
158        {
159            let col: RgbaColor<f32> = col.into();
160            col.into()
161        }
162        #[cfg(not(feature = "32-bit-color"))]
163        {
164            Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
165        }
166    }
167}
168
169impl Color {
170    /// Construct a color from an integer encoded as `0xAARRGGBB`
171    pub const fn from_argb_encoded(encoded: u32) -> Color {
172        Self::from_argb_u8(
173            (encoded >> 24) as u8,
174            (encoded >> 16) as u8,
175            (encoded >> 8) as u8,
176            encoded as u8,
177        )
178    }
179
180    /// Returns `(alpha, red, green, blue)` encoded as u32
181    pub fn as_argb_encoded(&self) -> u32 {
182        let col: RgbaColor<u8> = (*self).into();
183        ((col.red as u32) << 16)
184            | ((col.green as u32) << 8)
185            | (col.blue as u32)
186            | ((col.alpha as u32) << 24)
187    }
188
189    /// Construct a color from the alpha, red, green and blue color channel parameters.
190    pub const fn from_argb_u8(alpha: u8, red: u8, green: u8, blue: u8) -> Self {
191        #[cfg(feature = "32-bit-color")]
192        {
193            Self {
194                red: unquantize(red),
195                green: unquantize(green),
196                blue: unquantize(blue),
197                alpha: unquantize(alpha),
198            }
199        }
200        #[cfg(not(feature = "32-bit-color"))]
201        {
202            Self { red, green, blue, alpha }
203        }
204    }
205
206    /// Construct a color from the red, green and blue color channel parameters. The alpha
207    /// channel will have the value 255.
208    pub const fn from_rgb_u8(red: u8, green: u8, blue: u8) -> Self {
209        Self::from_argb_u8(255, red, green, blue)
210    }
211
212    /// Construct a color from the alpha, red, green and blue color channel parameters.
213    pub fn from_argb_f32(alpha: f32, red: f32, green: f32, blue: f32) -> Self {
214        RgbaColor { alpha, red, green, blue }.into()
215    }
216
217    /// Construct a color from the red, green and blue color channel parameters. The alpha
218    /// channel will have the value 255.
219    pub fn from_rgb_f32(red: f32, green: f32, blue: f32) -> Self {
220        Self::from_argb_f32(1.0, red, green, blue)
221    }
222
223    /// Converts this color to an RgbaColor struct for easy destructuring.
224    pub fn to_argb_u8(&self) -> RgbaColor<u8> {
225        RgbaColor::from(*self)
226    }
227
228    /// Converts this color to an RgbaColor struct for easy destructuring.
229    pub fn to_argb_f32(&self) -> RgbaColor<f32> {
230        RgbaColor::from(*self)
231    }
232
233    /// Converts this color to the HSV color space.
234    pub fn to_hsva(&self) -> HsvaColor {
235        let rgba: RgbaColor<f32> = (*self).into();
236        rgba.into()
237    }
238
239    /// Construct a color from the hue, saturation, and value HSV color space parameters.
240    ///
241    /// Hue is between 0 and 360, the others parameters between 0 and 1.
242    pub fn from_hsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self {
243        let hsva = HsvaColor { hue, saturation, value, alpha };
244        <RgbaColor<f32>>::from(hsva).into()
245    }
246
247    /// Converts this color to the Oklch color space.
248    ///
249    /// Oklch is a perceptually uniform color space with:
250    /// - Lightness (L): 0 to 1
251    /// - Chroma (C): typically 0 to ~0.4
252    /// - Hue (h): 0 to 360 degrees
253    pub fn to_oklch(&self) -> OklchColor {
254        let rgba: RgbaColor<f32> = (*self).into();
255        rgba.into()
256    }
257
258    /// Construct a color from the Oklch color space parameters.
259    ///
260    /// - `lightness`: 0 to 1 (black to white)
261    /// - `chroma`: typically 0 to ~0.4 (grayscale to vivid)
262    /// - `hue`: 0 to 360 degrees
263    /// - `alpha`: 0 to 1
264    pub fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
265        let oklch = OklchColor { lightness, chroma, hue, alpha };
266        <RgbaColor<f32>>::from(oklch).into()
267    }
268
269    /// Returns the red channel of the color as u8 in the range 0..255.
270    #[inline(always)]
271    pub fn red(self) -> u8 {
272        RgbaColor::<u8>::from(self).red
273    }
274
275    /// Returns the green channel of the color as u8 in the range 0..255.
276    #[inline(always)]
277    pub fn green(self) -> u8 {
278        RgbaColor::<u8>::from(self).green
279    }
280
281    /// Returns the blue channel of the color as u8 in the range 0..255.
282    #[inline(always)]
283    pub fn blue(self) -> u8 {
284        RgbaColor::<u8>::from(self).blue
285    }
286
287    /// Returns the alpha channel of the color as u8 in the range 0..255.
288    #[inline(always)]
289    pub fn alpha(self) -> u8 {
290        RgbaColor::<u8>::from(self).alpha
291    }
292
293    /// Returns a new version of this color that has the brightness increased
294    /// by the specified factor. This is done by converting the color to the HSV
295    /// color space and multiplying the brightness (value) with (1 + factor).
296    /// The result is converted back to RGB and the alpha channel is unchanged.
297    /// So for example `brighter(0.2)` will increase the brightness by 20%, and
298    /// calling `brighter(-0.5)` will return a color that's 50% darker.
299    #[must_use]
300    pub fn brighter(&self, factor: f32) -> Self {
301        let rgba: RgbaColor<f32> = (*self).into();
302        let mut hsva: HsvaColor = rgba.into();
303        hsva.value *= 1. + factor;
304        let rgba: RgbaColor<f32> = hsva.into();
305        rgba.into()
306    }
307
308    /// Returns a new version of this color that has the brightness decreased
309    /// by the specified factor. This is done by converting the color to the HSV
310    /// color space and dividing the brightness (value) by (1 + factor). The
311    /// result is converted back to RGB and the alpha channel is unchanged.
312    /// So for example `darker(0.3)` will decrease the brightness by 30%.
313    #[must_use]
314    pub fn darker(&self, factor: f32) -> Self {
315        let rgba: RgbaColor<f32> = (*self).into();
316        let mut hsva: HsvaColor = rgba.into();
317        hsva.value /= 1. + factor;
318        let rgba: RgbaColor<f32> = hsva.into();
319        rgba.into()
320    }
321
322    /// Returns a new version of this color with the opacity decreased by `factor`.
323    ///
324    /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
325    ///
326    /// # Examples
327    /// Decreasing the opacity of a red color by half:
328    /// ```
329    /// # use i_slint_core::graphics::Color;
330    /// let red = Color::from_argb_f32(1.0, 1.0, 0.0, 0.0);
331    /// assert_eq!(red.transparentize(0.5), Color::from_argb_f32(0.5, 1.0, 0.0, 0.0));
332    /// ```
333    ///
334    /// Decreasing the opacity of a blue color by 20%:
335    /// ```
336    /// # use i_slint_core::graphics::Color;
337    /// let blue = Color::from_argb_f32(1.0, 0.0, 0.0, 1.0);
338    /// assert_eq!(blue.transparentize(0.2), Color::from_argb_f32(0.8, 0.0, 0.0, 1.0));
339    /// ```
340    ///
341    /// Negative values increase the opacity
342    ///
343    /// ```
344    /// # use i_slint_core::graphics::Color;
345    /// let blue = Color::from_argb_f32(0.5, 0.0, 0.0, 1.0);
346    /// assert_eq!(blue.transparentize(-0.1), Color::from_argb_f32(0.55, 0.0, 0.0, 1.0));
347    /// ```
348    #[must_use]
349    pub fn transparentize(&self, factor: f32) -> Self {
350        let mut col: RgbaColor<f32> = (*self).into();
351        col.alpha = (col.alpha * (1.0 - factor)).clamp(0.0, 1.0);
352        col.into()
353    }
354
355    /// Returns a new color that is a mix of this color and `other`. The specified factor is
356    /// clamped to be between `0.0` and `1.0` and then applied to this color, while `1.0 - factor`
357    /// is applied to `other`.
358    ///
359    /// # Examples
360    /// Mix red with black half-and-half:
361    /// ```
362    /// # use i_slint_core::graphics::Color;
363    /// let red = Color::from_rgb_f32(1.0, 0.0, 0.0);
364    /// let black = Color::from_rgb_f32(0.0, 0.0, 0.0);
365    /// assert_eq!(red.mix(&black, 0.5), Color::from_rgb_f32(0.5, 0.0, 0.0));
366    /// ```
367    ///
368    /// Mix Purple with OrangeRed,  with `75%` purpe and `25%` orange red ratio:
369    /// ```
370    /// # use i_slint_core::graphics::{Color, RgbaColor};
371    /// let purple = Color::from_rgb_u8(128, 0, 128);
372    /// let orange_red = Color::from_rgb_u8(255, 69, 0);
373    /// assert_eq!(purple.mix(&orange_red, 0.75), Color::from_rgb_f32(0.6264706, 0.06764706, 0.37647063));
374    /// ```
375    #[must_use]
376    pub fn mix(&self, other: &Self, factor: f32) -> Self {
377        // * NOTE: The opacity (`alpha` as a "percentage") of each color involved
378        // *       must be taken into account when mixing them. Because of this,
379        // *       we cannot just interpolate between them.
380        // * NOTE: Considering the spec (textual):
381        // *       <https://github.com/sass/sass/blob/47d30713765b975c86fa32ec359ed16e83ad1ecc/spec/built-in-modules/color.md#mix>
382
383        fn lerp(v1: f32, v2: f32, f: f32) -> f32 {
384            (v1 * f + v2 * (1.0 - f)).clamp(0.0, 1.0)
385        }
386
387        let original_factor = factor.clamp(0.0, 1.0);
388
389        let col = RgbaColor::<f32>::from(*self);
390        let other = RgbaColor::<f32>::from(*other);
391
392        let normal_weight = 2.0 * original_factor - 1.0;
393        let alpha_distance = col.alpha - other.alpha;
394        let weight_by_distance = normal_weight * alpha_distance;
395
396        // As to not divide by 0.0
397        let combined_weight = if weight_by_distance == -1.0 {
398            normal_weight
399        } else {
400            (normal_weight + alpha_distance) / (1.0 + weight_by_distance)
401        };
402
403        let channels_factor = (combined_weight + 1.0) / 2.0;
404
405        let red = lerp(col.red, other.red, channels_factor);
406        let green = lerp(col.green, other.green, channels_factor);
407        let blue = lerp(col.blue, other.blue, channels_factor);
408
409        let alpha = lerp(col.alpha, other.alpha, original_factor);
410
411        RgbaColor { red, green, blue, alpha }.into()
412    }
413
414    /// Returns a new version of this color with the opacity set to `alpha`.
415    #[must_use]
416    pub fn with_alpha(&self, alpha: f32) -> Self {
417        let mut rgba: RgbaColor<f32> = (*self).into();
418        rgba.alpha = alpha.clamp(0.0, 1.0);
419        rgba.into()
420    }
421}
422
423impl InterpolatedPropertyValue for Color {
424    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
425        target_value.mix(self, t)
426    }
427}
428
429impl core::fmt::Display for Color {
430    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
431        write!(f, "argb({}, {}, {}, {})", self.alpha(), self.red(), self.green(), self.blue())
432    }
433}
434
435/// HsvaColor stores the hue, saturation, value and alpha components of a color
436/// in the HSV color space as `f32 ` fields.
437/// This is merely a helper struct for use with [`Color`].
438#[derive(Copy, Clone, PartialOrd, Debug, Default)]
439#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
440pub struct HsvaColor {
441    /// The hue component in degrees between 0 and 360.
442    pub hue: f32,
443    /// The saturation component, between 0 and 1.
444    pub saturation: f32,
445    /// The value component, between 0 and 1.
446    pub value: f32,
447    /// The alpha component, between 0 and 1.
448    pub alpha: f32,
449}
450
451impl PartialEq for HsvaColor {
452    fn eq(&self, other: &Self) -> bool {
453        (self.hue - other.hue).abs() < 0.00001
454            && (self.saturation - other.saturation).abs() < 0.00001
455            && (self.value - other.value).abs() < 0.00001
456            && (self.alpha - other.alpha).abs() < 0.00001
457    }
458}
459
460impl From<RgbaColor<f32>> for HsvaColor {
461    fn from(col: RgbaColor<f32>) -> Self {
462        // RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
463
464        let red = col.red;
465        let green = col.green;
466        let blue = col.blue;
467
468        let min = red.min(green).min(blue);
469        let max = red.max(green).max(blue);
470        let chroma = max - min;
471
472        #[allow(clippy::float_cmp)] // `max` is either `red`, `green` or `blue`
473        let hue = num_traits::Euclid::rem_euclid(
474            &(60.
475                * if chroma == 0.0 {
476                    0.0
477                } else if max == red {
478                    ((green - blue) / chroma) % 6.0
479                } else if max == green {
480                    2. + (blue - red) / chroma
481                } else {
482                    4. + (red - green) / chroma
483                }),
484            &360.0,
485        );
486        let saturation = if max == 0. { 0. } else { chroma / max };
487
488        Self { hue, saturation, value: max, alpha: col.alpha }
489    }
490}
491
492impl From<HsvaColor> for RgbaColor<f32> {
493    fn from(col: HsvaColor) -> Self {
494        // RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
495
496        let chroma = col.saturation * col.value;
497
498        let hue = num_traits::Euclid::rem_euclid(&col.hue, &360.0);
499
500        let x = chroma * (1. - ((hue / 60.) % 2. - 1.).abs());
501
502        let (red, green, blue) = match (hue / 60.0) as usize {
503            0 => (chroma, x, 0.),
504            1 => (x, chroma, 0.),
505            2 => (0., chroma, x),
506            3 => (0., x, chroma),
507            4 => (x, 0., chroma),
508            5 => (chroma, 0., x),
509            _ => (0., 0., 0.),
510        };
511
512        let m = col.value - chroma;
513
514        Self { red: red + m, green: green + m, blue: blue + m, alpha: col.alpha }
515    }
516}
517
518impl From<HsvaColor> for Color {
519    fn from(value: HsvaColor) -> Self {
520        RgbaColor::from(value).into()
521    }
522}
523
524impl From<Color> for HsvaColor {
525    fn from(value: Color) -> Self {
526        value.to_hsva()
527    }
528}
529
530/// OklchColor stores the lightness, chroma, hue and alpha components of a color
531/// in the Oklch color space as `f32` fields.
532/// Oklch is a perceptually uniform color space, useful for color manipulation.
533/// This is merely a helper struct for use with [`Color`].
534///
535/// Reference: <https://bottosson.github.io/posts/oklab/>
536#[derive(Copy, Clone, PartialOrd, Debug, Default)]
537#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
538pub struct OklchColor {
539    /// The lightness component, between 0 (black) and 1 (white).
540    pub lightness: f32,
541    /// The chroma component (color intensity), typically between 0 and about 0.4.
542    pub chroma: f32,
543    /// The hue component in degrees between 0 and 360.
544    pub hue: f32,
545    /// The alpha component, between 0 and 1.
546    pub alpha: f32,
547}
548
549impl PartialEq for OklchColor {
550    fn eq(&self, other: &Self) -> bool {
551        (self.lightness - other.lightness).abs() < 0.00001
552            && (self.chroma - other.chroma).abs() < 0.00001
553            && (self.hue - other.hue).abs() < 0.00001
554            && (self.alpha - other.alpha).abs() < 0.00001
555    }
556}
557
558/// Helper struct for Oklab color space (intermediate representation).
559#[derive(Copy, Clone, Debug)]
560struct OklabColor {
561    l: f32,
562    a: f32,
563    b: f32,
564    alpha: f32,
565}
566
567impl From<OklchColor> for OklabColor {
568    fn from(oklch: OklchColor) -> Self {
569        let hue_rad = oklch.hue * core::f32::consts::PI / 180.0;
570        Self {
571            l: oklch.lightness,
572            a: oklch.chroma * hue_rad.cos(),
573            b: oklch.chroma * hue_rad.sin(),
574            alpha: oklch.alpha,
575        }
576    }
577}
578
579impl From<OklabColor> for OklchColor {
580    fn from(oklab: OklabColor) -> Self {
581        let chroma = (oklab.a * oklab.a + oklab.b * oklab.b).sqrt();
582        let hue = if chroma < 0.00001 {
583            0.0
584        } else {
585            let hue_rad = oklab.b.atan2(oklab.a);
586            num_traits::Euclid::rem_euclid(&(hue_rad * 180.0 / core::f32::consts::PI), &360.0)
587        };
588        Self { lightness: oklab.l, chroma, hue, alpha: oklab.alpha }
589    }
590}
591
592/// Convert sRGB component to linear RGB.
593fn srgb_to_linear(c: f32) -> f32 {
594    if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
595}
596
597/// Convert linear RGB component to sRGB.
598fn linear_to_srgb(c: f32) -> f32 {
599    if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
600}
601
602impl From<RgbaColor<f32>> for OklabColor {
603    fn from(col: RgbaColor<f32>) -> Self {
604        // Convert sRGB to linear RGB
605        let r = srgb_to_linear(col.red);
606        let g = srgb_to_linear(col.green);
607        let b = srgb_to_linear(col.blue);
608
609        // Linear RGB to LMS (using Oklab M1 matrix)
610        let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
611        let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
612        let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
613
614        // Cube root
615        let l_ = l.cbrt();
616        let m_ = m.cbrt();
617        let s_ = s.cbrt();
618
619        // LMS' to Oklab (using M2 matrix)
620        Self {
621            l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
622            a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
623            b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
624            alpha: col.alpha,
625        }
626    }
627}
628
629impl From<OklabColor> for RgbaColor<f32> {
630    fn from(oklab: OklabColor) -> Self {
631        // Oklab to LMS' (inverse of M2 matrix)
632        let l_ = oklab.l + 0.3963377774 * oklab.a + 0.2158037573 * oklab.b;
633        let m_ = oklab.l - 0.1055613458 * oklab.a - 0.0638541728 * oklab.b;
634        let s_ = oklab.l - 0.0894841775 * oklab.a - 1.2914855480 * oklab.b;
635
636        // Cube to get LMS
637        let l = l_ * l_ * l_;
638        let m = m_ * m_ * m_;
639        let s = s_ * s_ * s_;
640
641        // LMS to linear RGB (inverse of M1 matrix)
642        let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
643        let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
644        let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
645
646        // Convert linear RGB to sRGB and clamp
647        Self {
648            red: linear_to_srgb(r).clamp(0.0, 1.0),
649            green: linear_to_srgb(g).clamp(0.0, 1.0),
650            blue: linear_to_srgb(b).clamp(0.0, 1.0),
651            alpha: oklab.alpha,
652        }
653    }
654}
655
656impl From<OklchColor> for RgbaColor<f32> {
657    fn from(oklch: OklchColor) -> Self {
658        let oklab = OklabColor::from(oklch);
659        RgbaColor::from(oklab)
660    }
661}
662
663impl From<RgbaColor<f32>> for OklchColor {
664    fn from(col: RgbaColor<f32>) -> Self {
665        let oklab = OklabColor::from(col);
666        OklchColor::from(oklab)
667    }
668}
669
670impl From<OklchColor> for Color {
671    fn from(value: OklchColor) -> Self {
672        RgbaColor::from(value).into()
673    }
674}
675
676impl From<Color> for OklchColor {
677    fn from(value: Color) -> Self {
678        value.to_oklch()
679    }
680}
681
682#[test]
683fn test_rgb_to_hsv() {
684    // White
685    assert_eq!(
686        HsvaColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 }),
687        HsvaColor { hue: 0., saturation: 0., value: 1., alpha: 0.5 }
688    );
689    assert_eq!(
690        RgbaColor::<f32>::from(HsvaColor { hue: 0., saturation: 0., value: 1., alpha: 0.3 }),
691        RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.3 }
692    );
693
694    // #8a0c77ff ensure the hue ends up positive
695    assert_eq!(
696        HsvaColor::from(Color::from_argb_u8(0xff, 0x8a, 0xc, 0x77,).to_argb_f32()),
697        HsvaColor { hue: 309.0476, saturation: 0.9130435, value: 0.5411765, alpha: 1.0 }
698    );
699
700    let received = RgbaColor::<f32>::from(HsvaColor {
701        hue: 309.0476,
702        saturation: 0.9130435,
703        value: 0.5411765,
704        alpha: 1.0,
705    });
706    let expected = Color::from_argb_u8(0xff, 0x8a, 0xc, 0x77).to_argb_f32();
707
708    assert!(
709        (received.alpha - expected.alpha).abs() < 0.00001
710            && (received.red - expected.red).abs() < 0.00001
711            && (received.green - expected.green).abs() < 0.00001
712            && (received.blue - expected.blue).abs() < 0.00001
713    );
714
715    // Bright greenish, verified via colorizer.org
716    assert_eq!(
717        HsvaColor::from(RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }),
718        HsvaColor { hue: 120., saturation: 1., value: 0.9, alpha: 1.0 }
719    );
720    assert_eq!(
721        RgbaColor::<f32>::from(HsvaColor { hue: 120., saturation: 1., value: 0.9, alpha: 1.0 }),
722        RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }
723    );
724
725    // Hue should wrap around 360deg i.e. 480 == 120 && -240 == 240
726    assert_eq!(
727        RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 },
728        RgbaColor::<f32>::from(HsvaColor { hue: 480., saturation: 1., value: 0.9, alpha: 1.0 }),
729    );
730    assert_eq!(
731        RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 },
732        RgbaColor::<f32>::from(HsvaColor { hue: -240., saturation: 1., value: 0.9, alpha: 1.0 }),
733    );
734}
735
736#[test]
737fn test_brighter_darker() {
738    let blue = Color::from_rgb_u8(0, 0, 128);
739    assert_eq!(blue.brighter(0.5), Color::from_rgb_f32(0.0, 0.0, 0.75294125));
740    assert_eq!(blue.darker(0.5), Color::from_rgb_f32(0.0, 0.0, 0.33464053));
741}
742
743#[test]
744fn test_transparent_transition() {
745    let color = Color::from_argb_f32(0.0, 0.0, 0.0, 0.0);
746    let interpolated = color.interpolate(&Color::from_rgb_f32(0.8, 0.8, 0.8), 0.25);
747    assert_eq!(interpolated, Color::from_argb_f32(0.25, 0.8, 0.8, 0.8));
748    let interpolated = color.interpolate(&Color::from_rgb_f32(0.8, 0.8, 0.8), 0.5);
749    assert_eq!(interpolated, Color::from_argb_f32(0.5, 0.8, 0.8, 0.8));
750    let interpolated = color.interpolate(&Color::from_rgb_f32(0.8, 0.8, 0.8), 0.75);
751    assert_eq!(interpolated, Color::from_argb_f32(0.75, 0.8, 0.8, 0.8));
752}
753
754#[test]
755fn test_oklch_roundtrip() {
756    // Test that Oklch roundtrips correctly through RGB
757    // Use colors with low chroma that are definitely within sRGB gamut
758    let test_colors = [
759        OklchColor { lightness: 0.5, chroma: 0.08, hue: 30.0, alpha: 1.0 },
760        OklchColor { lightness: 0.6, chroma: 0.1, hue: 120.0, alpha: 0.8 },
761        OklchColor { lightness: 0.4, chroma: 0.08, hue: 240.0, alpha: 1.0 },
762        OklchColor { lightness: 0.8, chroma: 0.05, hue: 0.0, alpha: 1.0 },
763        // Grayscale (chroma = 0)
764        OklchColor { lightness: 0.5, chroma: 0.0, hue: 0.0, alpha: 1.0 },
765    ];
766
767    for oklch in test_colors {
768        let rgba = RgbaColor::<f32>::from(oklch);
769        let roundtrip = OklchColor::from(rgba);
770        // Allow some tolerance due to floating point operations and gamut mapping
771        assert!(
772            (oklch.lightness - roundtrip.lightness).abs() < 0.01,
773            "Lightness mismatch: {:?} vs {:?}",
774            oklch,
775            roundtrip
776        );
777        // Skip chroma/hue comparison for grayscale since hue is undefined
778        if oklch.chroma > 0.001 {
779            assert!(
780                (oklch.chroma - roundtrip.chroma).abs() < 0.02,
781                "Chroma mismatch: {:?} vs {:?}",
782                oklch,
783                roundtrip
784            );
785            // Hue can wrap around, so we need to handle that
786            let hue_diff = (oklch.hue - roundtrip.hue).abs();
787            let hue_diff = hue_diff.min(360.0 - hue_diff);
788            assert!(hue_diff < 2.0, "Hue mismatch: {:?} vs {:?}", oklch, roundtrip);
789        }
790    }
791}
792
793#[test]
794fn test_oklch_known_values() {
795    // Test conversion of a known Oklch value to RGB
796    // These values are approximate and verified against online converters
797    let red_oklch = OklchColor { lightness: 0.63, chroma: 0.26, hue: 29.0, alpha: 1.0 };
798    let red_rgba = RgbaColor::<f32>::from(red_oklch);
799    // Red should have high red component and low green/blue
800    assert!(red_rgba.red > 0.8, "Red component should be high: {}", red_rgba.red);
801    assert!(red_rgba.green < 0.3, "Green component should be low: {}", red_rgba.green);
802    assert!(red_rgba.blue < 0.3, "Blue component should be low: {}", red_rgba.blue);
803}
804
805#[test]
806fn test_rgb_to_oklch() {
807    // White: should have high lightness, zero chroma
808    let white = OklchColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 });
809    assert!((white.lightness - 1.0).abs() < 0.01, "White lightness should be ~1.0");
810    assert!(white.chroma < 0.001, "White chroma should be ~0");
811    assert!((white.alpha - 0.5).abs() < 0.001, "Alpha should be preserved");
812
813    // Black: should have zero lightness, zero chroma
814    let black = OklchColor::from(RgbaColor::<f32> { red: 0., green: 0., blue: 0., alpha: 1.0 });
815    assert!(black.lightness < 0.01, "Black lightness should be ~0");
816    assert!(black.chroma < 0.001, "Black chroma should be ~0");
817
818    // Pure red: should have hue around 29 degrees (red in Oklch)
819    let red = OklchColor::from(RgbaColor::<f32> { red: 1., green: 0., blue: 0., alpha: 1.0 });
820    assert!(red.lightness > 0.5 && red.lightness < 0.7, "Red lightness should be ~0.63");
821    assert!(red.chroma > 0.2, "Red should have significant chroma");
822    assert!(red.hue > 20.0 && red.hue < 40.0, "Red hue should be around 29 degrees");
823
824    // Pure blue: should have hue around 264 degrees
825    let blue = OklchColor::from(RgbaColor::<f32> { red: 0., green: 0., blue: 1., alpha: 1.0 });
826    assert!(blue.lightness > 0.4 && blue.lightness < 0.5, "Blue lightness should be ~0.45");
827    assert!(blue.chroma > 0.2, "Blue should have significant chroma");
828    assert!(blue.hue > 250.0 && blue.hue < 280.0, "Blue hue should be around 264 degrees");
829}
830
831#[cfg(feature = "ffi")]
832pub(crate) mod ffi {
833    #![allow(unsafe_code)]
834    use super::*;
835
836    #[unsafe(no_mangle)]
837    pub unsafe extern "C" fn slint_color_brighter(col: &Color, factor: f32, out: *mut Color) {
838        unsafe { core::ptr::write(out, col.brighter(factor)) }
839    }
840
841    #[unsafe(no_mangle)]
842    pub unsafe extern "C" fn slint_color_darker(col: &Color, factor: f32, out: *mut Color) {
843        unsafe { core::ptr::write(out, col.darker(factor)) }
844    }
845
846    #[unsafe(no_mangle)]
847    pub unsafe extern "C" fn slint_color_transparentize(col: &Color, factor: f32, out: *mut Color) {
848        unsafe { core::ptr::write(out, col.transparentize(factor)) }
849    }
850
851    #[unsafe(no_mangle)]
852    pub unsafe extern "C" fn slint_color_mix(
853        col1: &Color,
854        col2: &Color,
855        factor: f32,
856        out: *mut Color,
857    ) {
858        unsafe { core::ptr::write(out, col1.mix(col2, factor)) }
859    }
860
861    #[unsafe(no_mangle)]
862    pub unsafe extern "C" fn slint_color_with_alpha(col: &Color, alpha: f32, out: *mut Color) {
863        unsafe { core::ptr::write(out, col.with_alpha(alpha)) }
864    }
865
866    #[unsafe(no_mangle)]
867    pub extern "C" fn slint_color_to_hsva(
868        col: &Color,
869        h: &mut f32,
870        s: &mut f32,
871        v: &mut f32,
872        a: &mut f32,
873    ) {
874        let hsv = col.to_hsva();
875        *h = hsv.hue;
876        *s = hsv.saturation;
877        *v = hsv.value;
878        *a = hsv.alpha;
879    }
880
881    #[unsafe(no_mangle)]
882    pub extern "C" fn slint_color_from_hsva(h: f32, s: f32, v: f32, a: f32) -> Color {
883        Color::from_hsva(h, s, v, a)
884    }
885
886    #[unsafe(no_mangle)]
887    pub extern "C" fn slint_color_from_oklch(l: f32, c: f32, h: f32, a: f32) -> Color {
888        Color::from_oklch(l, c, h, a)
889    }
890
891    #[unsafe(no_mangle)]
892    pub extern "C" fn slint_color_to_oklch(
893        col: &Color,
894        l: &mut f32,
895        c: &mut f32,
896        h: &mut f32,
897        a: &mut f32,
898    ) {
899        let oklch = col.to_oklch();
900        *l = oklch.lightness;
901        *c = oklch.chroma;
902        *h = oklch.hue;
903        *a = oklch.alpha;
904    }
905}