i_slint_core/graphics/
brush.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 brush related types for the run-time library.
6*/
7
8use super::Color;
9use crate::properties::InterpolatedPropertyValue;
10use crate::SharedVector;
11use euclid::default::{Point2D, Size2D};
12
13#[cfg(not(feature = "std"))]
14use num_traits::float::Float;
15
16/// A brush is a data structure that is used to describe how
17/// a shape, such as a rectangle, path or even text, shall be filled.
18/// A brush can also be applied to the outline of a shape, that means
19/// the fill of the outline itself.
20#[derive(Clone, PartialEq, Debug, derive_more::From)]
21#[repr(C)]
22#[non_exhaustive]
23pub enum Brush {
24    /// The color variant of brush is a plain color that is to be used for the fill.
25    SolidColor(Color),
26    /// The linear gradient variant of a brush describes the gradient stops for a fill
27    /// where all color stops are along a line that's rotated by the specified angle.
28    LinearGradient(LinearGradientBrush),
29    /// The radial gradient variant of a brush describes a circle variant centered
30    /// in the middle
31    RadialGradient(RadialGradientBrush),
32    /// The conical gradient variant of a brush describes a gradient that rotates around
33    /// a center point, like the hands of a clock
34    ConicGradient(ConicGradientBrush),
35}
36
37/// Construct a brush with transparent color
38impl Default for Brush {
39    fn default() -> Self {
40        Self::SolidColor(Color::default())
41    }
42}
43
44impl Brush {
45    /// If the brush is SolidColor, the contained color is returned.
46    /// If the brush is a LinearGradient, the color of the first stop is returned.
47    pub fn color(&self) -> Color {
48        match self {
49            Brush::SolidColor(col) => *col,
50            Brush::LinearGradient(gradient) => {
51                gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
52            }
53            Brush::RadialGradient(gradient) => {
54                gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
55            }
56            Brush::ConicGradient(gradient) => {
57                gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
58            }
59        }
60    }
61
62    /// Returns true if this brush contains a fully transparent color (alpha value is zero)
63    ///
64    /// ```
65    /// # use i_slint_core::graphics::*;
66    /// assert!(Brush::default().is_transparent());
67    /// assert!(Brush::SolidColor(Color::from_argb_u8(0, 255, 128, 140)).is_transparent());
68    /// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 128, 140, 210)).is_transparent());
69    /// ```
70    pub fn is_transparent(&self) -> bool {
71        match self {
72            Brush::SolidColor(c) => c.alpha() == 0,
73            Brush::LinearGradient(_) => false,
74            Brush::RadialGradient(_) => false,
75            Brush::ConicGradient(_) => false,
76        }
77    }
78
79    /// Returns true if this brush is fully opaque
80    ///
81    /// ```
82    /// # use i_slint_core::graphics::*;
83    /// assert!(!Brush::default().is_opaque());
84    /// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 255, 128, 140)).is_opaque());
85    /// assert!(Brush::SolidColor(Color::from_rgb_u8(128, 140, 210)).is_opaque());
86    /// ```
87    pub fn is_opaque(&self) -> bool {
88        match self {
89            Brush::SolidColor(c) => c.alpha() == 255,
90            Brush::LinearGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
91            Brush::RadialGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
92            Brush::ConicGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
93        }
94    }
95
96    /// Returns a new version of this brush that has the brightness increased
97    /// by the specified factor. This is done by calling [`Color::brighter`] on
98    /// all the colors of this brush.
99    #[must_use]
100    pub fn brighter(&self, factor: f32) -> Self {
101        match self {
102            Brush::SolidColor(c) => Brush::SolidColor(c.brighter(factor)),
103            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
104                g.angle(),
105                g.stops().map(|s| GradientStop {
106                    color: s.color.brighter(factor),
107                    position: s.position,
108                }),
109            )),
110            Brush::RadialGradient(g) => {
111                Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
112                    GradientStop { color: s.color.brighter(factor), position: s.position }
113                })))
114            }
115            Brush::ConicGradient(g) => {
116                Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
117                    color: s.color.brighter(factor),
118                    position: s.position,
119                })))
120            }
121        }
122    }
123
124    /// Returns a new version of this brush that has the brightness decreased
125    /// by the specified factor. This is done by calling [`Color::darker`] on
126    /// all the color of this brush.
127    #[must_use]
128    pub fn darker(&self, factor: f32) -> Self {
129        match self {
130            Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
131            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
132                g.angle(),
133                g.stops()
134                    .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
135            )),
136            Brush::RadialGradient(g) => Brush::RadialGradient(RadialGradientBrush::new_circle(
137                g.stops()
138                    .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
139            )),
140            Brush::ConicGradient(g) => Brush::ConicGradient(ConicGradientBrush::new(
141                g.stops()
142                    .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
143            )),
144        }
145    }
146
147    /// Returns a new version of this brush with the opacity decreased by `factor`.
148    ///
149    /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
150    ///
151    /// See also [`Color::transparentize`]
152    #[must_use]
153    pub fn transparentize(&self, amount: f32) -> Self {
154        match self {
155            Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
156            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
157                g.angle(),
158                g.stops().map(|s| GradientStop {
159                    color: s.color.transparentize(amount),
160                    position: s.position,
161                }),
162            )),
163            Brush::RadialGradient(g) => {
164                Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
165                    GradientStop { color: s.color.transparentize(amount), position: s.position }
166                })))
167            }
168            Brush::ConicGradient(g) => {
169                Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
170                    color: s.color.transparentize(amount),
171                    position: s.position,
172                })))
173            }
174        }
175    }
176
177    /// Returns a new version of this brush with the related color's opacities
178    /// set to `alpha`.
179    #[must_use]
180    pub fn with_alpha(&self, alpha: f32) -> Self {
181        match self {
182            Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
183            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
184                g.angle(),
185                g.stops().map(|s| GradientStop {
186                    color: s.color.with_alpha(alpha),
187                    position: s.position,
188                }),
189            )),
190            Brush::RadialGradient(g) => {
191                Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
192                    GradientStop { color: s.color.with_alpha(alpha), position: s.position }
193                })))
194            }
195            Brush::ConicGradient(g) => {
196                Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
197                    color: s.color.with_alpha(alpha),
198                    position: s.position,
199                })))
200            }
201        }
202    }
203}
204
205/// The LinearGradientBrush describes a way of filling a shape with different colors, which
206/// are interpolated between different stops. The colors are aligned with a line that's rotated
207/// by the LinearGradient's angle.
208#[derive(Clone, PartialEq, Debug)]
209#[repr(transparent)]
210pub struct LinearGradientBrush(SharedVector<GradientStop>);
211
212impl LinearGradientBrush {
213    /// Creates a new linear gradient, described by the specified angle and the provided color stops.
214    ///
215    /// The angle need to be specified in degrees.
216    /// The stops don't need to be sorted as this function will sort them.
217    pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
218        let stop_iter = stops.into_iter();
219        let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
220        // The gradient's first stop is a fake stop to store the angle
221        encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
222        encoded_angle_and_stops.extend(stop_iter);
223        Self(encoded_angle_and_stops)
224    }
225    /// Returns the angle of the linear gradient in degrees.
226    pub fn angle(&self) -> f32 {
227        self.0[0].position
228    }
229    /// Returns the color stops of the linear gradient.
230    /// The stops are sorted by positions.
231    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
232        // skip the first fake stop that just contains the angle
233        self.0.iter().skip(1)
234    }
235}
236
237/// The RadialGradientBrush describes a way of filling a shape with a circular gradient
238#[derive(Clone, PartialEq, Debug)]
239#[repr(transparent)]
240pub struct RadialGradientBrush(SharedVector<GradientStop>);
241
242impl RadialGradientBrush {
243    /// Creates a new circle radial gradient, centered in the middle and described
244    /// by the provided color stops.
245    pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
246        Self(stops.into_iter().collect())
247    }
248    /// Returns the color stops of the linear gradient.
249    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
250        self.0.iter()
251    }
252}
253
254/// The ConicGradientBrush describes a way of filling a shape with a gradient
255/// that rotates around a center point
256#[derive(Clone, PartialEq, Debug)]
257#[repr(transparent)]
258pub struct ConicGradientBrush(SharedVector<GradientStop>);
259
260impl ConicGradientBrush {
261    /// Creates a new conic gradient with the provided color stops.
262    /// The stops should have angle positions in the range 0.0 to 1.0,
263    /// where 0.0 is 0 degrees (north) and 1.0 is 360 degrees.
264    pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
265        Self(stops.into_iter().collect())
266    }
267
268    /// Returns the color stops of the conic gradient.
269    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
270        self.0.iter()
271    }
272}
273
274/// GradientStop describes a single color stop in a gradient. The colors between multiple
275/// stops are interpolated.
276#[repr(C)]
277#[derive(Copy, Clone, Debug, PartialEq)]
278pub struct GradientStop {
279    /// The color to draw at this stop.
280    pub color: Color,
281    /// The position of this stop on the entire shape, as a normalized value between 0 and 1.
282    pub position: f32,
283}
284
285/// Returns the start / end points of a gradient within a rectangle of the given size, based on the angle (in degree).
286pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
287    let angle = (angle + 90.).to_radians();
288    let (s, c) = angle.sin_cos();
289
290    let (a, b) = if s.abs() < f32::EPSILON {
291        let y = size.height / 2.;
292        return if c < 0. {
293            (Point2D::new(0., y), Point2D::new(size.width, y))
294        } else {
295            (Point2D::new(size.width, y), Point2D::new(0., y))
296        };
297    } else if c * s < 0. {
298        // Intersection between the gradient line, and an orthogonal line that goes through (height, 0)
299        let x = (s * size.width + c * size.height) * s / 2.;
300        let y = -c * x / s + size.height;
301        (Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
302    } else {
303        // Intersection between the gradient line, and an orthogonal line that goes through (0, 0)
304        let x = (s * size.width - c * size.height) * s / 2.;
305        let y = -c * x / s;
306        (Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
307    };
308
309    if s > 0. {
310        (a, b)
311    } else {
312        (b, a)
313    }
314}
315
316impl InterpolatedPropertyValue for Brush {
317    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
318        match (self, target_value) {
319            (Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
320                Brush::SolidColor(source_col.interpolate(target_col, t))
321            }
322            (Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
323                let mut new_grad = grad.clone();
324                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
325                    x.color = col.interpolate(&x.color, t);
326                }
327                Brush::LinearGradient(new_grad)
328            }
329            (a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
330                Self::interpolate(b, a, 1. - t)
331            }
332            (Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
333                if lhs.0.len() < rhs.0.len() {
334                    Self::interpolate(target_value, self, 1. - t)
335                } else {
336                    let mut new_grad = lhs.clone();
337                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
338                    {
339                        let angle = &mut iter.next().unwrap().position;
340                        *angle = angle.interpolate(&rhs.angle(), t);
341                    }
342                    for s2 in rhs.stops() {
343                        let s1 = iter.next().unwrap();
344                        s1.color = s1.color.interpolate(&s2.color, t);
345                        s1.position = s1.position.interpolate(&s2.position, t);
346                    }
347                    for x in iter {
348                        x.position = x.position.interpolate(&1.0, t);
349                    }
350                    Brush::LinearGradient(new_grad)
351                }
352            }
353            (Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
354                let mut new_grad = grad.clone();
355                for x in new_grad.0.make_mut_slice().iter_mut() {
356                    x.color = col.interpolate(&x.color, t);
357                }
358                Brush::RadialGradient(new_grad)
359            }
360            (a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
361                Self::interpolate(b, a, 1. - t)
362            }
363            (Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
364                if lhs.0.len() < rhs.0.len() {
365                    Self::interpolate(target_value, self, 1. - t)
366                } else {
367                    let mut new_grad = lhs.clone();
368                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
369                    let mut last_color = Color::default();
370                    for s2 in rhs.stops() {
371                        let s1 = iter.next().unwrap();
372                        last_color = s2.color;
373                        s1.color = s1.color.interpolate(&s2.color, t);
374                        s1.position = s1.position.interpolate(&s2.position, t);
375                    }
376                    for x in iter {
377                        x.position = x.position.interpolate(&1.0, t);
378                        x.color = x.color.interpolate(&last_color, t);
379                    }
380                    Brush::RadialGradient(new_grad)
381                }
382            }
383            (Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
384                let mut new_grad = grad.clone();
385                for x in new_grad.0.make_mut_slice().iter_mut() {
386                    x.color = col.interpolate(&x.color, t);
387                }
388                Brush::ConicGradient(new_grad)
389            }
390            (a @ Brush::ConicGradient(_), b @ Brush::SolidColor(_)) => {
391                Self::interpolate(b, a, 1. - t)
392            }
393            (Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
394                if lhs.0.len() < rhs.0.len() {
395                    Self::interpolate(target_value, self, 1. - t)
396                } else {
397                    let mut new_grad = lhs.clone();
398                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
399                    for s2 in rhs.stops() {
400                        let s1 = iter.next().unwrap();
401                        s1.color = s1.color.interpolate(&s2.color, t);
402                        s1.position = s1.position.interpolate(&s2.position, t);
403                    }
404                    for x in iter {
405                        x.position = x.position.interpolate(&1.0, t);
406                    }
407                    Brush::ConicGradient(new_grad)
408                }
409            }
410            (a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
411            | (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_))
412            | (a @ Brush::LinearGradient(_), b @ Brush::ConicGradient(_))
413            | (a @ Brush::ConicGradient(_), b @ Brush::LinearGradient(_))
414            | (a @ Brush::RadialGradient(_), b @ Brush::ConicGradient(_))
415            | (a @ Brush::ConicGradient(_), b @ Brush::RadialGradient(_)) => {
416                // Just go to an intermediate color.
417                let color = Color::interpolate(&b.color(), &a.color(), t);
418                if t < 0.5 {
419                    Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
420                } else {
421                    Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
422                }
423            }
424        }
425    }
426}
427
428#[test]
429#[allow(clippy::float_cmp)] // We want bit-wise equality here
430fn test_linear_gradient_encoding() {
431    let stops: SharedVector<GradientStop> = [
432        GradientStop { position: 0.0, color: Color::from_argb_u8(255, 255, 0, 0) },
433        GradientStop { position: 0.5, color: Color::from_argb_u8(255, 0, 255, 0) },
434        GradientStop { position: 1.0, color: Color::from_argb_u8(255, 0, 0, 255) },
435    ]
436    .into();
437    let grad = LinearGradientBrush::new(256., stops.clone());
438    assert_eq!(grad.angle(), 256.);
439    assert!(grad.stops().eq(stops.iter()));
440}