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 circular gradient.
30    /// The center defaults to the middle of the bounding box.
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                let mut new_grad = g.clone();
112                for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
113                    s.color = s.color.brighter(factor);
114                }
115                Brush::RadialGradient(new_grad)
116            }
117            Brush::ConicGradient(g) => {
118                let mut new_grad = g.clone();
119                for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
120                    x.color = x.color.brighter(factor);
121                }
122                Brush::ConicGradient(new_grad)
123            }
124        }
125    }
126
127    /// Returns a new version of this brush that has the brightness decreased
128    /// by the specified factor. This is done by calling [`Color::darker`] on
129    /// all the color of this brush.
130    #[must_use]
131    pub fn darker(&self, factor: f32) -> Self {
132        match self {
133            Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
134            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
135                g.angle(),
136                g.stops()
137                    .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
138            )),
139            Brush::RadialGradient(g) => {
140                let mut new_grad = g.clone();
141                for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
142                    s.color = s.color.darker(factor);
143                }
144                Brush::RadialGradient(new_grad)
145            }
146            Brush::ConicGradient(g) => {
147                let mut new_grad = g.clone();
148                for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
149                    x.color = x.color.darker(factor);
150                }
151                Brush::ConicGradient(new_grad)
152            }
153        }
154    }
155
156    /// Returns a new version of this brush with the opacity decreased by `factor`.
157    ///
158    /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
159    ///
160    /// See also [`Color::transparentize`]
161    #[must_use]
162    pub fn transparentize(&self, amount: f32) -> Self {
163        match self {
164            Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
165            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
166                g.angle(),
167                g.stops().map(|s| GradientStop {
168                    color: s.color.transparentize(amount),
169                    position: s.position,
170                }),
171            )),
172            Brush::RadialGradient(g) => {
173                let mut new_grad = g.clone();
174                for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
175                    s.color = s.color.transparentize(amount);
176                }
177                Brush::RadialGradient(new_grad)
178            }
179            Brush::ConicGradient(g) => {
180                let mut new_grad = g.clone();
181                for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
182                    x.color = x.color.transparentize(amount);
183                }
184                Brush::ConicGradient(new_grad)
185            }
186        }
187    }
188
189    /// Returns a new version of this brush with the related color's opacities
190    /// set to `alpha`.
191    #[must_use]
192    pub fn with_alpha(&self, alpha: f32) -> Self {
193        match self {
194            Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
195            Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
196                g.angle(),
197                g.stops().map(|s| GradientStop {
198                    color: s.color.with_alpha(alpha),
199                    position: s.position,
200                }),
201            )),
202            Brush::RadialGradient(g) => {
203                let mut new_grad = g.clone();
204                for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
205                    s.color = s.color.with_alpha(alpha);
206                }
207                Brush::RadialGradient(new_grad)
208            }
209            Brush::ConicGradient(g) => {
210                let mut new_grad = g.clone();
211                for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
212                    x.color = x.color.with_alpha(alpha);
213                }
214                Brush::ConicGradient(new_grad)
215            }
216        }
217    }
218}
219
220/// The LinearGradientBrush describes a way of filling a shape with different colors, which
221/// are interpolated between different stops. The colors are aligned with a line that's rotated
222/// by the LinearGradient's angle.
223#[derive(Clone, PartialEq, Debug)]
224#[repr(transparent)]
225pub struct LinearGradientBrush(SharedVector<GradientStop>);
226
227impl LinearGradientBrush {
228    /// Creates a new linear gradient, described by the specified angle and the provided color stops.
229    ///
230    /// The angle need to be specified in degrees.
231    /// The stops don't need to be sorted as this function will sort them.
232    pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
233        let stop_iter = stops.into_iter();
234        let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
235        // The gradient's first stop is a fake stop to store the angle
236        encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
237        encoded_angle_and_stops.extend(stop_iter);
238        Self(encoded_angle_and_stops)
239    }
240    /// Returns the angle of the linear gradient in degrees.
241    pub fn angle(&self) -> f32 {
242        self.0[0].position
243    }
244    /// Returns the color stops of the linear gradient.
245    /// The stops are sorted by positions.
246    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
247        // skip the first fake stop that just contains the angle
248        self.0.iter().skip(1)
249    }
250}
251
252/// NaN-aware float equality: two NaNs compare equal (unlike IEEE 754).
253#[inline]
254fn nan_eq(a: f32, b: f32) -> bool {
255    a == b || (a.is_nan() && b.is_nan())
256}
257
258/// Shared center resolution for the logical and scaled radial/conic methods.
259/// When `scale_factor` is 1.0 this is identical to the unscaled case.
260#[inline]
261fn center_or_bbox(cx: f32, cy: f32, width: f32, height: f32, scale_factor: f32) -> (f32, f32) {
262    if cx.is_nan() { (width / 2.0, height / 2.0) } else { (cx * scale_factor, cy * scale_factor) }
263}
264
265/// The RadialGradientBrush describes a way of filling a shape with a circular gradient.
266///
267/// The center defaults to the middle of the bounding box; the radius defaults to half the
268/// bounding box diagonal. Use [`with_center`](Self::with_center) and
269/// [`with_radius`](Self::with_radius) to override these defaults.
270///
271/// Internally the brush encodes center and radius as the first three fake
272/// [`GradientStop`] entries (indices 0–2), following the same pattern as
273/// [`LinearGradientBrush`] (which stores the angle as stop 0).
274#[derive(Clone, Debug)]
275#[repr(transparent)]
276pub struct RadialGradientBrush(SharedVector<GradientStop>);
277
278impl RadialGradientBrush {
279    const HEADER: usize = 3;
280
281    /// Creates a new circle radial gradient centered in the element's bounding box,
282    /// described by the provided color stops.
283    pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
284        let stop_iter = stops.into_iter();
285        let mut v = SharedVector::with_capacity(Self::HEADER + stop_iter.size_hint().0);
286        // Header stops: center_x (NaN=bbox), center_y (NaN=bbox), radius (negative=bbox diagonal/2)
287        v.push(GradientStop { color: Default::default(), position: f32::NAN });
288        v.push(GradientStop { color: Default::default(), position: f32::NAN });
289        v.push(GradientStop { color: Default::default(), position: -1.0 });
290        v.extend(stop_iter);
291        Self(v)
292    }
293
294    #[inline]
295    fn center_x(&self) -> f32 {
296        self.0[0].position
297    }
298    #[inline]
299    fn center_y(&self) -> f32 {
300        self.0[1].position
301    }
302    #[inline]
303    fn radius(&self) -> f32 {
304        self.0[2].position
305    }
306
307    /// Returns the color stops of the radial gradient.
308    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
309        self.0.iter().skip(Self::HEADER)
310    }
311
312    /// Sets an explicit center, returning `self` for chaining. `cx` and `cy` are in the
313    /// element's local logical coordinate space.
314    pub fn with_center(mut self, cx: f32, cy: f32) -> Self {
315        let s = self.0.make_mut_slice();
316        s[0].position = cx;
317        s[1].position = cy;
318        self
319    }
320
321    /// Sets an explicit radius, returning `self` for chaining. `r` is in the element's local
322    /// logical coordinate space.
323    pub fn with_radius(mut self, r: f32) -> Self {
324        self.0.make_mut_slice()[2].position = r;
325        self
326    }
327
328    /// Returns the gradient center, falling back to the bounding box center when not explicitly set.
329    ///
330    /// `width` and `height` are the element's logical dimensions.
331    pub fn center_or_default(&self, width: f32, height: f32) -> (f32, f32) {
332        debug_assert!(
333            self.center_x().is_nan() == self.center_y().is_nan(),
334            "center_x and center_y must both be NaN or both finite"
335        );
336        center_or_bbox(self.center_x(), self.center_y(), width, height, 1.0)
337    }
338
339    /// Returns the gradient center in a scaled coordinate space.
340    ///
341    /// `width` and `height` are the dimensions in the target coordinate space. Explicit center
342    /// values are local logical lengths and are multiplied by `scale_factor`; default centers are
343    /// derived from the dimensions directly.
344    pub fn center_or_default_scaled(
345        &self,
346        width: f32,
347        height: f32,
348        scale_factor: f32,
349    ) -> (f32, f32) {
350        debug_assert!(
351            self.center_x().is_nan() == self.center_y().is_nan(),
352            "center_x and center_y must both be NaN or both finite"
353        );
354        center_or_bbox(self.center_x(), self.center_y(), width, height, scale_factor)
355    }
356
357    /// Returns the gradient radius, falling back to half of the bounding box diagonal when not
358    /// explicitly set.
359    ///
360    /// `width` and `height` are the element's logical dimensions.
361    pub fn radius_or_default(&self, width: f32, height: f32) -> f32 {
362        let r = self.radius();
363        if r < 0.0 { 0.5 * (width * width + height * height).sqrt() } else { r }
364    }
365
366    /// Returns the gradient radius in a scaled coordinate space.
367    ///
368    /// `width` and `height` are the dimensions in the target coordinate space. Explicit radius
369    /// values are local logical lengths and are multiplied by `scale_factor`; the default radius is
370    /// derived from the dimensions directly.
371    pub fn radius_or_default_scaled(&self, width: f32, height: f32, scale_factor: f32) -> f32 {
372        let r = self.radius();
373        if r < 0.0 { 0.5 * (width * width + height * height).sqrt() } else { r * scale_factor }
374    }
375}
376
377/// Equality is render-equivalence: two NaN center fields compare equal because both use the
378/// bounding box center. Any two negative radii compare equal because both use the default radius.
379impl PartialEq for RadialGradientBrush {
380    fn eq(&self, other: &Self) -> bool {
381        if self.0.len() != other.0.len() {
382            return false;
383        }
384        nan_eq(self.center_x(), other.center_x())
385            && nan_eq(self.center_y(), other.center_y())
386            && (self.radius() == other.radius() || (self.radius() < 0.0 && other.radius() < 0.0))
387            && self.0.iter().skip(Self::HEADER).eq(other.0.iter().skip(Self::HEADER))
388    }
389}
390
391/// The ConicGradientBrush describes a way of filling a shape with a gradient
392/// that rotates around a center point.
393///
394/// The center defaults to the middle of the bounding box. Use
395/// [`with_center`](Self::with_center) to override.
396///
397/// Internally the first three fake [`GradientStop`] entries encode the starting angle
398/// (index 0), center_x (index 1), and center_y (index 2). Real color stops begin at index 3.
399#[derive(Clone, Debug)]
400#[repr(transparent)]
401pub struct ConicGradientBrush(SharedVector<GradientStop>);
402
403/// Equality is render-equivalence: two NaN center fields compare equal because both use the
404/// bounding box center.
405impl PartialEq for ConicGradientBrush {
406    fn eq(&self, other: &Self) -> bool {
407        if self.0.len() != other.0.len() {
408            return false;
409        }
410        // angle (index 0) uses plain f32 equality (not NaN-aware)
411        self.0[0].position == other.0[0].position
412            && nan_eq(self.center_x(), other.center_x())
413            && nan_eq(self.center_y(), other.center_y())
414            && self.0.iter().skip(Self::HEADER).eq(other.0.iter().skip(Self::HEADER))
415    }
416}
417
418impl ConicGradientBrush {
419    const HEADER: usize = 3;
420
421    /// Creates a new conic gradient, described by the specified angle and the provided color stops.
422    ///
423    /// The angle need to be specified in degrees (CSS `from <angle>` syntax).
424    /// The stops don't need to be sorted as this function will normalize and process them.
425    pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
426        let stop_iter = stops.into_iter();
427        let mut v = SharedVector::with_capacity(Self::HEADER + stop_iter.size_hint().0);
428        // Header stops: angle, center_x (NaN=bbox), center_y (NaN=bbox)
429        v.push(GradientStop { color: Default::default(), position: angle });
430        v.push(GradientStop { color: Default::default(), position: f32::NAN });
431        v.push(GradientStop { color: Default::default(), position: f32::NAN });
432        v.extend(stop_iter);
433        let mut result = Self(v);
434        result.normalize_stops();
435        if angle.abs() > f32::EPSILON {
436            result.apply_rotation(angle);
437        }
438        result
439    }
440
441    /// Normalizes the gradient stops to be within [0, 1] range with proper boundary stops.
442    fn normalize_stops(&mut self) {
443        // Check if we need to make any changes
444        let stops_slice = &self.0[Self::HEADER..];
445        let has_stop_at_0 = stops_slice.iter().any(|s| s.position.abs() < f32::EPSILON);
446        let has_stop_at_1 = stops_slice.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
447        let has_stops_outside = stops_slice.iter().any(|s| s.position < 0.0 || s.position > 1.0);
448        let is_empty = stops_slice.is_empty();
449
450        // If no changes needed, return early
451        if has_stop_at_0 && has_stop_at_1 && !has_stops_outside && !is_empty {
452            return;
453        }
454
455        // Need to make changes, so copy
456        let mut stops: alloc::vec::Vec<_> = stops_slice.to_vec();
457
458        // Add interpolated boundary stop at 0.0 if needed
459        if !has_stop_at_0 {
460            let stop_below_0 = stops.iter().filter(|s| s.position < 0.0).max_by(|a, b| {
461                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
462            });
463            let stop_above_0 = stops.iter().filter(|s| s.position > 0.0).min_by(|a, b| {
464                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
465            });
466            if let (Some(below), Some(above)) = (stop_below_0, stop_above_0) {
467                let t = (0.0 - below.position) / (above.position - below.position);
468                let color_at_0 = Self::interpolate_color(&below.color, &above.color, t);
469                stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
470            } else if let Some(above) = stop_above_0 {
471                stops.insert(0, GradientStop { position: 0.0, color: above.color });
472            } else if let Some(below) = stop_below_0 {
473                stops.insert(0, GradientStop { position: 0.0, color: below.color });
474            }
475        }
476
477        // Add interpolated boundary stop at 1.0 if needed
478        if !has_stop_at_1 {
479            let stop_below_1 = stops.iter().filter(|s| s.position < 1.0).max_by(|a, b| {
480                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
481            });
482            let stop_above_1 = stops.iter().filter(|s| s.position > 1.0).min_by(|a, b| {
483                a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
484            });
485
486            if let (Some(below), Some(above)) = (stop_below_1, stop_above_1) {
487                let t = (1.0 - below.position) / (above.position - below.position);
488                let color_at_1 = Self::interpolate_color(&below.color, &above.color, t);
489                stops.push(GradientStop { position: 1.0, color: color_at_1 });
490            } else if let Some(below) = stop_below_1 {
491                stops.push(GradientStop { position: 1.0, color: below.color });
492            } else if let Some(above) = stop_above_1 {
493                stops.push(GradientStop { position: 1.0, color: above.color });
494            }
495        }
496
497        // Drop stops outside [0, 1] range
498        if has_stops_outside {
499            stops.retain(|s| 0.0 <= s.position && s.position <= 1.0);
500        }
501
502        // Handle empty gradients
503        if stops.is_empty() {
504            stops.push(GradientStop { position: 0.0, color: Color::default() });
505            stops.push(GradientStop { position: 1.0, color: Color::default() });
506        }
507
508        // Rebuild internal storage, preserving the full header (angle, center_x, center_y)
509        let angle = self.angle();
510        let cx = self.center_x();
511        let cy = self.center_y();
512        self.0 = SharedVector::with_capacity(stops.len() + Self::HEADER);
513        self.0.push(GradientStop { color: Default::default(), position: angle });
514        self.0.push(GradientStop { color: Default::default(), position: cx });
515        self.0.push(GradientStop { color: Default::default(), position: cy });
516        self.0.extend(stops);
517    }
518
519    /// Apply rotation to the gradient (CSS `from <angle>` syntax).
520    ///
521    /// The `from_angle` parameter is specified in degrees and rotates the entire gradient clockwise.
522    fn apply_rotation(&mut self, from_angle: f32) {
523        // Convert degrees to normalized 0-1 range
524        let normalized_from_angle = (from_angle / 360.0) - (from_angle / 360.0).floor();
525
526        // If no rotation needed, just update the stored angle
527        if normalized_from_angle.abs() < f32::EPSILON {
528            self.0.make_mut_slice()[0].position = from_angle;
529            return;
530        }
531
532        // Update the stored angle
533        self.0.make_mut_slice()[0].position = from_angle;
534
535        // Need to rotate, so copy
536        let mut stops: alloc::vec::Vec<_> = self.0.iter().skip(Self::HEADER).copied().collect();
537
538        // Adjust first stop (at 0.0) to avoid duplicate with stop at 1.0
539        if let Some(first) = stops.first_mut()
540            && first.position.abs() < f32::EPSILON
541        {
542            first.position = f32::EPSILON;
543        }
544
545        // Step 1: Apply rotation by adding from_angle and wrapping to [0, 1) range
546        stops = stops
547            .iter()
548            .map(|stop| {
549                // f32::rem_euclid is not always available, and when it is it has a
550                // different signature than num_traits::Euclid::rem_euclid (issue #11333).
551                let rotated_position =
552                    num_traits::Euclid::rem_euclid(&(stop.position + normalized_from_angle), &1.0);
553                GradientStop { position: rotated_position, color: stop.color }
554            })
555            .collect();
556
557        // Step 2: Separate duplicate positions with different colors to avoid flickering
558        for i in 0..stops.len() {
559            let j = (i + 1) % stops.len();
560            if (stops[i].position - stops[j].position).abs() < f32::EPSILON
561                && stops[i].color != stops[j].color
562            {
563                stops[i].position = (stops[i].position - f32::EPSILON).max(0.0);
564                stops[j].position = (stops[j].position + f32::EPSILON).min(1.0);
565            }
566        }
567
568        // Step 3: Sort by rotated position
569        stops.sort_by(|a, b| {
570            a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
571        });
572
573        // Step 4: Add boundary stops at 0.0 and 1.0 if missing
574        let has_stop_at_0 = stops.iter().any(|s| s.position.abs() < f32::EPSILON);
575        if !has_stop_at_0 && let (Some(last), Some(first)) = (stops.last(), stops.first()) {
576            let gap = 1.0 - last.position + first.position;
577            let color_at_0 = if gap > f32::EPSILON {
578                let t = (1.0 - last.position) / gap;
579                Self::interpolate_color(&last.color, &first.color, t)
580            } else {
581                last.color
582            };
583            stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
584        }
585
586        let has_stop_at_1 = stops.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
587        if !has_stop_at_1 && let Some(first) = stops.first() {
588            stops.push(GradientStop { position: 1.0, color: first.color });
589        }
590
591        // Rebuild internal storage, preserving the full header (angle, center_x, center_y)
592        let cx = self.center_x();
593        let cy = self.center_y();
594        self.0 = SharedVector::with_capacity(stops.len() + Self::HEADER);
595        self.0.push(GradientStop { color: Default::default(), position: from_angle });
596        self.0.push(GradientStop { color: Default::default(), position: cx });
597        self.0.push(GradientStop { color: Default::default(), position: cy });
598        self.0.extend(stops);
599    }
600
601    /// Returns the starting angle (rotation) of the conic gradient in degrees.
602    fn angle(&self) -> f32 {
603        self.0[0].position
604    }
605
606    #[inline]
607    fn center_x(&self) -> f32 {
608        self.0[1].position
609    }
610    #[inline]
611    fn center_y(&self) -> f32 {
612        self.0[2].position
613    }
614
615    /// Returns the color stops of the conic gradient.
616    /// The stops are already rotated according to the `from_angle` specified in `new()`.
617    pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
618        self.0.iter().skip(Self::HEADER)
619    }
620
621    /// Sets an explicit center, returning `self` for chaining. `cx` and `cy` are in the
622    /// element's local logical coordinate space.
623    pub fn with_center(mut self, cx: f32, cy: f32) -> Self {
624        let s = self.0.make_mut_slice();
625        s[1].position = cx;
626        s[2].position = cy;
627        self
628    }
629
630    /// Returns the gradient center, falling back to the bounding box center when not explicitly set.
631    ///
632    /// `width` and `height` are the element's logical dimensions.
633    pub fn center_or_default(&self, width: f32, height: f32) -> (f32, f32) {
634        debug_assert!(
635            self.center_x().is_nan() == self.center_y().is_nan(),
636            "center_x and center_y must both be NaN or both finite"
637        );
638        center_or_bbox(self.center_x(), self.center_y(), width, height, 1.0)
639    }
640
641    /// Returns the gradient center in a scaled coordinate space.
642    ///
643    /// `width` and `height` are the dimensions in the target coordinate space. Explicit center
644    /// values are local logical lengths and are multiplied by `scale_factor`; default centers are
645    /// derived from the dimensions directly.
646    pub fn center_or_default_scaled(
647        &self,
648        width: f32,
649        height: f32,
650        scale_factor: f32,
651    ) -> (f32, f32) {
652        debug_assert!(
653            self.center_x().is_nan() == self.center_y().is_nan(),
654            "center_x and center_y must both be NaN or both finite"
655        );
656        center_or_bbox(self.center_x(), self.center_y(), width, height, scale_factor)
657    }
658
659    /// Helper: Linearly interpolate between two colors using premultiplied alpha.
660    ///
661    /// This is used for interpolating gradient boundary colors in CSS-style gradients.
662    /// We cannot use Color::mix() here because it implements Sass color mixing algorithm,
663    /// which is different from CSS gradient color interpolation.
664    ///
665    /// CSS gradients interpolate in premultiplied RGBA space:
666    /// https://www.w3.org/TR/css-color-4/#interpolation-alpha
667    fn interpolate_color(c1: &Color, c2: &Color, factor: f32) -> Color {
668        let argb1 = c1.to_argb_u8();
669        let argb2 = c2.to_argb_u8();
670
671        // Convert to premultiplied alpha
672        let a1 = argb1.alpha as f32 / 255.0;
673        let a2 = argb2.alpha as f32 / 255.0;
674        let r1 = argb1.red as f32 * a1;
675        let g1 = argb1.green as f32 * a1;
676        let b1 = argb1.blue as f32 * a1;
677        let r2 = argb2.red as f32 * a2;
678        let g2 = argb2.green as f32 * a2;
679        let b2 = argb2.blue as f32 * a2;
680
681        // Interpolate in premultiplied space
682        let alpha = (1.0 - factor) * a1 + factor * a2;
683        let red = (1.0 - factor) * r1 + factor * r2;
684        let green = (1.0 - factor) * g1 + factor * g2;
685        let blue = (1.0 - factor) * b1 + factor * b2;
686
687        // Convert back from premultiplied alpha
688        if alpha > 0.0 {
689            Color::from_argb_u8(
690                (alpha * 255.0) as u8,
691                (red / alpha).min(255.0) as u8,
692                (green / alpha).min(255.0) as u8,
693                (blue / alpha).min(255.0) as u8,
694            )
695        } else {
696            Color::from_argb_u8(0, 0, 0, 0)
697        }
698    }
699}
700
701/// C FFI function to normalize the gradient stops to be within [0, 1] range
702#[cfg(feature = "ffi")]
703#[unsafe(no_mangle)]
704pub extern "C" fn slint_conic_gradient_normalize_stops(gradient: &mut ConicGradientBrush) {
705    gradient.normalize_stops();
706}
707
708/// C FFI function to apply rotation to a ConicGradientBrush
709#[cfg(feature = "ffi")]
710#[unsafe(no_mangle)]
711pub extern "C" fn slint_conic_gradient_apply_rotation(
712    gradient: &mut ConicGradientBrush,
713    angle_degrees: f32,
714) {
715    gradient.apply_rotation(angle_degrees);
716}
717
718/// Compare two brushes using Rust's render-equivalent equality.
719#[cfg(feature = "ffi")]
720#[unsafe(no_mangle)]
721pub extern "C" fn slint_brush_compare_equal(brush1: &Brush, brush2: &Brush) -> bool {
722    brush1.eq(brush2)
723}
724
725/// GradientStop describes a single color stop in a gradient. The colors between multiple
726/// stops are interpolated.
727#[repr(C)]
728#[derive(Copy, Clone, Debug, PartialEq)]
729pub struct GradientStop {
730    /// The color to draw at this stop.
731    pub color: Color,
732    /// The position of this stop on the entire shape, as a normalized value between 0 and 1.
733    pub position: f32,
734}
735
736/// Returns the start / end points of a gradient within a rectangle of the given size, based on the angle (in degree).
737pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
738    let angle = (angle + 90.).to_radians();
739    let (s, c) = angle.sin_cos();
740
741    let (a, b) = if s.abs() < f32::EPSILON {
742        let y = size.height / 2.;
743        return if c < 0. {
744            (Point2D::new(0., y), Point2D::new(size.width, y))
745        } else {
746            (Point2D::new(size.width, y), Point2D::new(0., y))
747        };
748    } else if c * s < 0. {
749        // Intersection between the gradient line, and an orthogonal line that goes through (height, 0)
750        let x = (s * size.width + c * size.height) * s / 2.;
751        let y = -c * x / s + size.height;
752        (Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
753    } else {
754        // Intersection between the gradient line, and an orthogonal line that goes through (0, 0)
755        let x = (s * size.width - c * size.height) * s / 2.;
756        let y = -c * x / s;
757        (Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
758    };
759
760    if s > 0. { (a, b) } else { (b, a) }
761}
762
763impl InterpolatedPropertyValue for Brush {
764    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
765        match (self, target_value) {
766            (Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
767                Brush::SolidColor(source_col.interpolate(target_col, t))
768            }
769            (Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
770                let mut new_grad = grad.clone();
771                for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
772                    x.color = col.interpolate(&x.color, t);
773                }
774                Brush::LinearGradient(new_grad)
775            }
776            (a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
777                Self::interpolate(b, a, 1. - t)
778            }
779            (Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
780                if lhs.0.len() < rhs.0.len() {
781                    Self::interpolate(target_value, self, 1. - t)
782                } else {
783                    let mut new_grad = lhs.clone();
784                    let mut iter = new_grad.0.make_mut_slice().iter_mut();
785                    {
786                        let angle = &mut iter.next().unwrap().position;
787                        *angle = angle.interpolate(&rhs.angle(), t);
788                    }
789                    for s2 in rhs.stops() {
790                        let s1 = iter.next().unwrap();
791                        s1.color = s1.color.interpolate(&s2.color, t);
792                        s1.position = s1.position.interpolate(&s2.position, t);
793                    }
794                    for x in iter {
795                        x.position = x.position.interpolate(&1.0, t);
796                    }
797                    Brush::LinearGradient(new_grad)
798                }
799            }
800            (Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
801                let mut new_grad = grad.clone();
802                for x in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
803                    x.color = col.interpolate(&x.color, t);
804                }
805                Brush::RadialGradient(new_grad)
806            }
807            (a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
808                Self::interpolate(b, a, 1. - t)
809            }
810            (Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
811                if lhs.0.len() < rhs.0.len() {
812                    Self::interpolate(target_value, self, 1. - t)
813                } else {
814                    let mut new_grad = lhs.clone();
815                    {
816                        let s = new_grad.0.make_mut_slice();
817                        // Center: interpolate when both sides are explicit. When one side is the
818                        // default (NaN), lhs wins for t < 1 and snaps to rhs at t == 1.
819                        if !lhs.center_x().is_nan() && !rhs.center_x().is_nan() {
820                            s[0].position = lhs.center_x().interpolate(&rhs.center_x(), t);
821                            s[1].position = lhs.center_y().interpolate(&rhs.center_y(), t);
822                        } else if t >= 1.0 {
823                            s[0].position = rhs.center_x();
824                            s[1].position = rhs.center_y();
825                        }
826                        // Radius: same snap behavior when one side is the default (negative).
827                        if lhs.radius() >= 0.0 && rhs.radius() >= 0.0 {
828                            s[2].position = lhs.radius().interpolate(&rhs.radius(), t);
829                        } else if t >= 1.0 {
830                            s[2].position = rhs.radius();
831                        }
832                        let mut rhs_stops = rhs.stops();
833                        let mut iter = s.iter_mut().skip(RadialGradientBrush::HEADER);
834                        let mut last_color = Color::default();
835                        for s2 in &mut rhs_stops {
836                            let s1 = iter.next().unwrap();
837                            last_color = s2.color;
838                            s1.color = s1.color.interpolate(&s2.color, t);
839                            s1.position = s1.position.interpolate(&s2.position, t);
840                        }
841                        for x in iter {
842                            x.position = x.position.interpolate(&1.0, t);
843                            x.color = x.color.interpolate(&last_color, t);
844                        }
845                    }
846                    Brush::RadialGradient(new_grad)
847                }
848            }
849            (Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
850                let mut new_grad = grad.clone();
851                for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
852                    x.color = col.interpolate(&x.color, t);
853                }
854                Brush::ConicGradient(new_grad)
855            }
856            (a @ Brush::ConicGradient(_), b @ Brush::SolidColor(_)) => {
857                Self::interpolate(b, a, 1. - t)
858            }
859            (Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
860                if lhs.0.len() < rhs.0.len() {
861                    Self::interpolate(target_value, self, 1. - t)
862                } else {
863                    let mut new_grad = lhs.clone();
864                    {
865                        let s = new_grad.0.make_mut_slice();
866                        // angle (s[0])
867                        s[0].position = lhs.angle().interpolate(&rhs.angle(), t);
868                        // Center: interpolate when both sides are explicit. When one side is the
869                        // default (NaN), lhs wins for t < 1 and snaps to rhs at t == 1.
870                        if !lhs.center_x().is_nan() && !rhs.center_x().is_nan() {
871                            s[1].position = lhs.center_x().interpolate(&rhs.center_x(), t);
872                            s[2].position = lhs.center_y().interpolate(&rhs.center_y(), t);
873                        } else if t >= 1.0 {
874                            s[1].position = rhs.center_x();
875                            s[2].position = rhs.center_y();
876                        }
877                        let mut rhs_stops = rhs.stops();
878                        let mut iter = s.iter_mut().skip(ConicGradientBrush::HEADER);
879                        for s2 in &mut rhs_stops {
880                            let s1 = iter.next().unwrap();
881                            s1.color = s1.color.interpolate(&s2.color, t);
882                            s1.position = s1.position.interpolate(&s2.position, t);
883                        }
884                        for x in iter {
885                            x.position = x.position.interpolate(&1.0, t);
886                        }
887                    }
888                    Brush::ConicGradient(new_grad)
889                }
890            }
891            (a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
892            | (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_))
893            | (a @ Brush::LinearGradient(_), b @ Brush::ConicGradient(_))
894            | (a @ Brush::ConicGradient(_), b @ Brush::LinearGradient(_))
895            | (a @ Brush::RadialGradient(_), b @ Brush::ConicGradient(_))
896            | (a @ Brush::ConicGradient(_), b @ Brush::RadialGradient(_)) => {
897                // Just go to an intermediate color.
898                let color = Color::interpolate(&b.color(), &a.color(), t);
899                if t < 0.5 {
900                    Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
901                } else {
902                    Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
903                }
904            }
905        }
906    }
907}
908
909#[test]
910#[allow(clippy::float_cmp)] // We want bit-wise equality here
911fn test_linear_gradient_encoding() {
912    let stops: SharedVector<GradientStop> = [
913        GradientStop { position: 0.0, color: Color::from_argb_u8(255, 255, 0, 0) },
914        GradientStop { position: 0.5, color: Color::from_argb_u8(255, 0, 255, 0) },
915        GradientStop { position: 1.0, color: Color::from_argb_u8(255, 0, 0, 255) },
916    ]
917    .into();
918    let grad = LinearGradientBrush::new(256., stops.clone());
919    assert_eq!(grad.angle(), 256.);
920    assert!(grad.stops().eq(stops.iter()));
921}
922
923#[test]
924fn test_conic_gradient_basic() {
925    // Test basic conic gradient with no rotation
926    let grad = ConicGradientBrush::new(
927        0.0,
928        [
929            GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
930            GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
931            GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
932        ],
933    );
934    assert_eq!(grad.angle(), 0.0);
935    assert_eq!(grad.stops().count(), 3);
936}
937
938#[test]
939fn test_conic_gradient_with_rotation() {
940    // Test conic gradient with 90 degree rotation
941    let grad = ConicGradientBrush::new(
942        90.0,
943        [
944            GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
945            GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
946        ],
947    );
948    assert_eq!(grad.angle(), 90.0);
949    // After rotation, stops should still be present and sorted
950    assert!(grad.stops().count() >= 2);
951}
952
953#[test]
954fn test_conic_gradient_negative_angle() {
955    // Test with negative angle - should be normalized
956    let grad = ConicGradientBrush::new(
957        -90.0,
958        [GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) }],
959    );
960    assert_eq!(grad.angle(), -90.0); // Angle is stored as-is
961    assert!(grad.stops().count() >= 2); // Should have boundary stops added
962}
963
964#[test]
965fn test_conic_gradient_stops_outside_range() {
966    // Test with stops outside [0, 1] range
967    let grad = ConicGradientBrush::new(
968        0.0,
969        [
970            GradientStop { position: -0.2, color: Color::from_rgb_u8(255, 0, 0) },
971            GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
972            GradientStop { position: 1.2, color: Color::from_rgb_u8(0, 0, 255) },
973        ],
974    );
975    // All stops should be within [0, 1] after processing
976    for stop in grad.stops() {
977        assert!(stop.position >= 0.0 && stop.position <= 1.0);
978    }
979}
980
981#[test]
982fn test_conic_gradient_all_stops_below_zero() {
983    // Test edge case: all stops are below 0
984    let grad = ConicGradientBrush::new(
985        0.0,
986        [
987            GradientStop { position: -0.5, color: Color::from_rgb_u8(255, 0, 0) },
988            GradientStop { position: -0.3, color: Color::from_rgb_u8(0, 255, 0) },
989        ],
990    );
991    // Should create valid boundary stops
992    assert!(grad.stops().count() >= 2);
993    // First stop should be at or near 0.0
994    let first = grad.stops().next().unwrap();
995    assert!(first.position >= 0.0 && first.position < 0.1);
996}
997
998#[test]
999fn test_conic_gradient_all_stops_above_one() {
1000    // Test edge case: all stops are above 1
1001    let grad = ConicGradientBrush::new(
1002        0.0,
1003        [
1004            GradientStop { position: 1.2, color: Color::from_rgb_u8(255, 0, 0) },
1005            GradientStop { position: 1.5, color: Color::from_rgb_u8(0, 255, 0) },
1006        ],
1007    );
1008    // Should create valid boundary stops
1009    assert!(grad.stops().count() >= 2);
1010    // Last stop should be at or near 1.0
1011    let last = grad.stops().last().unwrap();
1012    assert!(last.position > 0.9 && last.position <= 1.0);
1013}
1014
1015#[test]
1016fn test_conic_gradient_empty() {
1017    // Test edge case: no stops provided
1018    let grad = ConicGradientBrush::new(0.0, []);
1019    // Should create default transparent stops
1020    assert_eq!(grad.stops().count(), 2);
1021}
1022
1023#[test]
1024fn test_radial_gradient_preserves_center_on_brighter() {
1025    let grad = RadialGradientBrush::new_circle([
1026        GradientStop { position: 0.0, color: Color::from_rgb_u8(200, 100, 50) },
1027        GradientStop { position: 1.0, color: Color::from_rgb_u8(50, 200, 100) },
1028    ])
1029    .with_center(10.0, 20.0)
1030    .with_radius(30.0);
1031    let brighter = Brush::RadialGradient(grad.clone()).brighter(0.5);
1032    if let Brush::RadialGradient(b) = brighter {
1033        assert_eq!(b.center_x(), 10.0);
1034        assert_eq!(b.center_y(), 20.0);
1035        assert_eq!(b.radius(), 30.0);
1036    } else {
1037        panic!("Expected RadialGradient");
1038    }
1039}
1040
1041#[test]
1042fn test_radial_gradient_default_center() {
1043    let grad = RadialGradientBrush::new_circle([]);
1044    assert!(grad.center_x().is_nan());
1045    assert!(grad.center_y().is_nan());
1046    assert!(grad.radius() < 0.0);
1047    assert_eq!(grad.center_or_default(100.0, 80.0), (50.0, 40.0));
1048    assert!((grad.radius_or_default(60.0, 80.0) - 50.0).abs() < 0.01);
1049}
1050
1051#[test]
1052fn test_radial_gradient_scaled_explicit_values() {
1053    let grad = RadialGradientBrush::new_circle([]).with_center(10.0, 20.0).with_radius(30.0);
1054
1055    assert_eq!(grad.center_or_default_scaled(200.0, 160.0, 2.0), (20.0, 40.0));
1056    assert_eq!(grad.radius_or_default_scaled(200.0, 160.0, 2.0), 60.0);
1057}
1058
1059#[test]
1060fn test_radial_gradient_scaled_defaults_use_physical_frame() {
1061    let grad = RadialGradientBrush::new_circle([]);
1062
1063    assert_eq!(grad.center_or_default_scaled(200.0, 160.0, 2.0), (100.0, 80.0));
1064    assert!((grad.radius_or_default_scaled(120.0, 160.0, 2.0) - 100.0).abs() < 0.01);
1065}
1066
1067#[test]
1068fn test_radial_gradient_interpolation_reaches_explicit_metadata() {
1069    let source = Brush::RadialGradient(RadialGradientBrush::new_circle([
1070        GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1071        GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1072    ]));
1073    let target_grad = RadialGradientBrush::new_circle([
1074        GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1075        GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1076    ])
1077    .with_center(10.0, 20.0)
1078    .with_radius(30.0);
1079    let target = Brush::RadialGradient(target_grad.clone());
1080
1081    if let Brush::RadialGradient(result) = source.interpolate(&target, 1.0) {
1082        assert_eq!(result.center_x(), target_grad.center_x());
1083        assert_eq!(result.center_y(), target_grad.center_y());
1084        assert_eq!(result.radius(), target_grad.radius());
1085    } else {
1086        panic!("Expected RadialGradient");
1087    }
1088}
1089
1090#[test]
1091fn test_radial_gradient_interpolation_reaches_default_metadata() {
1092    let source_grad = RadialGradientBrush::new_circle([
1093        GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1094        GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1095    ])
1096    .with_center(10.0, 20.0)
1097    .with_radius(30.0);
1098    let source = Brush::RadialGradient(source_grad);
1099    let target = Brush::RadialGradient(RadialGradientBrush::new_circle([
1100        GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1101        GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1102    ]));
1103
1104    if let Brush::RadialGradient(result) = source.interpolate(&target, 1.0) {
1105        assert!(result.center_x().is_nan());
1106        assert!(result.center_y().is_nan());
1107        assert!(result.radius() < 0.0);
1108    } else {
1109        panic!("Expected RadialGradient");
1110    }
1111}
1112
1113#[test]
1114fn test_conic_gradient_interpolation_reaches_explicit_center() {
1115    let source = Brush::ConicGradient(ConicGradientBrush::new(
1116        0.0,
1117        [
1118            GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1119            GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1120        ],
1121    ));
1122    let target_grad = ConicGradientBrush::new(
1123        0.0,
1124        [
1125            GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1126            GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1127        ],
1128    )
1129    .with_center(40.0, 50.0);
1130    let target = Brush::ConicGradient(target_grad.clone());
1131
1132    if let Brush::ConicGradient(result) = source.interpolate(&target, 1.0) {
1133        assert_eq!(result.center_x(), target_grad.center_x());
1134        assert_eq!(result.center_y(), target_grad.center_y());
1135    } else {
1136        panic!("Expected ConicGradient");
1137    }
1138}