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 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}
33
34/// Construct a brush with transparent color
35impl Default for Brush {
36 fn default() -> Self {
37 Self::SolidColor(Color::default())
38 }
39}
40
41impl Brush {
42 /// If the brush is SolidColor, the contained color is returned.
43 /// If the brush is a LinearGradient, the color of the first stop is returned.
44 pub fn color(&self) -> Color {
45 match self {
46 Brush::SolidColor(col) => *col,
47 Brush::LinearGradient(gradient) => {
48 gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
49 }
50 Brush::RadialGradient(gradient) => {
51 gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
52 }
53 }
54 }
55
56 /// Returns true if this brush contains a fully transparent color (alpha value is zero)
57 ///
58 /// ```
59 /// # use i_slint_core::graphics::*;
60 /// assert!(Brush::default().is_transparent());
61 /// assert!(Brush::SolidColor(Color::from_argb_u8(0, 255, 128, 140)).is_transparent());
62 /// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 128, 140, 210)).is_transparent());
63 /// ```
64 pub fn is_transparent(&self) -> bool {
65 match self {
66 Brush::SolidColor(c) => c.alpha() == 0,
67 Brush::LinearGradient(_) => false,
68 Brush::RadialGradient(_) => false,
69 }
70 }
71
72 /// Returns true if this brush is fully opaque
73 ///
74 /// ```
75 /// # use i_slint_core::graphics::*;
76 /// assert!(!Brush::default().is_opaque());
77 /// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 255, 128, 140)).is_opaque());
78 /// assert!(Brush::SolidColor(Color::from_rgb_u8(128, 140, 210)).is_opaque());
79 /// ```
80 pub fn is_opaque(&self) -> bool {
81 match self {
82 Brush::SolidColor(c) => c.alpha() == 255,
83 Brush::LinearGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
84 Brush::RadialGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
85 }
86 }
87
88 /// Returns a new version of this brush that has the brightness increased
89 /// by the specified factor. This is done by calling [`Color::brighter`] on
90 /// all the colors of this brush.
91 #[must_use]
92 pub fn brighter(&self, factor: f32) -> Self {
93 match self {
94 Brush::SolidColor(c) => Brush::SolidColor(c.brighter(factor)),
95 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
96 g.angle(),
97 g.stops().map(|s| GradientStop {
98 color: s.color.brighter(factor),
99 position: s.position,
100 }),
101 )),
102 Brush::RadialGradient(g) => {
103 Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
104 GradientStop { color: s.color.brighter(factor), position: s.position }
105 })))
106 }
107 }
108 }
109
110 /// Returns a new version of this brush that has the brightness decreased
111 /// by the specified factor. This is done by calling [`Color::darker`] on
112 /// all the color of this brush.
113 #[must_use]
114 pub fn darker(&self, factor: f32) -> Self {
115 match self {
116 Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
117 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
118 g.angle(),
119 g.stops()
120 .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
121 )),
122 Brush::RadialGradient(g) => Brush::RadialGradient(RadialGradientBrush::new_circle(
123 g.stops()
124 .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
125 )),
126 }
127 }
128
129 /// Returns a new version of this brush with the opacity decreased by `factor`.
130 ///
131 /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
132 ///
133 /// See also [`Color::transparentize`]
134 #[must_use]
135 pub fn transparentize(&self, amount: f32) -> Self {
136 match self {
137 Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
138 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
139 g.angle(),
140 g.stops().map(|s| GradientStop {
141 color: s.color.transparentize(amount),
142 position: s.position,
143 }),
144 )),
145 Brush::RadialGradient(g) => {
146 Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
147 GradientStop { color: s.color.transparentize(amount), position: s.position }
148 })))
149 }
150 }
151 }
152
153 /// Returns a new version of this brush with the related color's opacities
154 /// set to `alpha`.
155 #[must_use]
156 pub fn with_alpha(&self, alpha: f32) -> Self {
157 match self {
158 Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
159 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
160 g.angle(),
161 g.stops().map(|s| GradientStop {
162 color: s.color.with_alpha(alpha),
163 position: s.position,
164 }),
165 )),
166 Brush::RadialGradient(g) => {
167 Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
168 GradientStop { color: s.color.with_alpha(alpha), position: s.position }
169 })))
170 }
171 }
172 }
173}
174
175/// The LinearGradientBrush describes a way of filling a shape with different colors, which
176/// are interpolated between different stops. The colors are aligned with a line that's rotated
177/// by the LinearGradient's angle.
178#[derive(Clone, PartialEq, Debug)]
179#[repr(transparent)]
180pub struct LinearGradientBrush(SharedVector<GradientStop>);
181
182impl LinearGradientBrush {
183 /// Creates a new linear gradient, described by the specified angle and the provided color stops.
184 ///
185 /// The angle need to be specified in degrees.
186 /// The stops don't need to be sorted as this function will sort them.
187 pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
188 let stop_iter: as IntoIterator>::IntoIter = stops.into_iter();
189 let mut encoded_angle_and_stops: SharedVector = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
190 // The gradient's first stop is a fake stop to store the angle
191 encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
192 encoded_angle_and_stops.extend(stop_iter);
193 Self(encoded_angle_and_stops)
194 }
195 /// Returns the angle of the linear gradient in degrees.
196 pub fn angle(&self) -> f32 {
197 self.0[0].position
198 }
199 /// Returns the color stops of the linear gradient.
200 /// The stops are sorted by positions.
201 pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
202 // skip the first fake stop that just contains the angle
203 self.0.iter().skip(1)
204 }
205}
206
207/// The RadialGradientBrush describes a way of filling a shape with a circular gradient
208#[derive(Clone, PartialEq, Debug)]
209#[repr(transparent)]
210pub struct RadialGradientBrush(SharedVector<GradientStop>);
211
212impl RadialGradientBrush {
213 /// Creates a new circle radial gradient, centered in the middle and described
214 /// by the provided color stops.
215 pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
216 Self(stops.into_iter().collect())
217 }
218 /// Returns the color stops of the linear gradient.
219 pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
220 self.0.iter()
221 }
222}
223
224/// GradientStop describes a single color stop in a gradient. The colors between multiple
225/// stops are interpolated.
226#[repr(C)]
227#[derive(Copy, Clone, Debug, PartialEq)]
228pub struct GradientStop {
229 /// The color to draw at this stop.
230 pub color: Color,
231 /// The position of this stop on the entire shape, as a normalized value between 0 and 1.
232 pub position: f32,
233}
234
235/// Returns the start / end points of a gradient within a rectangle of the given size, based on the angle (in degree).
236pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
237 let angle = (angle + 90.).to_radians();
238 let (s, c) = angle.sin_cos();
239
240 let (a, b) = if s.abs() < f32::EPSILON {
241 let y = size.height / 2.;
242 return if c < 0. {
243 (Point2D::new(0., y), Point2D::new(size.width, y))
244 } else {
245 (Point2D::new(size.width, y), Point2D::new(0., y))
246 };
247 } else if c * s < 0. {
248 // Intersection between the gradient line, and an orthogonal line that goes through (height, 0)
249 let x = (s * size.width + c * size.height) * s / 2.;
250 let y = -c * x / s + size.height;
251 (Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
252 } else {
253 // Intersection between the gradient line, and an orthogonal line that goes through (0, 0)
254 let x = (s * size.width - c * size.height) * s / 2.;
255 let y = -c * x / s;
256 (Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
257 };
258
259 if s > 0. {
260 (a, b)
261 } else {
262 (b, a)
263 }
264}
265
266impl InterpolatedPropertyValue for Brush {
267 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
268 match (self, target_value) {
269 (Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
270 Brush::SolidColor(source_col.interpolate(target_col, t))
271 }
272 (Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
273 let mut new_grad = grad.clone();
274 for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
275 x.color = col.interpolate(&x.color, t);
276 }
277 Brush::LinearGradient(new_grad)
278 }
279 (a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
280 Self::interpolate(b, a, 1. - t)
281 }
282 (Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
283 if lhs.0.len() < rhs.0.len() {
284 Self::interpolate(target_value, self, 1. - t)
285 } else {
286 let mut new_grad = lhs.clone();
287 let mut iter = new_grad.0.make_mut_slice().iter_mut();
288 {
289 let angle = &mut iter.next().unwrap().position;
290 *angle = angle.interpolate(&rhs.angle(), t);
291 }
292 for s2 in rhs.stops() {
293 let s1 = iter.next().unwrap();
294 s1.color = s1.color.interpolate(&s2.color, t);
295 s1.position = s1.position.interpolate(&s2.position, t);
296 }
297 for x in iter {
298 x.position = x.position.interpolate(&1.0, t);
299 }
300 Brush::LinearGradient(new_grad)
301 }
302 }
303 (Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
304 let mut new_grad = grad.clone();
305 for x in new_grad.0.make_mut_slice().iter_mut() {
306 x.color = col.interpolate(&x.color, t);
307 }
308 Brush::RadialGradient(new_grad)
309 }
310 (a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
311 Self::interpolate(b, a, 1. - t)
312 }
313 (Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
314 if lhs.0.len() < rhs.0.len() {
315 Self::interpolate(target_value, self, 1. - t)
316 } else {
317 let mut new_grad = lhs.clone();
318 let mut iter = new_grad.0.make_mut_slice().iter_mut();
319 let mut last_color = Color::default();
320 for s2 in rhs.stops() {
321 let s1 = iter.next().unwrap();
322 last_color = s2.color;
323 s1.color = s1.color.interpolate(&s2.color, t);
324 s1.position = s1.position.interpolate(&s2.position, t);
325 }
326 for x in iter {
327 x.position = x.position.interpolate(&1.0, t);
328 x.color = x.color.interpolate(&last_color, t);
329 }
330 Brush::RadialGradient(new_grad)
331 }
332 }
333 (a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
334 | (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_)) => {
335 // Just go to an intermediate color.
336 let color = Color::interpolate(&b.color(), &a.color(), t);
337 if t < 0.5 {
338 Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
339 } else {
340 Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
341 }
342 }
343 }
344 }
345}
346
347#[test]
348#[allow(clippy::float_cmp)] // We want bit-wise equality here
349fn test_linear_gradient_encoding() {
350 let stops: SharedVector<GradientStop> = [
351 GradientStop { position: 0.0, color: Color::from_argb_u8(alpha:255, red:255, green:0, blue:0) },
352 GradientStop { position: 0.5, color: Color::from_argb_u8(alpha:255, red:0, green:255, blue:0) },
353 GradientStop { position: 1.0, color: Color::from_argb_u8(alpha:255, red:0, green:0, blue:255) },
354 ]
355 .into();
356 let grad: LinearGradientBrush = LinearGradientBrush::new(angle:256., stops.clone());
357 assert_eq!(grad.angle(), 256.);
358 assert!(grad.stops().eq(stops.iter()));
359}
360