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 | /*! |
5 | This module contains color related types for the run-time library. |
6 | */ |
7 | |
8 | use crate::properties::InterpolatedPropertyValue; |
9 | |
10 | #[cfg (not(feature = "std" ))] |
11 | use 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)] |
19 | pub 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)] |
49 | pub struct Color { |
50 | red: u8, |
51 | green: u8, |
52 | blue: u8, |
53 | alpha: u8, |
54 | } |
55 | |
56 | impl 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 | |
62 | impl 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 | |
68 | impl 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 | |
79 | impl From<Color> for RgbaColor<f32> { |
80 | fn from(col: Color) -> Self { |
81 | let u8col: RgbaColor<u8> = col.into(); |
82 | u8col.into() |
83 | } |
84 | } |
85 | |
86 | impl 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 | |
97 | impl 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 | |
305 | impl 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 | |
316 | impl 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)] |
323 | struct HsvaColor { |
324 | h: f32, |
325 | s: f32, |
326 | v: f32, |
327 | alpha: f32, |
328 | } |
329 | |
330 | impl 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 | |
360 | impl 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 ] |
385 | fn 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 ] |
408 | fn 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" )] |
415 | pub(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 | |