Skip to main content

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::SharedVector;
10use crate::properties::InterpolatedPropertyValue;
11use euclid::default::{Point2D, Size2D};
12
13#[cfg(not(feature = "std"))]
14use num_traits::Euclid;
15#[cfg(not(feature = "std"))]
16use num_traits::float::Float;
17
18/// A brush is a data structure that is used to describe how
19/// a shape, such as a rectangle, path or even text, shall be filled.
20/// A brush can also be applied to the outline of a shape, that means
21/// the fill of the outline itself.
22#[derive(Clone, PartialEq, Debug, derive_more::From)]
23#[repr(C)]
24#[non_exhaustive]
25pub enum Brush {
26    /// The color variant of brush is a plain color that is to be used for the fill.
27    SolidColor(Color),
28    /// The linear gradient variant of a brush describes the gradient stops for a fill
29    /// where all color stops are along a line that's rotated by the specified angle.
30    LinearGradient(LinearGradientBrush),
31    /// The radial gradient variant of a brush describes a circle variant centered
32    /// in the middle
33    RadialGradient(RadialGradientBrush),
34    /// The conical gradient variant of a brush describes a gradient that rotates around
35    /// a center point, like the hands of a clock
36    ConicGradient(ConicGradientBrush),
37}
38
39/// Construct a brush with transparent color
40impl Default for Brush {
41    fn default() -> Self {
42        Self::SolidColor(Color::default())
43    }
44}
45
46impl Brush {
47    /// If the brush is SolidColor, the contained color is returned.
48    /// If the brush is a LinearGradient, the color of the first stop is returned.
49    pub fn color(&self) -> Color {
50        match self {
51            Brush::SolidColor(col) => *col,
52            Brush::LinearGradient(gradient) => {
53                gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
54            }
55            Brush::RadialGradient(gradient) => {
56                gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
57            }
58            Brush::ConicGradient(gradient) => {
59                gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
60            }
61        }
62    }
63
64    /// Returns true if this brush contains a fully transparent color (alpha value is zero)
65    ///
66    /// ```
67    /// # use i_slint_core::graphics::*;
68    /// assert!(Brush::default().is_transparent());
69    /// assert!(Brush::SolidColor(Color::from_argb_u8(0, 255, 128, 140)).is_transparent());
70    /// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 128, 140, 210)).is_transparent());
71    /// ```
72    pub fn is_transparent(&self) -> bool {
73        match self {
74            Brush::SolidColor(c) => c.alpha() == 0,
75            Brush::LinearGradient(_) => false,
76            Brush::RadialGradient(_) => false,
77            Brush::ConicGradient(_) => false,
78        }
79    }
80
81    /// Returns true if this brush is fully opaque
82    ///
83    /// ```
84    /// # use i_slint_core::graphics::*;
85    /// assert!(!Brush::default().is_opaque());
86    /// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 255, 128, 140)).is_opaque());
87    /// assert!(Brush::SolidColor(Color::from_rgb_u8(128, 140, 210)).is_opaque());
88    /// ```
89    pub fn is_opaque(&self) -> bool {
90        match self {
91            Brush::SolidColor(c) => c.alpha() == 255,
92            Brush::LinearGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
93            Brush::RadialGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
94            Brush::ConicGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
95        }
96    }
97
98    /// Returns a new version of this brush that has the brightness increased
99    /// by the specified factor. This is done by calling [`Color::brighter`] on
100    /// all the colors of this brush.
101    #[must_use]
102    pub fn brighter(&self, factor: f32) -> Self {
103        match self {
104            Brush::SolidColor(c) => Brush::SolidColor(c.brighter(factor)),
105            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
106                g.angle(),
107                g.stops().map(|s| GradientStop {
108                    color: s.color.brighter(factor),
109                    position: s.position,
110                }),
111            )),
112            Brush::RadialGradient(g) => {
113                Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
114                    GradientStop { color: s.color.brighter(factor), position: s.position }
115                })))
116            }
117            Brush::ConicGradient(g) => {
118                let mut new_grad = g.clone();
119                // Skip the first stop (which contains the angle), modify only color stops
120                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
121                    x.color = x.color.brighter(factor);
122                }
123                Brush::ConicGradient(new_grad)
124            }
125        }
126    }
127
128    /// Returns a new version of this brush that has the brightness decreased
129    /// by the specified factor. This is done by calling [`Color::darker`] on
130    /// all the color of this brush.
131    #[must_use]
132    pub fn darker(&self, factor: f32) -> Self {
133        match self {
134            Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
135            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
136                g.angle(),
137                g.stops()
138                    .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
139            )),
140            Brush::RadialGradient(g) => Brush::RadialGradient(RadialGradientBrush::new_circle(
141                g.stops()
142                    .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
143            )),
144            Brush::ConicGradient(g) => {
145                let mut new_grad = g.clone();
146                // Skip the first stop (which contains the angle), modify only color stops
147                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
148                    x.color = x.color.darker(factor);
149                }
150                Brush::ConicGradient(new_grad)
151            }
152        }
153    }
154
155    /// Returns a new version of this brush with the opacity decreased by `factor`.
156    ///
157    /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
158    ///
159    /// See also [`Color::transparentize`]
160    #[must_use]
161    pub fn transparentize(&self, amount: f32) -> Self {
162        match self {
163            Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
164            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
165                g.angle(),
166                g.stops().map(|s| GradientStop {
167                    color: s.color.transparentize(amount),
168                    position: s.position,
169                }),
170            )),
171            Brush::RadialGradient(g) => {
172                Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
173                    GradientStop { color: s.color.transparentize(amount), position: s.position }
174                })))
175            }
176            Brush::ConicGradient(g) => {
177                let mut new_grad = g.clone();
178                // Skip the first stop (which contains the angle), modify only color stops
179                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
180                    x.color = x.color.transparentize(amount);
181                }
182                Brush::ConicGradient(new_grad)
183            }
184        }
185    }
186
187    /// Returns a new version of this brush with the related color's opacities
188    /// set to `alpha`.
189    #[must_use]
190    pub fn with_alpha(&self, alpha: f32) -> Self {
191        match self {
192            Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
193            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
194                g.angle(),
195                g.stops().map(|s| GradientStop {
196                    color: s.color.with_alpha(alpha),
197                    position: s.position,
198                }),
199            )),
200            Brush::RadialGradient(g) => {
201                Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
202                    GradientStop { color: s.color.with_alpha(alpha), position: s.position }
203                })))
204            }
205            Brush::ConicGradient(g) => {
206                let mut new_grad = g.clone();
207                // Skip the first stop (which contains the angle), modify only color stops
208                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
209                    x.color = x.color.with_alpha(alpha);
210                }
211                Brush::ConicGradient(new_grad)
212            }
213        }
214    }
215}
216
217/// The LinearGradientBrush describes a way of filling a shape with different colors, which
218/// are interpolated between different stops. The colors are aligned with a line that's rotated
219/// by the LinearGradient's angle.
220#[derive(Clone, PartialEq, Debug)]
221#[repr(transparent)]
222pub struct LinearGradientBrush(SharedVector<GradientStop>);
223
224impl LinearGradientBrush {
225    /// Creates a new linear gradient, described by the specified angle and the provided color stops.
226    ///
227    /// The angle need to be specified in degrees.
228    /// The stops don't need to be sorted as this function will sort them.
229    pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
230        let stop_iter = stops.into_iter();
231        let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
232        // The gradient's first stop is a fake stop to store the angle
233        encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
234        encoded_angle_and_stops.extend(stop_iter);
235        Self(encoded_angle_and_stops)
236    }
237    /// Returns the angle of the linear gradient in degrees.
238    pub fn angle(&self) -> f32 {
239        self.0[0].position
240    }
241    /// Returns the color stops of the linear gradient.
242    /// The stops are sorted by positions.
243    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
244        // skip the first fake stop that just contains the angle
245        self.0.iter().skip(1)
246    }
247}
248
249/// The RadialGradientBrush describes a way of filling a shape with a circular gradient
250#[derive(Clone, PartialEq, Debug)]
251#[repr(transparent)]
252pub struct RadialGradientBrush(SharedVector<GradientStop>);
253
254impl RadialGradientBrush {
255    /// Creates a new circle radial gradient, centered in the middle and described
256    /// by the provided color stops.
257    pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
258        Self(stops.into_iter().collect())
259    }
260    /// Returns the color stops of the linear gradient.
261    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
262        self.0.iter()
263    }
264}
265
266/// The ConicGradientBrush describes a way of filling a shape with a gradient
267/// that rotates around a center point
268#[derive(Clone, PartialEq, Debug)]
269#[repr(transparent)]
270pub struct ConicGradientBrush(SharedVector<GradientStop>);
271
272impl ConicGradientBrush {
273    /// Creates a new conic gradient, described by the specified angle and the provided color stops.
274    ///
275    /// The angle need to be specified in degrees (CSS `from <angle>` syntax).
276    /// The stops don't need to be sorted as this function will normalize and process them.
277    pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
278        let stop_iter = stops.into_iter();
279        let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
280        // The gradient's first stop is a fake stop to store the angle
281        encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
282        encoded_angle_and_stops.extend(stop_iter);
283        let mut result = Self(encoded_angle_and_stops);
284        result.normalize_stops();
285        if angle.abs() > f32::EPSILON {
286            result.apply_rotation(angle);
287        }
288        result
289    }
290
291    /// Normalizes the gradient stops to be within [0, 1] range with proper boundary stops.
292    fn normalize_stops(&mut self) {
293        // Check if we need to make any changes
294        let stops_slice = &self.0[1..];
295        let has_stop_at_0 = stops_slice.iter().any(|s| s.position.abs() < f32::EPSILON);
296        let has_stop_at_1 = stops_slice.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
297        let has_stops_outside = stops_slice.iter().any(|s| s.position < 0.0 || s.position > 1.0);
298        let is_empty = stops_slice.is_empty();
299
300        // If no changes needed, return early
301        if has_stop_at_0 && has_stop_at_1 && !has_stops_outside && !is_empty {
302            return;
303        }
304
305        // Need to make changes, so copy
306        let mut stops: alloc::vec::Vec<_> = stops_slice.iter().copied().collect();
307
308        // Add interpolated boundary stop at 0.0 if needed
309        if !has_stop_at_0 {
310            let stop_below_0 = stops.iter().filter(|s| s.position < 0.0).max_by(|a, b| {
311                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
312            });
313            let stop_above_0 = stops.iter().filter(|s| s.position > 0.0).min_by(|a, b| {
314                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
315            });
316            if let (Some(below), Some(above)) = (stop_below_0, stop_above_0) {
317                let t = (0.0 - below.position) / (above.position - below.position);
318                let color_at_0 = Self::interpolate_color(&below.color, &above.color, t);
319                stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
320            } else if let Some(above) = stop_above_0 {
321                stops.insert(0, GradientStop { position: 0.0, color: above.color });
322            } else if let Some(below) = stop_below_0 {
323                stops.insert(0, GradientStop { position: 0.0, color: below.color });
324            }
325        }
326
327        // Add interpolated boundary stop at 1.0 if needed
328        if !has_stop_at_1 {
329            let stop_below_1 = stops.iter().filter(|s| s.position < 1.0).max_by(|a, b| {
330                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
331            });
332            let stop_above_1 = stops.iter().filter(|s| s.position > 1.0).min_by(|a, b| {
333                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
334            });
335
336            if let (Some(below), Some(above)) = (stop_below_1, stop_above_1) {
337                let t = (1.0 - below.position) / (above.position - below.position);
338                let color_at_1 = Self::interpolate_color(&below.color, &above.color, t);
339                stops.push(GradientStop { position: 1.0, color: color_at_1 });
340            } else if let Some(below) = stop_below_1 {
341                stops.push(GradientStop { position: 1.0, color: below.color });
342            } else if let Some(above) = stop_above_1 {
343                stops.push(GradientStop { position: 1.0, color: above.color });
344            }
345        }
346
347        // Drop stops outside [0, 1] range
348        if has_stops_outside {
349            stops.retain(|s| 0.0 <= s.position && s.position <= 1.0);
350        }
351
352        // Handle empty gradients
353        if stops.is_empty() {
354            stops.push(GradientStop { position: 0.0, color: Color::default() });
355            stops.push(GradientStop { position: 1.0, color: Color::default() });
356        }
357
358        // Update the internal storage
359        let angle = self.angle();
360        self.0 = SharedVector::with_capacity(stops.len() + 1);
361        self.0.push(GradientStop { color: Default::default(), position: angle });
362        self.0.extend(stops.into_iter());
363    }
364
365    /// Apply rotation to the gradient (CSS `from <angle>` syntax).
366    ///
367    /// The `from_angle` parameter is specified in degrees and rotates the entire gradient clockwise.
368    fn apply_rotation(&mut self, from_angle: f32) {
369        // Convert degrees to normalized 0-1 range
370        let normalized_from_angle = (from_angle / 360.0) - (from_angle / 360.0).floor();
371
372        // If no rotation needed, just update the stored angle
373        if normalized_from_angle.abs() < f32::EPSILON {
374            self.0.make_mut_slice()[0].position = from_angle;
375            return;
376        }
377
378        // Update the stored angle
379        self.0.make_mut_slice()[0].position = from_angle;
380
381        // Need to rotate, so copy
382        let mut stops: alloc::vec::Vec<_> = self.0.iter().skip(1).copied().collect();
383
384        // Adjust first stop (at 0.0) to avoid duplicate with stop at 1.0
385        if let Some(first) = stops.first_mut() {
386            if first.position.abs() < f32::EPSILON {
387                first.position = f32::EPSILON;
388            }
389        }
390
391        // Step 1: Apply rotation by adding from_angle and wrapping to [0, 1) range
392        stops = stops
393            .iter()
394            .map(|stop| {
395                #[cfg(feature = "std")]
396                let rotated_position = (stop.position + normalized_from_angle).rem_euclid(1.0);
397                #[cfg(not(feature = "std"))]
398                let rotated_position = (stop.position + normalized_from_angle).rem_euclid(&1.0);
399                GradientStop { position: rotated_position, color: stop.color }
400            })
401            .collect();
402
403        // Step 2: Separate duplicate positions with different colors to avoid flickering
404        for i in 0..stops.len() {
405            let j = (i + 1) % stops.len();
406            if (stops[i].position - stops[j].position).abs() < f32::EPSILON
407                && stops[i].color != stops[j].color
408            {
409                stops[i].position = (stops[i].position - f32::EPSILON).max(0.0);
410                stops[j].position = (stops[j].position + f32::EPSILON).min(1.0);
411            }
412        }
413
414        // Step 3: Sort by rotated position
415        stops.sort_by(|a, b| {
416            a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
417        });
418
419        // Step 4: Add boundary stops at 0.0 and 1.0 if missing
420        let has_stop_at_0 = stops.iter().any(|s| s.position.abs() < f32::EPSILON);
421        if !has_stop_at_0 {
422            if let (Some(last), Some(first)) = (stops.last(), stops.first()) {
423                let gap = 1.0 - last.position + first.position;
424                let color_at_0 = if gap > f32::EPSILON {
425                    let t = (1.0 - last.position) / gap;
426                    Self::interpolate_color(&last.color, &first.color, t)
427                } else {
428                    last.color
429                };
430                stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
431            }
432        }
433
434        let has_stop_at_1 = stops.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
435        if !has_stop_at_1 {
436            if let Some(first) = stops.first() {
437                stops.push(GradientStop { position: 1.0, color: first.color });
438            }
439        }
440
441        // Update the internal storage
442        self.0 = SharedVector::with_capacity(stops.len() + 1);
443        self.0.push(GradientStop { color: Default::default(), position: from_angle });
444        self.0.extend(stops.into_iter());
445    }
446
447    /// Returns the starting angle (rotation) of the conic gradient in degrees.
448    fn angle(&self) -> f32 {
449        self.0[0].position
450    }
451
452    /// Returns the color stops of the conic gradient.
453    /// The stops are already rotated according to the `from_angle` specified in `new()`.
454    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
455        // skip the first fake stop that just contains the angle
456        self.0.iter().skip(1)
457    }
458
459    /// Helper: Linearly interpolate between two colors using premultiplied alpha.
460    ///
461    /// This is used for interpolating gradient boundary colors in CSS-style gradients.
462    /// We cannot use Color::mix() here because it implements Sass color mixing algorithm,
463    /// which is different from CSS gradient color interpolation.
464    ///
465    /// CSS gradients interpolate in premultiplied RGBA space:
466    /// https://www.w3.org/TR/css-color-4/#interpolation-alpha
467    fn interpolate_color(c1: &Color, c2: &Color, factor: f32) -> Color {
468        let argb1 = c1.to_argb_u8();
469        let argb2 = c2.to_argb_u8();
470
471        // Convert to premultiplied alpha
472        let a1 = argb1.alpha as f32 / 255.0;
473        let a2 = argb2.alpha as f32 / 255.0;
474        let r1 = argb1.red as f32 * a1;
475        let g1 = argb1.green as f32 * a1;
476        let b1 = argb1.blue as f32 * a1;
477        let r2 = argb2.red as f32 * a2;
478        let g2 = argb2.green as f32 * a2;
479        let b2 = argb2.blue as f32 * a2;
480
481        // Interpolate in premultiplied space
482        let alpha = (1.0 - factor) * a1 + factor * a2;
483        let red = (1.0 - factor) * r1 + factor * r2;
484        let green = (1.0 - factor) * g1 + factor * g2;
485        let blue = (1.0 - factor) * b1 + factor * b2;
486
487        // Convert back from premultiplied alpha
488        if alpha > 0.0 {
489            Color::from_argb_u8(
490                (alpha * 255.0) as u8,
491                (red / alpha).min(255.0) as u8,
492                (green / alpha).min(255.0) as u8,
493                (blue / alpha).min(255.0) as u8,
494            )
495        } else {
496            Color::from_argb_u8(0, 0, 0, 0)
497        }
498    }
499}
500
501/// C FFI function to normalize the gradient stops to be within [0, 1] range
502#[cfg(feature = "ffi")]
503#[unsafe(no_mangle)]
504pub extern "C" fn slint_conic_gradient_normalize_stops(gradient: &mut ConicGradientBrush) {
505    gradient.normalize_stops();
506}
507
508/// C FFI function to apply rotation to a ConicGradientBrush
509#[cfg(feature = "ffi")]
510#[unsafe(no_mangle)]
511pub extern "C" fn slint_conic_gradient_apply_rotation(
512    gradient: &mut ConicGradientBrush,
513    angle_degrees: f32,
514) {
515    gradient.apply_rotation(angle_degrees);
516}
517
518/// GradientStop describes a single color stop in a gradient. The colors between multiple
519/// stops are interpolated.
520#[repr(C)]
521#[derive(Copy, Clone, Debug, PartialEq)]
522pub struct GradientStop {
523    /// The color to draw at this stop.
524    pub color: Color,
525    /// The position of this stop on the entire shape, as a normalized value between 0 and 1.
526    pub position: f32,
527}
528
529/// Returns the start / end points of a gradient within a rectangle of the given size, based on the angle (in degree).
530pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
531    let angle = (angle + 90.).to_radians();
532    let (s, c) = angle.sin_cos();
533
534    let (a, b) = if s.abs() < f32::EPSILON {
535        let y = size.height / 2.;
536        return if c < 0. {
537            (Point2D::new(0., y), Point2D::new(size.width, y))
538        } else {
539            (Point2D::new(size.width, y), Point2D::new(0., y))
540        };
541    } else if c * s < 0. {
542        // Intersection between the gradient line, and an orthogonal line that goes through (height, 0)
543        let x = (s * size.width + c * size.height) * s / 2.;
544        let y = -c * x / s + size.height;
545        (Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
546    } else {
547        // Intersection between the gradient line, and an orthogonal line that goes through (0, 0)
548        let x = (s * size.width - c * size.height) * s / 2.;
549        let y = -c * x / s;
550        (Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
551    };
552
553    if s > 0. { (a, b) } else { (b, a) }
554}
555
556impl InterpolatedPropertyValue for Brush {
557    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
558        match (self, target_value) {
559            (Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
560                Brush::SolidColor(source_col.interpolate(target_col, t))
561            }
562            (Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
563                let mut new_grad = grad.clone();
564                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
565                    x.color = col.interpolate(&x.color, t);
566                }
567                Brush::LinearGradient(new_grad)
568            }
569            (a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
570                Self::interpolate(b, a, 1. - t)
571            }
572            (Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
573                if lhs.0.len() < rhs.0.len() {
574                    Self::interpolate(target_value, self, 1. - t)
575                } else {
576                    let mut new_grad = lhs.clone();
577                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
578                    {
579                        let angle = &mut iter.next().unwrap().position;
580                        *angle = angle.interpolate(&rhs.angle(), t);
581                    }
582                    for s2 in rhs.stops() {
583                        let s1 = iter.next().unwrap();
584                        s1.color = s1.color.interpolate(&s2.color, t);
585                        s1.position = s1.position.interpolate(&s2.position, t);
586                    }
587                    for x in iter {
588                        x.position = x.position.interpolate(&1.0, t);
589                    }
590                    Brush::LinearGradient(new_grad)
591                }
592            }
593            (Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
594                let mut new_grad = grad.clone();
595                for x in new_grad.0.make_mut_slice().iter_mut() {
596                    x.color = col.interpolate(&x.color, t);
597                }
598                Brush::RadialGradient(new_grad)
599            }
600            (a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
601                Self::interpolate(b, a, 1. - t)
602            }
603            (Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
604                if lhs.0.len() < rhs.0.len() {
605                    Self::interpolate(target_value, self, 1. - t)
606                } else {
607                    let mut new_grad = lhs.clone();
608                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
609                    let mut last_color = Color::default();
610                    for s2 in rhs.stops() {
611                        let s1 = iter.next().unwrap();
612                        last_color = s2.color;
613                        s1.color = s1.color.interpolate(&s2.color, t);
614                        s1.position = s1.position.interpolate(&s2.position, t);
615                    }
616                    for x in iter {
617                        x.position = x.position.interpolate(&1.0, t);
618                        x.color = x.color.interpolate(&last_color, t);
619                    }
620                    Brush::RadialGradient(new_grad)
621                }
622            }
623            (Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
624                let mut new_grad = grad.clone();
625                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
626                    x.color = col.interpolate(&x.color, t);
627                }
628                Brush::ConicGradient(new_grad)
629            }
630            (a @ Brush::ConicGradient(_), b @ Brush::SolidColor(_)) => {
631                Self::interpolate(b, a, 1. - t)
632            }
633            (Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
634                if lhs.0.len() < rhs.0.len() {
635                    Self::interpolate(target_value, self, 1. - t)
636                } else {
637                    let mut new_grad = lhs.clone();
638                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
639                    {
640                        let angle = &mut iter.next().unwrap().position;
641                        *angle = angle.interpolate(&rhs.angle(), t);
642                    }
643                    for s2 in rhs.stops() {
644                        let s1 = iter.next().unwrap();
645                        s1.color = s1.color.interpolate(&s2.color, t);
646                        s1.position = s1.position.interpolate(&s2.position, t);
647                    }
648                    for x in iter {
649                        x.position = x.position.interpolate(&1.0, t);
650                    }
651                    Brush::ConicGradient(new_grad)
652                }
653            }
654            (a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
655            | (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_))
656            | (a @ Brush::LinearGradient(_), b @ Brush::ConicGradient(_))
657            | (a @ Brush::ConicGradient(_), b @ Brush::LinearGradient(_))
658            | (a @ Brush::RadialGradient(_), b @ Brush::ConicGradient(_))
659            | (a @ Brush::ConicGradient(_), b @ Brush::RadialGradient(_)) => {
660                // Just go to an intermediate color.
661                let color = Color::interpolate(&b.color(), &a.color(), t);
662                if t < 0.5 {
663                    Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
664                } else {
665                    Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
666                }
667            }
668        }
669    }
670}
671
672#[test]
673#[allow(clippy::float_cmp)] // We want bit-wise equality here
674fn test_linear_gradient_encoding() {
675    let stops: SharedVector<GradientStop> = [
676        GradientStop { position: 0.0, color: Color::from_argb_u8(255, 255, 0, 0) },
677        GradientStop { position: 0.5, color: Color::from_argb_u8(255, 0, 255, 0) },
678        GradientStop { position: 1.0, color: Color::from_argb_u8(255, 0, 0, 255) },
679    ]
680    .into();
681    let grad = LinearGradientBrush::new(256., stops.clone());
682    assert_eq!(grad.angle(), 256.);
683    assert!(grad.stops().eq(stops.iter()));
684}
685
686#[test]
687fn test_conic_gradient_basic() {
688    // Test basic conic gradient with no rotation
689    let grad = ConicGradientBrush::new(
690        0.0,
691        [
692            GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
693            GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
694            GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
695        ],
696    );
697    assert_eq!(grad.angle(), 0.0);
698    assert_eq!(grad.stops().count(), 3);
699}
700
701#[test]
702fn test_conic_gradient_with_rotation() {
703    // Test conic gradient with 90 degree rotation
704    let grad = ConicGradientBrush::new(
705        90.0,
706        [
707            GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
708            GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
709        ],
710    );
711    assert_eq!(grad.angle(), 90.0);
712    // After rotation, stops should still be present and sorted
713    assert!(grad.stops().count() >= 2);
714}
715
716#[test]
717fn test_conic_gradient_negative_angle() {
718    // Test with negative angle - should be normalized
719    let grad = ConicGradientBrush::new(
720        -90.0,
721        [GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) }],
722    );
723    assert_eq!(grad.angle(), -90.0); // Angle is stored as-is
724    assert!(grad.stops().count() >= 2); // Should have boundary stops added
725}
726
727#[test]
728fn test_conic_gradient_stops_outside_range() {
729    // Test with stops outside [0, 1] range
730    let grad = ConicGradientBrush::new(
731        0.0,
732        [
733            GradientStop { position: -0.2, color: Color::from_rgb_u8(255, 0, 0) },
734            GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
735            GradientStop { position: 1.2, color: Color::from_rgb_u8(0, 0, 255) },
736        ],
737    );
738    // All stops should be within [0, 1] after processing
739    for stop in grad.stops() {
740        assert!(stop.position >= 0.0 && stop.position <= 1.0);
741    }
742}
743
744#[test]
745fn test_conic_gradient_all_stops_below_zero() {
746    // Test edge case: all stops are below 0
747    let grad = ConicGradientBrush::new(
748        0.0,
749        [
750            GradientStop { position: -0.5, color: Color::from_rgb_u8(255, 0, 0) },
751            GradientStop { position: -0.3, color: Color::from_rgb_u8(0, 255, 0) },
752        ],
753    );
754    // Should create valid boundary stops
755    assert!(grad.stops().count() >= 2);
756    // First stop should be at or near 0.0
757    let first = grad.stops().next().unwrap();
758    assert!(first.position >= 0.0 && first.position < 0.1);
759}
760
761#[test]
762fn test_conic_gradient_all_stops_above_one() {
763    // Test edge case: all stops are above 1
764    let grad = ConicGradientBrush::new(
765        0.0,
766        [
767            GradientStop { position: 1.2, color: Color::from_rgb_u8(255, 0, 0) },
768            GradientStop { position: 1.5, color: Color::from_rgb_u8(0, 255, 0) },
769        ],
770    );
771    // Should create valid boundary stops
772    assert!(grad.stops().count() >= 2);
773    // Last stop should be at or near 1.0
774    let last = grad.stops().last().unwrap();
775    assert!(last.position > 0.9 && last.position <= 1.0);
776}
777
778#[test]
779fn test_conic_gradient_empty() {
780    // Test edge case: no stops provided
781    let grad = ConicGradientBrush::new(0.0, []);
782    // Should create default transparent stops
783    assert_eq!(grad.stops().count(), 2);
784}