1use super::Color;
9use crate::SharedVector;
10use crate::lengths::{PhysicalPx, ScaleFactor};
11use crate::properties::InterpolatedPropertyValue;
12use alloc::borrow::Cow;
13use euclid::default::{Point2D, Size2D};
14
15#[cfg(not(feature = "std"))]
16use num_traits::float::Float;
17
18#[derive(Clone, PartialEq, Debug, derive_more::From)]
23#[repr(C)]
24#[non_exhaustive]
25pub enum Brush {
26 SolidColor(Color),
28 LinearGradient(LinearGradientBrush),
31 RadialGradient(RadialGradientBrush),
34 ConicGradient(ConicGradientBrush),
37}
38
39impl Default for Brush {
41 fn default() -> Self {
42 Self::SolidColor(Color::default())
43 }
44}
45
46impl Brush {
47 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 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 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 #[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 let mut new_grad = g.clone();
114 for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
115 s.color = s.color.brighter(factor);
116 }
117 Brush::RadialGradient(new_grad)
118 }
119 Brush::ConicGradient(g) => {
120 let mut new_grad = g.clone();
121 for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
122 x.color = x.color.brighter(factor);
123 }
124 Brush::ConicGradient(new_grad)
125 }
126 }
127 }
128
129 #[must_use]
133 pub fn darker(&self, factor: f32) -> Self {
134 match self {
135 Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
136 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
137 g.angle(),
138 g.stops()
139 .map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
140 )),
141 Brush::RadialGradient(g) => {
142 let mut new_grad = g.clone();
143 for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
144 s.color = s.color.darker(factor);
145 }
146 Brush::RadialGradient(new_grad)
147 }
148 Brush::ConicGradient(g) => {
149 let mut new_grad = g.clone();
150 for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
151 x.color = x.color.darker(factor);
152 }
153 Brush::ConicGradient(new_grad)
154 }
155 }
156 }
157
158 #[must_use]
164 pub fn transparentize(&self, amount: f32) -> Self {
165 match self {
166 Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
167 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
168 g.angle(),
169 g.stops().map(|s| GradientStop {
170 color: s.color.transparentize(amount),
171 position: s.position,
172 }),
173 )),
174 Brush::RadialGradient(g) => {
175 let mut new_grad = g.clone();
176 for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
177 s.color = s.color.transparentize(amount);
178 }
179 Brush::RadialGradient(new_grad)
180 }
181 Brush::ConicGradient(g) => {
182 let mut new_grad = g.clone();
183 for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
184 x.color = x.color.transparentize(amount);
185 }
186 Brush::ConicGradient(new_grad)
187 }
188 }
189 }
190
191 #[must_use]
194 pub fn with_alpha(&self, alpha: f32) -> Self {
195 match self {
196 Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
197 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
198 g.angle(),
199 g.stops().map(|s| GradientStop {
200 color: s.color.with_alpha(alpha),
201 position: s.position,
202 }),
203 )),
204 Brush::RadialGradient(g) => {
205 let mut new_grad = g.clone();
206 for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
207 s.color = s.color.with_alpha(alpha);
208 }
209 Brush::RadialGradient(new_grad)
210 }
211 Brush::ConicGradient(g) => {
212 let mut new_grad = g.clone();
213 for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
214 x.color = x.color.with_alpha(alpha);
215 }
216 Brush::ConicGradient(new_grad)
217 }
218 }
219 }
220}
221
222#[derive(Clone, PartialEq, Debug)]
226#[repr(transparent)]
227pub struct LinearGradientBrush(SharedVector<GradientStop>);
228
229impl LinearGradientBrush {
230 pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
235 let stop_iter = stops.into_iter();
236 let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
237 encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
239 encoded_angle_and_stops.extend(stop_iter);
240 Self(encoded_angle_and_stops)
241 }
242 pub fn angle(&self) -> f32 {
244 self.0[0].position
245 }
246 pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
249 self.0.iter().skip(1)
251 }
252
253 fn stops_slice(&self) -> &[GradientStop] {
255 self.0.as_slice().get(1..).unwrap_or_default()
256 }
257}
258
259#[inline]
261fn nan_eq(a: f32, b: f32) -> bool {
262 a == b || (a.is_nan() && b.is_nan())
263}
264
265#[inline]
268fn center_or_bbox(cx: f32, cy: f32, width: f32, height: f32, scale_factor: f32) -> (f32, f32) {
269 if cx.is_nan() { (width / 2.0, height / 2.0) } else { (cx * scale_factor, cy * scale_factor) }
270}
271
272#[derive(Clone, Debug)]
282#[repr(transparent)]
283pub struct RadialGradientBrush(SharedVector<GradientStop>);
284
285impl RadialGradientBrush {
286 const HEADER: usize = 3;
287
288 pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
291 let stop_iter = stops.into_iter();
292 let mut v = SharedVector::with_capacity(Self::HEADER + stop_iter.size_hint().0);
293 v.push(GradientStop { color: Default::default(), position: f32::NAN });
295 v.push(GradientStop { color: Default::default(), position: f32::NAN });
296 v.push(GradientStop { color: Default::default(), position: -1.0 });
297 v.extend(stop_iter);
298 Self(v)
299 }
300
301 #[inline]
302 fn center_x(&self) -> f32 {
303 self.0[0].position
304 }
305 #[inline]
306 fn center_y(&self) -> f32 {
307 self.0[1].position
308 }
309 #[inline]
310 fn radius(&self) -> f32 {
311 self.0[2].position
312 }
313
314 pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
316 self.0.iter().skip(Self::HEADER)
317 }
318
319 fn stops_slice(&self) -> &[GradientStop] {
321 self.0.as_slice().get(Self::HEADER..).unwrap_or_default()
322 }
323
324 pub fn with_center(mut self, cx: f32, cy: f32) -> Self {
327 let s = self.0.make_mut_slice();
328 s[0].position = cx;
329 s[1].position = cy;
330 self
331 }
332
333 pub fn with_radius(mut self, r: f32) -> Self {
336 self.0.make_mut_slice()[2].position = r;
337 self
338 }
339
340 pub fn center_or_default(&self, width: f32, height: f32) -> (f32, f32) {
344 debug_assert!(
345 self.center_x().is_nan() == self.center_y().is_nan(),
346 "center_x and center_y must both be NaN or both finite"
347 );
348 center_or_bbox(self.center_x(), self.center_y(), width, height, 1.0)
349 }
350
351 pub fn center_or_default_scaled(
357 &self,
358 width: f32,
359 height: f32,
360 scale_factor: f32,
361 ) -> (f32, f32) {
362 debug_assert!(
363 self.center_x().is_nan() == self.center_y().is_nan(),
364 "center_x and center_y must both be NaN or both finite"
365 );
366 center_or_bbox(self.center_x(), self.center_y(), width, height, scale_factor)
367 }
368
369 pub fn radius_or_default(&self, width: f32, height: f32) -> f32 {
374 let r = self.radius();
375 if r < 0.0 { 0.5 * (width * width + height * height).sqrt() } else { r }
376 }
377
378 pub fn radius_or_default_scaled(&self, width: f32, height: f32, scale_factor: f32) -> f32 {
384 let r = self.radius();
385 if r < 0.0 { 0.5 * (width * width + height * height).sqrt() } else { r * scale_factor }
386 }
387}
388
389impl PartialEq for RadialGradientBrush {
392 fn eq(&self, other: &Self) -> bool {
393 if self.0.len() != other.0.len() {
394 return false;
395 }
396 nan_eq(self.center_x(), other.center_x())
397 && nan_eq(self.center_y(), other.center_y())
398 && (self.radius() == other.radius() || (self.radius() < 0.0 && other.radius() < 0.0))
399 && self.0.iter().skip(Self::HEADER).eq(other.0.iter().skip(Self::HEADER))
400 }
401}
402
403#[derive(Clone, Debug)]
412#[repr(transparent)]
413pub struct ConicGradientBrush(SharedVector<GradientStop>);
414
415impl PartialEq for ConicGradientBrush {
418 fn eq(&self, other: &Self) -> bool {
419 if self.0.len() != other.0.len() {
420 return false;
421 }
422 self.0[0].position == other.0[0].position
424 && nan_eq(self.center_x(), other.center_x())
425 && nan_eq(self.center_y(), other.center_y())
426 && self.0.iter().skip(Self::HEADER).eq(other.0.iter().skip(Self::HEADER))
427 }
428}
429
430impl ConicGradientBrush {
431 const HEADER: usize = 3;
432
433 pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
438 let stop_iter = stops.into_iter();
439 let mut v = SharedVector::with_capacity(Self::HEADER + stop_iter.size_hint().0);
440 v.push(GradientStop { color: Default::default(), position: angle });
442 v.push(GradientStop { color: Default::default(), position: f32::NAN });
443 v.push(GradientStop { color: Default::default(), position: f32::NAN });
444 v.extend(stop_iter);
445 let mut result = Self(v);
446 result.normalize_stops();
447 if angle.abs() > f32::EPSILON {
448 result.apply_rotation(angle);
449 }
450 result
451 }
452
453 fn normalize_stops(&mut self) {
455 let stops_slice = &self.0[Self::HEADER..];
457 let has_stop_at_0 = stops_slice.iter().any(|s| s.position.abs() < f32::EPSILON);
458 let has_stop_at_1 = stops_slice.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
459 let has_stops_outside = stops_slice.iter().any(|s| s.position < 0.0 || s.position > 1.0);
460 let is_empty = stops_slice.is_empty();
461
462 if has_stop_at_0 && has_stop_at_1 && !has_stops_outside && !is_empty {
464 return;
465 }
466
467 let mut stops: alloc::vec::Vec<_> = stops_slice.to_vec();
469
470 if !has_stop_at_0 {
472 let stop_below_0 = stops.iter().filter(|s| s.position < 0.0).max_by(|a, b| {
473 a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
474 });
475 let stop_above_0 = stops.iter().filter(|s| s.position > 0.0).min_by(|a, b| {
476 a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
477 });
478 if let (Some(below), Some(above)) = (stop_below_0, stop_above_0) {
479 let t = (0.0 - below.position) / (above.position - below.position);
480 let color_at_0 = Self::interpolate_color(&below.color, &above.color, t);
481 stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
482 } else if let Some(above) = stop_above_0 {
483 stops.insert(0, GradientStop { position: 0.0, color: above.color });
484 } else if let Some(below) = stop_below_0 {
485 stops.insert(0, GradientStop { position: 0.0, color: below.color });
486 }
487 }
488
489 if !has_stop_at_1 {
491 let stop_below_1 = stops.iter().filter(|s| s.position < 1.0).max_by(|a, b| {
492 a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
493 });
494 let stop_above_1 = stops.iter().filter(|s| s.position > 1.0).min_by(|a, b| {
495 a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
496 });
497
498 if let (Some(below), Some(above)) = (stop_below_1, stop_above_1) {
499 let t = (1.0 - below.position) / (above.position - below.position);
500 let color_at_1 = Self::interpolate_color(&below.color, &above.color, t);
501 stops.push(GradientStop { position: 1.0, color: color_at_1 });
502 } else if let Some(below) = stop_below_1 {
503 stops.push(GradientStop { position: 1.0, color: below.color });
504 } else if let Some(above) = stop_above_1 {
505 stops.push(GradientStop { position: 1.0, color: above.color });
506 }
507 }
508
509 if has_stops_outside {
511 stops.retain(|s| 0.0 <= s.position && s.position <= 1.0);
512 }
513
514 if stops.is_empty() {
516 stops.push(GradientStop { position: 0.0, color: Color::default() });
517 stops.push(GradientStop { position: 1.0, color: Color::default() });
518 }
519
520 let angle = self.angle();
522 let cx = self.center_x();
523 let cy = self.center_y();
524 self.0 = SharedVector::with_capacity(stops.len() + Self::HEADER);
525 self.0.push(GradientStop { color: Default::default(), position: angle });
526 self.0.push(GradientStop { color: Default::default(), position: cx });
527 self.0.push(GradientStop { color: Default::default(), position: cy });
528 self.0.extend(stops);
529 }
530
531 fn apply_rotation(&mut self, from_angle: f32) {
535 let normalized_from_angle = (from_angle / 360.0) - (from_angle / 360.0).floor();
537
538 if normalized_from_angle.abs() < f32::EPSILON {
540 self.0.make_mut_slice()[0].position = from_angle;
541 return;
542 }
543
544 self.0.make_mut_slice()[0].position = from_angle;
546
547 let mut stops: alloc::vec::Vec<_> = self.0.iter().skip(Self::HEADER).copied().collect();
549
550 if let Some(first) = stops.first_mut()
552 && first.position.abs() < f32::EPSILON
553 {
554 first.position = f32::EPSILON;
555 }
556
557 stops = stops
559 .iter()
560 .map(|stop| {
561 let rotated_position =
564 num_traits::Euclid::rem_euclid(&(stop.position + normalized_from_angle), &1.0);
565 GradientStop { position: rotated_position, color: stop.color }
566 })
567 .collect();
568
569 for i in 0..stops.len() {
571 let j = (i + 1) % stops.len();
572 if (stops[i].position - stops[j].position).abs() < f32::EPSILON
573 && stops[i].color != stops[j].color
574 {
575 stops[i].position = (stops[i].position - f32::EPSILON).max(0.0);
576 stops[j].position = (stops[j].position + f32::EPSILON).min(1.0);
577 }
578 }
579
580 stops.sort_by(|a, b| {
582 a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
583 });
584
585 let has_stop_at_0 = stops.iter().any(|s| s.position.abs() < f32::EPSILON);
587 if !has_stop_at_0 && let (Some(last), Some(first)) = (stops.last(), stops.first()) {
588 let gap = 1.0 - last.position + first.position;
589 let color_at_0 = if gap > f32::EPSILON {
590 let t = (1.0 - last.position) / gap;
591 Self::interpolate_color(&last.color, &first.color, t)
592 } else {
593 last.color
594 };
595 stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
596 }
597
598 let has_stop_at_1 = stops.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
599 if !has_stop_at_1 && let Some(first) = stops.first() {
600 stops.push(GradientStop { position: 1.0, color: first.color });
601 }
602
603 let cx = self.center_x();
605 let cy = self.center_y();
606 self.0 = SharedVector::with_capacity(stops.len() + Self::HEADER);
607 self.0.push(GradientStop { color: Default::default(), position: from_angle });
608 self.0.push(GradientStop { color: Default::default(), position: cx });
609 self.0.push(GradientStop { color: Default::default(), position: cy });
610 self.0.extend(stops);
611 }
612
613 fn angle(&self) -> f32 {
615 self.0[0].position
616 }
617
618 #[inline]
619 fn center_x(&self) -> f32 {
620 self.0[1].position
621 }
622 #[inline]
623 fn center_y(&self) -> f32 {
624 self.0[2].position
625 }
626
627 pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
630 self.0.iter().skip(Self::HEADER)
631 }
632
633 fn stops_slice(&self) -> &[GradientStop] {
635 self.0.as_slice().get(Self::HEADER..).unwrap_or_default()
636 }
637
638 pub fn with_center(mut self, cx: f32, cy: f32) -> Self {
641 let s = self.0.make_mut_slice();
642 s[1].position = cx;
643 s[2].position = cy;
644 self
645 }
646
647 pub fn center_or_default(&self, width: f32, height: f32) -> (f32, f32) {
651 debug_assert!(
652 self.center_x().is_nan() == self.center_y().is_nan(),
653 "center_x and center_y must both be NaN or both finite"
654 );
655 center_or_bbox(self.center_x(), self.center_y(), width, height, 1.0)
656 }
657
658 pub fn center_or_default_scaled(
664 &self,
665 width: f32,
666 height: f32,
667 scale_factor: f32,
668 ) -> (f32, f32) {
669 debug_assert!(
670 self.center_x().is_nan() == self.center_y().is_nan(),
671 "center_x and center_y must both be NaN or both finite"
672 );
673 center_or_bbox(self.center_x(), self.center_y(), width, height, scale_factor)
674 }
675
676 fn interpolate_color(c1: &Color, c2: &Color, factor: f32) -> Color {
685 let argb1 = c1.to_argb_u8();
686 let argb2 = c2.to_argb_u8();
687
688 let a1 = argb1.alpha as f32 / 255.0;
690 let a2 = argb2.alpha as f32 / 255.0;
691 let r1 = argb1.red as f32 * a1;
692 let g1 = argb1.green as f32 * a1;
693 let b1 = argb1.blue as f32 * a1;
694 let r2 = argb2.red as f32 * a2;
695 let g2 = argb2.green as f32 * a2;
696 let b2 = argb2.blue as f32 * a2;
697
698 let alpha = (1.0 - factor) * a1 + factor * a2;
700 let red = (1.0 - factor) * r1 + factor * r2;
701 let green = (1.0 - factor) * g1 + factor * g2;
702 let blue = (1.0 - factor) * b1 + factor * b2;
703
704 if alpha > 0.0 {
706 Color::from_argb_u8(
707 (alpha * 255.0) as u8,
708 (red / alpha).min(255.0) as u8,
709 (green / alpha).min(255.0) as u8,
710 (blue / alpha).min(255.0) as u8,
711 )
712 } else {
713 Color::from_argb_u8(0, 0, 0, 0)
714 }
715 }
716}
717
718#[cfg(feature = "ffi")]
720#[unsafe(no_mangle)]
721pub extern "C" fn slint_conic_gradient_normalize_stops(gradient: &mut ConicGradientBrush) {
722 gradient.normalize_stops();
723}
724
725#[cfg(feature = "ffi")]
727#[unsafe(no_mangle)]
728pub extern "C" fn slint_conic_gradient_apply_rotation(
729 gradient: &mut ConicGradientBrush,
730 angle_degrees: f32,
731) {
732 gradient.apply_rotation(angle_degrees);
733}
734
735#[cfg(feature = "ffi")]
737#[unsafe(no_mangle)]
738pub extern "C" fn slint_brush_compare_equal(brush1: &Brush, brush2: &Brush) -> bool {
739 brush1.eq(brush2)
740}
741
742#[repr(C)]
745#[derive(Copy, Clone, Debug, PartialEq)]
746pub struct GradientStop {
747 pub color: Color,
749 pub position: f32,
751}
752
753pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
755 let angle = (angle + 90.).to_radians();
756 let (s, c) = angle.sin_cos();
757
758 let (a, b) = if s.abs() < f32::EPSILON {
759 let y = size.height / 2.;
760 return if c < 0. {
761 (Point2D::new(0., y), Point2D::new(size.width, y))
762 } else {
763 (Point2D::new(size.width, y), Point2D::new(0., y))
764 };
765 } else if c * s < 0. {
766 let x = (s * size.width + c * size.height) * s / 2.;
768 let y = -c * x / s + size.height;
769 (Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
770 } else {
771 let x = (s * size.width - c * size.height) * s / 2.;
773 let y = -c * x / s;
774 (Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
775 };
776
777 if s > 0. { (a, b) } else { (b, a) }
778}
779
780impl InterpolatedPropertyValue for Brush {
781 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
782 match (self, target_value) {
783 (Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
784 Brush::SolidColor(source_col.interpolate(target_col, t))
785 }
786 (Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
787 let mut new_grad = grad.clone();
788 for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
789 x.color = col.interpolate(&x.color, t);
790 }
791 Brush::LinearGradient(new_grad)
792 }
793 (a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
794 Self::interpolate(b, a, 1. - t)
795 }
796 (Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
797 if lhs.0.len() < rhs.0.len() {
798 Self::interpolate(target_value, self, 1. - t)
799 } else {
800 let mut new_grad = lhs.clone();
801 let mut iter = new_grad.0.make_mut_slice().iter_mut();
802 {
803 let angle = &mut iter.next().unwrap().position;
804 *angle = angle.interpolate(&rhs.angle(), t);
805 }
806 for s2 in rhs.stops() {
807 let s1 = iter.next().unwrap();
808 s1.color = s1.color.interpolate(&s2.color, t);
809 s1.position = s1.position.interpolate(&s2.position, t);
810 }
811 for x in iter {
812 x.position = x.position.interpolate(&1.0, t);
813 }
814 Brush::LinearGradient(new_grad)
815 }
816 }
817 (Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
818 let mut new_grad = grad.clone();
819 for x in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
820 x.color = col.interpolate(&x.color, t);
821 }
822 Brush::RadialGradient(new_grad)
823 }
824 (a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
825 Self::interpolate(b, a, 1. - t)
826 }
827 (Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
828 if lhs.0.len() < rhs.0.len() {
829 Self::interpolate(target_value, self, 1. - t)
830 } else {
831 let mut new_grad = lhs.clone();
832 {
833 let s = new_grad.0.make_mut_slice();
834 if !lhs.center_x().is_nan() && !rhs.center_x().is_nan() {
837 s[0].position = lhs.center_x().interpolate(&rhs.center_x(), t);
838 s[1].position = lhs.center_y().interpolate(&rhs.center_y(), t);
839 } else if t >= 1.0 {
840 s[0].position = rhs.center_x();
841 s[1].position = rhs.center_y();
842 }
843 if lhs.radius() >= 0.0 && rhs.radius() >= 0.0 {
845 s[2].position = lhs.radius().interpolate(&rhs.radius(), t);
846 } else if t >= 1.0 {
847 s[2].position = rhs.radius();
848 }
849 let mut rhs_stops = rhs.stops();
850 let mut iter = s.iter_mut().skip(RadialGradientBrush::HEADER);
851 let mut last_color = Color::default();
852 for s2 in &mut rhs_stops {
853 let s1 = iter.next().unwrap();
854 last_color = s2.color;
855 s1.color = s1.color.interpolate(&s2.color, t);
856 s1.position = s1.position.interpolate(&s2.position, t);
857 }
858 for x in iter {
859 x.position = x.position.interpolate(&1.0, t);
860 x.color = x.color.interpolate(&last_color, t);
861 }
862 }
863 Brush::RadialGradient(new_grad)
864 }
865 }
866 (Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
867 let mut new_grad = grad.clone();
868 for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
869 x.color = col.interpolate(&x.color, t);
870 }
871 Brush::ConicGradient(new_grad)
872 }
873 (a @ Brush::ConicGradient(_), b @ Brush::SolidColor(_)) => {
874 Self::interpolate(b, a, 1. - t)
875 }
876 (Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
877 if lhs.0.len() < rhs.0.len() {
878 Self::interpolate(target_value, self, 1. - t)
879 } else {
880 let mut new_grad = lhs.clone();
881 {
882 let s = new_grad.0.make_mut_slice();
883 s[0].position = lhs.angle().interpolate(&rhs.angle(), t);
885 if !lhs.center_x().is_nan() && !rhs.center_x().is_nan() {
888 s[1].position = lhs.center_x().interpolate(&rhs.center_x(), t);
889 s[2].position = lhs.center_y().interpolate(&rhs.center_y(), t);
890 } else if t >= 1.0 {
891 s[1].position = rhs.center_x();
892 s[2].position = rhs.center_y();
893 }
894 let mut rhs_stops = rhs.stops();
895 let mut iter = s.iter_mut().skip(ConicGradientBrush::HEADER);
896 for s2 in &mut rhs_stops {
897 let s1 = iter.next().unwrap();
898 s1.color = s1.color.interpolate(&s2.color, t);
899 s1.position = s1.position.interpolate(&s2.position, t);
900 }
901 for x in iter {
902 x.position = x.position.interpolate(&1.0, t);
903 }
904 }
905 Brush::ConicGradient(new_grad)
906 }
907 }
908 (a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
909 | (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_))
910 | (a @ Brush::LinearGradient(_), b @ Brush::ConicGradient(_))
911 | (a @ Brush::ConicGradient(_), b @ Brush::LinearGradient(_))
912 | (a @ Brush::RadialGradient(_), b @ Brush::ConicGradient(_))
913 | (a @ Brush::ConicGradient(_), b @ Brush::RadialGradient(_)) => {
914 let color = Color::interpolate(&b.color(), &a.color(), t);
916 if t < 0.5 {
917 Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
918 } else {
919 Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
920 }
921 }
922 }
923 }
924}
925
926#[derive(Clone, Debug, PartialEq)]
930pub enum ResolvedBrush<'a> {
931 SolidColor(Color),
933 LinearGradient(ResolvedLinearGradient<'a>),
935 RadialGradient(ResolvedRadialGradient<'a>),
937 ConicGradient(ResolvedConicGradient<'a>),
939}
940
941#[derive(Clone, Debug, PartialEq)]
943pub struct ResolvedLinearGradient<'a> {
944 pub start: euclid::Point2D<f32, PhysicalPx>,
946 pub end: euclid::Point2D<f32, PhysicalPx>,
948 pub stops: Cow<'a, [GradientStop]>,
950}
951
952#[derive(Clone, Debug, PartialEq)]
954pub struct ResolvedRadialGradient<'a> {
955 pub center: euclid::Point2D<f32, PhysicalPx>,
957 pub radius: euclid::Length<f32, PhysicalPx>,
959 pub stops: Cow<'a, [GradientStop]>,
961}
962
963#[derive(Clone, Debug, PartialEq)]
966pub struct ResolvedConicGradient<'a> {
967 pub center: euclid::Point2D<f32, PhysicalPx>,
969 pub stops: Cow<'a, [GradientStop]>,
971}
972
973pub fn resolve_brush<'a>(
988 brush: &'a Brush,
989 size: euclid::Size2D<f32, PhysicalPx>,
990 scale_factor: ScaleFactor,
991) -> Option<ResolvedBrush<'a>> {
992 if brush.is_transparent() {
993 return None;
994 }
995 Some(match brush {
996 Brush::SolidColor(color) => ResolvedBrush::SolidColor(*color),
997 Brush::LinearGradient(gradient) => {
998 let (stops, extent) = sanitize_color_stops(gradient.stops_slice(), true);
999 let (start, mut end) = line_for_angle(gradient.angle(), size.to_untyped());
1000 if extent != 1.0 {
1001 end = start + (end - start) * extent;
1004 }
1005 ResolvedBrush::LinearGradient(ResolvedLinearGradient {
1006 start: start.cast_unit(),
1007 end: end.cast_unit(),
1008 stops,
1009 })
1010 }
1011 Brush::RadialGradient(gradient) => {
1012 let (stops, extent) = sanitize_color_stops(gradient.stops_slice(), true);
1013 let (center_x, center_y) =
1014 gradient.center_or_default_scaled(size.width, size.height, scale_factor.get());
1015 let radius =
1016 gradient.radius_or_default_scaled(size.width, size.height, scale_factor.get())
1017 * extent;
1018 ResolvedBrush::RadialGradient(ResolvedRadialGradient {
1019 center: euclid::point2(center_x, center_y),
1020 radius: euclid::Length::new(radius),
1021 stops,
1022 })
1023 }
1024 Brush::ConicGradient(gradient) => {
1025 let (stops, _) = sanitize_color_stops(gradient.stops_slice(), false);
1026 let (center_x, center_y) =
1027 gradient.center_or_default_scaled(size.width, size.height, scale_factor.get());
1028 ResolvedBrush::ConicGradient(ResolvedConicGradient {
1029 center: euclid::point2(center_x, center_y),
1030 stops,
1031 })
1032 }
1033 })
1034}
1035
1036fn sanitize_color_stops(
1044 stops: &[GradientStop],
1045 can_extend: bool,
1046) -> (Cow<'_, [GradientStop]>, f32) {
1047 fn color_at(position: f32, a: &GradientStop, b: &GradientStop) -> Color {
1049 let t = if b.position > a.position {
1050 ((position - a.position) / (b.position - a.position)).clamp(0., 1.)
1051 } else {
1052 0.
1053 };
1054 let (ca, cb) = (a.color.to_argb_u8(), b.color.to_argb_u8());
1055 let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
1056 Color::from_argb_u8(
1057 lerp(ca.alpha, cb.alpha),
1058 lerp(ca.red, cb.red),
1059 lerp(ca.green, cb.green),
1060 lerp(ca.blue, cb.blue),
1061 )
1062 }
1063
1064 if stops.first().is_none_or(|first| first.position >= 0.)
1067 && stops.last().is_none_or(|last| last.position <= 1.)
1068 && stops.windows(2).all(|pair| pair[0].position < pair[1].position)
1069 {
1070 return (Cow::Borrowed(stops), 1.0);
1071 }
1072
1073 let mut stops: alloc::vec::Vec<GradientStop> = stops.to_vec();
1074 stops.sort_by(|a, b| a.position.total_cmp(&b.position));
1075
1076 while stops.len() >= 2 && stops[1].position <= 0. {
1078 stops.remove(0);
1079 }
1080 if let [first, second, ..] = stops.as_slice()
1081 && first.position < 0.
1082 {
1083 stops[0] = GradientStop { color: color_at(0., first, second), position: 0. };
1084 } else if let [only] = stops.as_slice()
1085 && only.position < 0.
1086 {
1087 stops[0].position = 0.;
1088 }
1089
1090 let mut extent = 1.0f32;
1093 if stops.last().is_some_and(|last| last.position > 1.) {
1094 if can_extend {
1095 extent = stops.last().unwrap().position;
1096 for stop in &mut stops {
1097 stop.position /= extent;
1098 }
1099 } else {
1100 while stops.len() >= 2 && stops[stops.len() - 2].position >= 1. {
1101 stops.pop();
1102 }
1103 let clamped_last = match stops.as_slice() {
1104 [.., second_to_last, last] if last.position > 1. => {
1105 Some(GradientStop { color: color_at(1., second_to_last, last), position: 1. })
1106 }
1107 [only] if only.position > 1. => {
1108 Some(GradientStop { color: only.color, position: 1. })
1109 }
1110 _ => None,
1111 };
1112 if let Some(stop) = clamped_last {
1113 *stops.last_mut().unwrap() = stop;
1114 }
1115 }
1116 }
1117
1118 let mut previous = f32::NEG_INFINITY;
1121 for stop in &mut stops {
1122 if stop.position <= previous {
1123 stop.position = previous.next_up();
1124 }
1125 previous = stop.position;
1126 }
1127 let mut next = 1.0f32.next_up();
1128 for stop in stops.iter_mut().rev() {
1129 if stop.position >= next {
1130 stop.position = next.next_down();
1131 }
1132 next = stop.position;
1133 }
1134
1135 (Cow::Owned(stops), extent)
1136}
1137
1138#[test]
1139fn test_resolve_sanitizes_out_of_range_stops() {
1140 let brush = Brush::LinearGradient(LinearGradientBrush::new(
1142 180.,
1143 [
1144 GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
1145 GradientStop { position: 2.0, color: Color::from_rgb_u8(0, 0, 255) },
1146 ],
1147 ));
1148 let Some(ResolvedBrush::LinearGradient(gradient)) =
1149 resolve_brush(&brush, [100., 50.].into(), ScaleFactor::new(1.0))
1150 else {
1151 panic!("expected a resolved linear gradient");
1152 };
1153 assert_eq!(gradient.stops.last().unwrap().position, 1.0);
1154 assert_eq!(gradient.start.y, 0.);
1156 assert_eq!(gradient.end.y, 100.);
1157
1158 let brush = Brush::ConicGradient(ConicGradientBrush::new(
1162 0.,
1163 [
1164 GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
1165 GradientStop { position: 2.0, color: Color::from_rgb_u8(0, 0, 255) },
1166 ],
1167 ));
1168 let Some(ResolvedBrush::ConicGradient(gradient)) =
1169 resolve_brush(&brush, [100., 50.].into(), ScaleFactor::new(1.0))
1170 else {
1171 panic!("expected a resolved conic gradient");
1172 };
1173 assert!(gradient.stops.iter().all(|stop| (0. ..=1.).contains(&stop.position)));
1174 assert_eq!(gradient.center, euclid::point2(50., 25.));
1175}
1176
1177#[test]
1178fn test_resolve_makes_stops_strictly_increasing() {
1179 let brush = Brush::LinearGradient(LinearGradientBrush::new(
1180 0.,
1181 [
1182 GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) },
1183 GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
1184 GradientStop { position: 0.2, color: Color::from_rgb_u8(0, 0, 255) },
1185 ],
1186 ));
1187 let Some(ResolvedBrush::LinearGradient(gradient)) =
1188 resolve_brush(&brush, [100., 100.].into(), ScaleFactor::new(1.0))
1189 else {
1190 panic!("expected a resolved linear gradient");
1191 };
1192 assert!(gradient.stops.windows(2).all(|pair| pair[0].position < pair[1].position));
1194 assert_eq!(gradient.stops[0].color, Color::from_rgb_u8(0, 0, 255));
1195}
1196
1197#[test]
1198fn test_resolve_replaces_stops_below_zero() {
1199 let brush = Brush::LinearGradient(LinearGradientBrush::new(
1200 0.,
1201 [
1202 GradientStop { position: -1.0, color: Color::from_rgb_u8(0, 0, 0) },
1203 GradientStop { position: 1.0, color: Color::from_rgb_u8(200, 200, 200) },
1204 ],
1205 ));
1206 let Some(ResolvedBrush::LinearGradient(gradient)) =
1207 resolve_brush(&brush, [100., 100.].into(), ScaleFactor::new(1.0))
1208 else {
1209 panic!("expected a resolved linear gradient");
1210 };
1211 assert_eq!(gradient.stops[0].position, 0.0);
1213 assert_eq!(gradient.stops[0].color, Color::from_rgb_u8(100, 100, 100));
1214}
1215
1216#[test]
1217fn test_resolve_transparent_brush() {
1218 assert_eq!(resolve_brush(&Brush::default(), [100., 100.].into(), ScaleFactor::new(1.0)), None);
1219}
1220
1221#[test]
1222fn test_resolve_borrows_canonical_stops() {
1223 let brush = Brush::LinearGradient(LinearGradientBrush::new(
1225 90.,
1226 [
1227 GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
1228 GradientStop { position: 1.0, color: Color::from_rgb_u8(0, 0, 255) },
1229 ],
1230 ));
1231 let Some(ResolvedBrush::LinearGradient(gradient)) =
1232 resolve_brush(&brush, [100., 100.].into(), ScaleFactor::new(1.0))
1233 else {
1234 panic!("expected a resolved linear gradient");
1235 };
1236 assert!(matches!(gradient.stops, Cow::Borrowed(_)));
1237 assert_eq!(gradient.stops.len(), 2);
1238}
1239
1240#[test]
1241#[allow(clippy::float_cmp)] fn test_linear_gradient_encoding() {
1243 let stops: SharedVector<GradientStop> = [
1244 GradientStop { position: 0.0, color: Color::from_argb_u8(255, 255, 0, 0) },
1245 GradientStop { position: 0.5, color: Color::from_argb_u8(255, 0, 255, 0) },
1246 GradientStop { position: 1.0, color: Color::from_argb_u8(255, 0, 0, 255) },
1247 ]
1248 .into();
1249 let grad = LinearGradientBrush::new(256., stops.clone());
1250 assert_eq!(grad.angle(), 256.);
1251 assert!(grad.stops().eq(stops.iter()));
1252}
1253
1254#[test]
1255fn test_conic_gradient_basic() {
1256 let grad = ConicGradientBrush::new(
1258 0.0,
1259 [
1260 GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
1261 GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
1262 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
1263 ],
1264 );
1265 assert_eq!(grad.angle(), 0.0);
1266 assert_eq!(grad.stops().count(), 3);
1267}
1268
1269#[test]
1270fn test_conic_gradient_with_rotation() {
1271 let grad = ConicGradientBrush::new(
1273 90.0,
1274 [
1275 GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
1276 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
1277 ],
1278 );
1279 assert_eq!(grad.angle(), 90.0);
1280 assert!(grad.stops().count() >= 2);
1282}
1283
1284#[test]
1285fn test_conic_gradient_negative_angle() {
1286 let grad = ConicGradientBrush::new(
1288 -90.0,
1289 [GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) }],
1290 );
1291 assert_eq!(grad.angle(), -90.0); assert!(grad.stops().count() >= 2); }
1294
1295#[test]
1296fn test_conic_gradient_stops_outside_range() {
1297 let grad = ConicGradientBrush::new(
1299 0.0,
1300 [
1301 GradientStop { position: -0.2, color: Color::from_rgb_u8(255, 0, 0) },
1302 GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
1303 GradientStop { position: 1.2, color: Color::from_rgb_u8(0, 0, 255) },
1304 ],
1305 );
1306 for stop in grad.stops() {
1308 assert!(stop.position >= 0.0 && stop.position <= 1.0);
1309 }
1310}
1311
1312#[test]
1313fn test_conic_gradient_all_stops_below_zero() {
1314 let grad = ConicGradientBrush::new(
1316 0.0,
1317 [
1318 GradientStop { position: -0.5, color: Color::from_rgb_u8(255, 0, 0) },
1319 GradientStop { position: -0.3, color: Color::from_rgb_u8(0, 255, 0) },
1320 ],
1321 );
1322 assert!(grad.stops().count() >= 2);
1324 let first = grad.stops().next().unwrap();
1326 assert!(first.position >= 0.0 && first.position < 0.1);
1327}
1328
1329#[test]
1330fn test_conic_gradient_all_stops_above_one() {
1331 let grad = ConicGradientBrush::new(
1333 0.0,
1334 [
1335 GradientStop { position: 1.2, color: Color::from_rgb_u8(255, 0, 0) },
1336 GradientStop { position: 1.5, color: Color::from_rgb_u8(0, 255, 0) },
1337 ],
1338 );
1339 assert!(grad.stops().count() >= 2);
1341 let last = grad.stops().last().unwrap();
1343 assert!(last.position > 0.9 && last.position <= 1.0);
1344}
1345
1346#[test]
1347fn test_conic_gradient_empty() {
1348 let grad = ConicGradientBrush::new(0.0, []);
1350 assert_eq!(grad.stops().count(), 2);
1352}
1353
1354#[test]
1355fn test_radial_gradient_preserves_center_on_brighter() {
1356 let grad = RadialGradientBrush::new_circle([
1357 GradientStop { position: 0.0, color: Color::from_rgb_u8(200, 100, 50) },
1358 GradientStop { position: 1.0, color: Color::from_rgb_u8(50, 200, 100) },
1359 ])
1360 .with_center(10.0, 20.0)
1361 .with_radius(30.0);
1362 let brighter = Brush::RadialGradient(grad.clone()).brighter(0.5);
1363 if let Brush::RadialGradient(b) = brighter {
1364 assert_eq!(b.center_x(), 10.0);
1365 assert_eq!(b.center_y(), 20.0);
1366 assert_eq!(b.radius(), 30.0);
1367 } else {
1368 panic!("Expected RadialGradient");
1369 }
1370}
1371
1372#[test]
1373fn test_radial_gradient_default_center() {
1374 let grad = RadialGradientBrush::new_circle([]);
1375 assert!(grad.center_x().is_nan());
1376 assert!(grad.center_y().is_nan());
1377 assert!(grad.radius() < 0.0);
1378 assert_eq!(grad.center_or_default(100.0, 80.0), (50.0, 40.0));
1379 assert!((grad.radius_or_default(60.0, 80.0) - 50.0).abs() < 0.01);
1380}
1381
1382#[test]
1383fn test_radial_gradient_scaled_explicit_values() {
1384 let grad = RadialGradientBrush::new_circle([]).with_center(10.0, 20.0).with_radius(30.0);
1385
1386 assert_eq!(grad.center_or_default_scaled(200.0, 160.0, 2.0), (20.0, 40.0));
1387 assert_eq!(grad.radius_or_default_scaled(200.0, 160.0, 2.0), 60.0);
1388}
1389
1390#[test]
1391fn test_radial_gradient_scaled_defaults_use_physical_frame() {
1392 let grad = RadialGradientBrush::new_circle([]);
1393
1394 assert_eq!(grad.center_or_default_scaled(200.0, 160.0, 2.0), (100.0, 80.0));
1395 assert!((grad.radius_or_default_scaled(120.0, 160.0, 2.0) - 100.0).abs() < 0.01);
1396}
1397
1398#[test]
1399fn test_radial_gradient_interpolation_reaches_explicit_metadata() {
1400 let source = Brush::RadialGradient(RadialGradientBrush::new_circle([
1401 GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1402 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1403 ]));
1404 let target_grad = RadialGradientBrush::new_circle([
1405 GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1406 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1407 ])
1408 .with_center(10.0, 20.0)
1409 .with_radius(30.0);
1410 let target = Brush::RadialGradient(target_grad.clone());
1411
1412 if let Brush::RadialGradient(result) = source.interpolate(&target, 1.0) {
1413 assert_eq!(result.center_x(), target_grad.center_x());
1414 assert_eq!(result.center_y(), target_grad.center_y());
1415 assert_eq!(result.radius(), target_grad.radius());
1416 } else {
1417 panic!("Expected RadialGradient");
1418 }
1419}
1420
1421#[test]
1422fn test_radial_gradient_interpolation_reaches_default_metadata() {
1423 let source_grad = RadialGradientBrush::new_circle([
1424 GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1425 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1426 ])
1427 .with_center(10.0, 20.0)
1428 .with_radius(30.0);
1429 let source = Brush::RadialGradient(source_grad);
1430 let target = Brush::RadialGradient(RadialGradientBrush::new_circle([
1431 GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1432 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1433 ]));
1434
1435 if let Brush::RadialGradient(result) = source.interpolate(&target, 1.0) {
1436 assert!(result.center_x().is_nan());
1437 assert!(result.center_y().is_nan());
1438 assert!(result.radius() < 0.0);
1439 } else {
1440 panic!("Expected RadialGradient");
1441 }
1442}
1443
1444#[test]
1445fn test_conic_gradient_interpolation_reaches_explicit_center() {
1446 let source = Brush::ConicGradient(ConicGradientBrush::new(
1447 0.0,
1448 [
1449 GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1450 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1451 ],
1452 ));
1453 let target_grad = ConicGradientBrush::new(
1454 0.0,
1455 [
1456 GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
1457 GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
1458 ],
1459 )
1460 .with_center(40.0, 50.0);
1461 let target = Brush::ConicGradient(target_grad.clone());
1462
1463 if let Brush::ConicGradient(result) = source.interpolate(&target, 1.0) {
1464 assert_eq!(result.center_x(), target_grad.center_x());
1465 assert_eq!(result.center_y(), target_grad.center_y());
1466 } else {
1467 panic!("Expected ConicGradient");
1468 }
1469}