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