1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
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 /// Returns the red channel of the color as u8 in the range 0..255.
149 #[inline(always)]
150 pub fn red(self) -> u8 {
151 self.red
152 }
153
154 /// Returns the green channel of the color as u8 in the range 0..255.
155 #[inline(always)]
156 pub fn green(self) -> u8 {
157 self.green
158 }
159
160 /// Returns the blue channel of the color as u8 in the range 0..255.
161 #[inline(always)]
162 pub fn blue(self) -> u8 {
163 self.blue
164 }
165
166 /// Returns the alpha channel of the color as u8 in the range 0..255.
167 #[inline(always)]
168 pub fn alpha(self) -> u8 {
169 self.alpha
170 }
171
172 /// Returns a new version of this color that has the brightness increased
173 /// by the specified factor. This is done by converting the color to the HSV
174 /// color space and multiplying the brightness (value) with (1 + factor).
175 /// The result is converted back to RGB and the alpha channel is unchanged.
176 /// So for example `brighter(0.2)` will increase the brightness by 20%, and
177 /// calling `brighter(-0.5)` will return a color that's 50% darker.
178 #[must_use]
179 pub fn brighter(&self, factor: f32) -> Self {
180 let rgba: RgbaColor<f32> = (*self).into();
181 let mut hsva: HsvaColor = rgba.into();
182 hsva.v *= 1. + factor;
183 let rgba: RgbaColor<f32> = hsva.into();
184 rgba.into()
185 }
186
187 /// Returns a new version of this color that has the brightness decreased
188 /// by the specified factor. This is done by converting the color to the HSV
189 /// color space and dividing the brightness (value) by (1 + factor). The
190 /// result is converted back to RGB and the alpha channel is unchanged.
191 /// So for example `darker(0.3)` will decrease the brightness by 30%.
192 #[must_use]
193 pub fn darker(&self, factor: f32) -> Self {
194 let rgba: RgbaColor<f32> = (*self).into();
195 let mut hsva: HsvaColor = rgba.into();
196 hsva.v /= 1. + factor;
197 let rgba: RgbaColor<f32> = hsva.into();
198 rgba.into()
199 }
200
201 /// Returns a new version of this color with the opacity decreased by `factor`.
202 ///
203 /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
204 ///
205 /// # Examples
206 /// Decreasing the opacity of a red color by half:
207 /// ```
208 /// # use i_slint_core::graphics::Color;
209 /// let red = Color::from_argb_u8(255, 255, 0, 0);
210 /// assert_eq!(red.transparentize(0.5), Color::from_argb_u8(128, 255, 0, 0));
211 /// ```
212 ///
213 /// Decreasing the opacity of a blue color by 20%:
214 /// ```
215 /// # use i_slint_core::graphics::Color;
216 /// let blue = Color::from_argb_u8(200, 0, 0, 255);
217 /// assert_eq!(blue.transparentize(0.2), Color::from_argb_u8(160, 0, 0, 255));
218 /// ```
219 ///
220 /// Negative values increase the opacity
221 ///
222 /// ```
223 /// # use i_slint_core::graphics::Color;
224 /// let blue = Color::from_argb_u8(200, 0, 0, 255);
225 /// assert_eq!(blue.transparentize(-0.1), Color::from_argb_u8(220, 0, 0, 255));
226 /// ```
227
228 #[must_use]
229 pub fn transparentize(&self, factor: f32) -> Self {
230 let mut color = *self;
231 color.alpha = ((self.alpha as f32) * (1.0 - factor))
232 .round()
233 .clamp(u8::MIN as f32, u8::MAX as f32) as u8;
234 color
235 }
236
237 /// Returns a new color that is a mix of `self` and `other`, with a proportion
238 /// factor given by `factor` (which will be clamped to be between `0.0` and `1.0`).
239 ///
240 /// # Examples
241 /// Mix red with black half-and-half:
242 /// ```
243 /// # use i_slint_core::graphics::Color;
244 /// let red = Color::from_rgb_u8(255, 0, 0);
245 /// let black = Color::from_rgb_u8(0, 0, 0);
246 /// assert_eq!(red.mix(&black, 0.5), Color::from_rgb_u8(128, 0, 0));
247 /// ```
248 ///
249 /// Mix Purple with OrangeRed with `75%`:`25%` ratio:
250 /// ```
251 /// # use i_slint_core::graphics::Color;
252 /// let purple = Color::from_rgb_u8(128, 0, 128);
253 /// let orange_red = Color::from_rgb_u8(255, 69, 0);
254 /// assert_eq!(purple.mix(&orange_red, 0.75), Color::from_rgb_u8(160, 17, 96));
255 /// ```
256 #[must_use]
257 pub fn mix(&self, other: &Self, factor: f32) -> Self {
258 // * NOTE: The opacity (`alpha` as a "percentage") of each color involved
259 // * must be taken into account when mixing them. Because of this,
260 // * we cannot just interpolate between them.
261 // * NOTE: Considering the spec (textual):
262 // * <https://github.com/sass/sass/blob/47d30713765b975c86fa32ec359ed16e83ad1ecc/spec/built-in-modules/color.md#mix>
263
264 fn lerp(v1: u8, v2: u8, f: f32) -> u8 {
265 (v1 as f32 * f + v2 as f32 * (1.0 - f)).clamp(u8::MIN as f32, u8::MAX as f32).round()
266 as u8
267 }
268
269 let original_factor = factor.clamp(0.0, 1.0);
270
271 let self_opacity = RgbaColor::<f32>::from(*self).alpha;
272 let other_opacity = RgbaColor::<f32>::from(*other).alpha;
273
274 let normal_weight = 2.0 * original_factor - 1.0;
275 let alpha_distance = self_opacity - other_opacity;
276 let weight_by_distance = normal_weight * alpha_distance;
277
278 // As to not divide by 0.0
279 let combined_weight = if weight_by_distance == -1.0 {
280 normal_weight
281 } else {
282 (normal_weight + alpha_distance) / (1.0 + weight_by_distance)
283 };
284
285 let channels_factor = (combined_weight + 1.0) / 2.0;
286
287 let red = lerp(self.red, other.red, channels_factor);
288 let green = lerp(self.green, other.green, channels_factor);
289 let blue = lerp(self.blue, other.blue, channels_factor);
290
291 let alpha = lerp(self.alpha, other.alpha, original_factor);
292
293 Self { red, green, blue, alpha }
294 }
295
296 /// Returns a new version of this color with the opacity set to `alpha`.
297 #[must_use]
298 pub fn with_alpha(&self, alpha: f32) -> Self {
299 let mut rgba: RgbaColor<f32> = (*self).into();
300 rgba.alpha = alpha.clamp(0.0, 1.0);
301 rgba.into()
302 }
303}
304
305impl InterpolatedPropertyValue for Color {
306 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
307 Self {
308 red: self.red.interpolate(&target_value.red, t),
309 green: self.green.interpolate(&target_value.green, t),
310 blue: self.blue.interpolate(&target_value.blue, t),
311 alpha: self.alpha.interpolate(&target_value.alpha, t),
312 }
313 }
314}
315
316impl core::fmt::Display for Color {
317 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
318 write!(f, "argb({}, {}, {}, {})", self.alpha, self.red, self.green, self.blue)
319 }
320}
321
322#[derive(Debug, Clone, Copy, PartialEq)]
323struct HsvaColor {
324 h: f32,
325 s: f32,
326 v: f32,
327 alpha: f32,
328}
329
330impl From<RgbaColor<f32>> for HsvaColor {
331 fn from(col: RgbaColor<f32>) -> Self {
332 // RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
333
334 let red = col.red;
335 let green = col.green;
336 let blue = col.blue;
337
338 let min = red.min(green).min(blue);
339 let max = red.max(green).max(blue);
340 let chroma = max - min;
341
342 #[allow(clippy::float_cmp)] // `max` is either `red`, `green` or `blue`
343 let hue = 60.
344 * if chroma == 0. {
345 0.0
346 } else if max == red {
347 ((green - blue) / chroma) % 6.0
348 } else if max == green {
349 2. + (blue - red) / chroma
350 } else {
351 4. + (red - green) / chroma
352 };
353
354 let saturation = if max == 0. { 0. } else { chroma / max };
355
356 Self { h: hue, s: saturation, v: max, alpha: col.alpha }
357 }
358}
359
360impl From<HsvaColor> for RgbaColor<f32> {
361 fn from(col: HsvaColor) -> Self {
362 // RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
363
364 let chroma: f32 = col.s * col.v;
365
366 let x: f32 = chroma * (1. - ((col.h / 60.) % 2. - 1.).abs());
367
368 let (red: f32, green: f32, blue: f32) = match (col.h / 60.0) as usize {
369 0 => (chroma, x, 0.),
370 1 => (x, chroma, 0.),
371 2 => (0., chroma, x),
372 3 => (0., x, chroma),
373 4 => (x, 0., chroma),
374 5 => (chroma, 0., x),
375 _ => (0., 0., 0.),
376 };
377
378 let m: f32 = col.v - chroma;
379
380 Self { red: red + m, green: green + m, blue: blue + m, alpha: col.alpha }
381 }
382}
383
384#[test]
385fn test_rgb_to_hsv() {
386 // White
387 assert_eq!(
388 HsvaColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 }),
389 HsvaColor { h: 0., s: 0., v: 1., alpha: 0.5 }
390 );
391 assert_eq!(
392 RgbaColor::<f32>::from(HsvaColor { h: 0., s: 0., v: 1., alpha: 0.3 }),
393 RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.3 }
394 );
395
396 // Bright greenish, verified via colorizer.org
397 assert_eq!(
398 HsvaColor::from(RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }),
399 HsvaColor { h: 120., s: 1., v: 0.9, alpha: 1.0 }
400 );
401 assert_eq!(
402 RgbaColor::<f32>::from(HsvaColor { h: 120., s: 1., v: 0.9, alpha: 1.0 }),
403 RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }
404 );
405}
406
407#[test]
408fn test_brighter_darker() {
409 let blue: Color = Color::from_rgb_u8(red:0, green:0, blue:128);
410 assert_eq!(blue.brighter(0.5), Color::from_rgb_u8(0, 0, 192));
411 assert_eq!(blue.darker(0.5), Color::from_rgb_u8(0, 0, 85));
412}
413
414#[cfg(feature = "ffi")]
415pub(crate) mod ffi {
416 #![allow(unsafe_code)]
417 use super::*;
418
419 #[no_mangle]
420 pub unsafe extern "C" fn slint_color_brighter(col: &Color, factor: f32, out: *mut Color) {
421 core::ptr::write(out, col.brighter(factor))
422 }
423
424 #[no_mangle]
425 pub unsafe extern "C" fn slint_color_darker(col: &Color, factor: f32, out: *mut Color) {
426 core::ptr::write(out, col.darker(factor))
427 }
428
429 #[no_mangle]
430 pub unsafe extern "C" fn slint_color_transparentize(col: &Color, factor: f32, out: *mut Color) {
431 core::ptr::write(out, col.transparentize(factor))
432 }
433
434 #[no_mangle]
435 pub unsafe extern "C" fn slint_color_mix(
436 col1: &Color,
437 col2: &Color,
438 factor: f32,
439 out: *mut Color,
440 ) {
441 core::ptr::write(out, col1.mix(col2, factor))
442 }
443
444 #[no_mangle]
445 pub unsafe extern "C" fn slint_color_with_alpha(col: &Color, alpha: f32, out: *mut Color) {
446 core::ptr::write(out, col.with_alpha(alpha))
447 }
448}
449