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 brush related types for the run-time library. |
6 | */ |
7 | |
8 | use super::Color; |
9 | use crate::properties::InterpolatedPropertyValue; |
10 | use crate::SharedVector; |
11 | use euclid::default::{Point2D, Size2D}; |
12 | |
13 | #[cfg (not(feature = "std" ))] |
14 | use 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 ] |
23 | pub 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 |
35 | impl Default for Brush { |
36 | fn default() -> Self { |
37 | Self::SolidColor(Color::default()) |
38 | } |
39 | } |
40 | |
41 | impl 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)] |
180 | pub struct LinearGradientBrush(SharedVector<GradientStop>); |
181 | |
182 | impl 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)] |
210 | pub struct RadialGradientBrush(SharedVector<GradientStop>); |
211 | |
212 | impl 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)] |
228 | pub 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). |
236 | pub 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 | |
266 | impl 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 |
349 | fn 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 | |