zng_color/
gradient.rs

1//! Gradient types.
2
3use std::{fmt, ops::Range};
4
5use zng_layout::{context::*, unit::*};
6
7use crate::*;
8
9/// Specifies how to draw the gradient outside the first and last stop.
10#[derive(Clone, Default, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11pub enum ExtendMode {
12    /// The color values at the ends of the gradient vector fill the remaining space.
13    ///
14    /// This is the default mode.
15    #[default]
16    Clamp,
17    /// The gradient is repeated until the space is filled.
18    Repeat,
19    /// The gradient is repeated alternating direction until the space is filled.
20    Reflect,
21}
22impl fmt::Debug for ExtendMode {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        if f.alternate() {
25            write!(f, "ExtendMode::")?;
26        }
27        match self {
28            ExtendMode::Clamp => write!(f, "Clamp"),
29            ExtendMode::Repeat => write!(f, "Repeat"),
30            ExtendMode::Reflect => write!(f, "Reflect"),
31        }
32    }
33}
34impl From<ExtendMode> for RenderExtendMode {
35    /// `Reflect` is converted to `Repeat`, you need to prepare the color stops to repeat *reflecting*.
36    fn from(mode: ExtendMode) -> Self {
37        match mode {
38            ExtendMode::Clamp => RenderExtendMode::Clamp,
39            ExtendMode::Repeat => RenderExtendMode::Repeat,
40            ExtendMode::Reflect => RenderExtendMode::Repeat,
41        }
42    }
43}
44
45/// Gradient extend mode supported by the render.
46///
47/// Note that [`ExtendMode::Reflect`] is not supported
48/// directly, you must duplicate and mirror the stops and use the `Repeat` render mode.
49pub type RenderExtendMode = zng_view_api::ExtendMode;
50
51/// The radial gradient radius base length.
52///
53/// This is the full available length for the radius value, so a radius of `100.pct()` will have
54/// the exact length defined by this enum. The available lengths are all defined as the distance from
55/// the center point to an edge or corner.
56///
57/// Note that the color stops are layout in the longest dimension and then *squished* in the shortest dimension.
58#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
59pub enum GradientRadiusBase {
60    /// Length to the closest edge from the center point.
61    ClosestSide,
62    /// Length to the closest corner from the center point.
63    ClosestCorner,
64    /// Length to the farthest edge from the center point.
65    FarthestSide,
66    /// Length to the farthest corner from the center point.
67    ///
68    /// This is the default value.
69    #[default]
70    FarthestCorner,
71}
72impl fmt::Debug for GradientRadiusBase {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        if f.alternate() {
75            write!(f, "GradientRadiusBase::")?;
76        }
77        match self {
78            Self::ClosestSide => write!(f, "ClosestSide"),
79            Self::ClosestCorner => write!(f, "ClosestCorner"),
80            Self::FarthestSide => write!(f, "FarthestSide"),
81            Self::FarthestCorner => write!(f, "FarthestCorner"),
82        }
83    }
84}
85
86/// The radial gradient radius length in both dimensions.
87#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
88pub struct GradientRadius {
89    /// How the base length is calculated. The base length is the `100.pct()` length.
90    pub base: GradientRadiusBase,
91
92    /// If the gradient is circular or elliptical.
93    ///
94    /// If `true` the radius is the same in both dimensions, if `false` the radius can be different.
95    pub circle: bool,
96
97    /// The length of the rendered gradient stops.
98    pub radii: Size,
99}
100impl fmt::Debug for GradientRadius {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("GradientRadius")
103            .field("base", &self.base)
104            .field("radius", &self.radii)
105            .finish()
106    }
107}
108impl Default for GradientRadius {
109    /// `farthest_corner(100.pct())`
110    fn default() -> Self {
111        Self::farthest_corner(1.fct())
112    }
113}
114impl GradientRadius {
115    /// Ellipse radii relative from center to the closest edge.
116    pub fn closest_side(radius: impl Into<Size>) -> Self {
117        Self {
118            base: GradientRadiusBase::ClosestSide,
119            circle: false,
120            radii: radius.into(),
121        }
122    }
123
124    /// Ellipse radii relative from center to the closest corner.
125    pub fn closest_corner(radius: impl Into<Size>) -> Self {
126        Self {
127            base: GradientRadiusBase::ClosestCorner,
128            circle: false,
129            radii: radius.into(),
130        }
131    }
132
133    /// Ellipse radii relative from center to the farthest edge.
134    pub fn farthest_side(radius: impl Into<Size>) -> Self {
135        Self {
136            base: GradientRadiusBase::FarthestSide,
137            circle: false,
138            radii: radius.into(),
139        }
140    }
141
142    /// Ellipse radii relative from center to the farthest corner.
143    pub fn farthest_corner(radius: impl Into<Size>) -> Self {
144        Self {
145            base: GradientRadiusBase::FarthestCorner,
146            circle: false,
147            radii: radius.into(),
148        }
149    }
150
151    /// Enable circular radius.
152    pub fn circle(mut self) -> Self {
153        self.circle = true;
154        self
155    }
156
157    /// Compute the radius in the current [`LAYOUT`] context.
158    ///
159    /// [`LAYOUT`]: zng_layout::context::LAYOUT
160    pub fn layout(&self, center: PxPoint) -> PxSize {
161        let size = LAYOUT.constraints().fill_size();
162
163        let min_sides = || {
164            PxSize::new(
165                center.x.min(size.width - center.x).max(Px(0)),
166                center.y.min(size.height - center.y).max(Px(0)),
167            )
168        };
169        let max_sides = || {
170            PxSize::new(
171                center.x.max(size.width - center.x).max(Px(0)),
172                center.y.max(size.height - center.y).max(Px(0)),
173            )
174        };
175
176        let base_size = match self.base {
177            GradientRadiusBase::ClosestSide => {
178                let min = min_sides();
179                if self.circle {
180                    PxSize::splat(min.width.min(min.height))
181                } else {
182                    min
183                }
184            }
185            GradientRadiusBase::ClosestCorner => {
186                let min = min_sides();
187                if self.circle {
188                    let s = min.cast::<f32>();
189                    let l = s.width.hypot(s.height);
190                    PxSize::splat(Px(l as _))
191                } else {
192                    // preserve aspect-ratio of ClosestSide
193                    let s = std::f32::consts::FRAC_1_SQRT_2 * 2.0;
194                    PxSize::new(min.width * s, min.height * s)
195                }
196            }
197            GradientRadiusBase::FarthestSide => {
198                let max = max_sides();
199                if self.circle {
200                    PxSize::splat(max.width.max(max.height))
201                } else {
202                    max
203                }
204            }
205            GradientRadiusBase::FarthestCorner => {
206                let max = max_sides();
207                if self.circle {
208                    let s = max.cast::<f32>();
209                    let l = s.width.hypot(s.height);
210                    PxSize::splat(Px(l as _))
211                } else {
212                    let s = std::f32::consts::FRAC_1_SQRT_2 * 2.0;
213                    PxSize::new(max.width * s, max.height * s)
214                }
215            }
216        };
217
218        LAYOUT.with_constraints(PxConstraints2d::new_exact_size(base_size), || self.radii.layout_dft(base_size))
219    }
220}
221impl_from_and_into_var! {
222    /// Ellipse fill the base radius.
223    fn from(base: GradientRadiusBase) -> GradientRadius {
224        GradientRadius {
225            base,
226            circle: false,
227            radii: Size::fill(),
228        }
229    }
230
231    /// Ellipse [`GradientRadiusBase`] and ellipse radius.
232    fn from<B: Into<GradientRadiusBase>, R: Into<Length>>((base, radius): (B, R)) -> GradientRadius {
233        GradientRadius {
234            base: base.into(),
235            circle: false,
236            radii: Size::splat(radius),
237        }
238    }
239
240    /// Ellipse [`GradientRadius::farthest_corner`].
241    fn from(radius: Length) -> GradientRadius {
242        GradientRadius::farthest_corner(radius)
243    }
244    /// Ellipse [`GradientRadius::farthest_corner`].
245    fn from(radii: Size) -> GradientRadius {
246        GradientRadius::farthest_corner(radii)
247    }
248
249    /// Conversion to [`Length::Factor`] and to radius.
250    fn from(percent: FactorPercent) -> GradientRadius {
251        Length::Factor(percent.into()).into()
252    }
253    /// Conversion to [`Length::Factor`] and to radius.
254    fn from(norm: Factor) -> GradientRadius {
255        Length::Factor(norm).into()
256    }
257    /// Conversion to [`Length::DipF32`] and to radius.
258    fn from(f: f32) -> GradientRadius {
259        Length::DipF32(f).into()
260    }
261    /// Conversion to [`Length::Dip`] and to radius.
262    fn from(i: i32) -> GradientRadius {
263        Length::Dip(Dip::new(i)).into()
264    }
265    /// Conversion to [`Length::Px`] and to radius.
266    fn from(l: Px) -> GradientRadius {
267        Length::Px(l).into()
268    }
269    /// Conversion to [`Length::Dip`] and to radius.
270    fn from(l: Dip) -> GradientRadius {
271        Length::Dip(l).into()
272    }
273}
274
275/// The [angle](AngleUnits) or [line](zng_layout::unit::Line) that defines a linear gradient.
276///
277/// # Examples
278///
279/// ```
280/// # use zng_layout::unit::*;
281/// # use zng_color::colors;
282/// # use zng_color::gradient::*;
283/// # fn linear_gradient(axis: impl Into<LinearGradientAxis>, stops: impl Into<GradientStops>) { }
284/// let angle_gradient = linear_gradient(90.deg(), [colors::BLACK, colors::WHITE]);
285/// let line_gradient = linear_gradient((0, 0).to(50, 30), [colors::BLACK, colors::WHITE]);
286/// ```
287#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
288pub enum LinearGradientAxis {
289    /// Line defined by an angle. 0º is a line from bottom-to-top, 90º is a line from left-to-right.
290    ///
291    /// The line end-points are calculated so that the full gradient is visible from corner-to-corner, this is
292    /// sometimes called *magic corners*.
293    Angle(AngleRadian),
294
295    /// Line defined by two points. If the points are inside the fill area the gradient is extended-out in the
296    /// same direction defined by the line, according to the extend mode.
297    Line(Line),
298}
299impl fmt::Debug for LinearGradientAxis {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        if f.alternate() {
302            match self {
303                LinearGradientAxis::Angle(a) => f.debug_tuple("LinearGradientAxis::Angle").field(a).finish(),
304                LinearGradientAxis::Line(l) => f.debug_tuple("LinearGradientAxis::Line").field(l).finish(),
305            }
306        } else {
307            match self {
308                LinearGradientAxis::Angle(a) => write!(f, "{}.deg()", AngleDegree::from(*a).0),
309                LinearGradientAxis::Line(l) => write!(f, "{l:?}"),
310            }
311        }
312    }
313}
314impl Layout2d for LinearGradientAxis {
315    type Px = PxLine;
316
317    fn layout(&self) -> Self::Px {
318        self.layout_dft(PxLine::new(PxPoint::new(Px(0), LAYOUT.viewport().height), PxPoint::zero()))
319    }
320
321    fn layout_dft(&self, default: Self::Px) -> Self::Px {
322        match self {
323            LinearGradientAxis::Angle(rad) => {
324                let dir_x = rad.0.sin();
325                let dir_y = -rad.0.cos();
326
327                let av = LAYOUT.constraints().fill_size();
328                let av_width = av.width.0 as f32;
329                let av_height = av.height.0 as f32;
330
331                let line_length = (dir_x * av_width).abs() + (dir_y * av_height).abs();
332
333                let inv_dir_length = 1.0 / (dir_x * dir_x + dir_y * dir_y).sqrt();
334
335                let delta = euclid::Vector2D::<_, ()>::new(
336                    dir_x * inv_dir_length * line_length / 2.0,
337                    dir_y * inv_dir_length * line_length / 2.0,
338                );
339
340                let center = euclid::Point2D::new(av_width / 2.0, av_height / 2.0);
341
342                let start = center - delta;
343                let end = center + delta;
344                PxLine::new(
345                    PxPoint::new(Px(start.x as i32), Px(start.y as i32)),
346                    PxPoint::new(Px(end.x as i32), Px(end.y as i32)),
347                )
348            }
349            LinearGradientAxis::Line(line) => line.layout_dft(default),
350        }
351    }
352
353    fn affect_mask(&self) -> LayoutMask {
354        match self {
355            LinearGradientAxis::Angle(_) => LayoutMask::CONSTRAINTS,
356            LinearGradientAxis::Line(line) => line.affect_mask(),
357        }
358    }
359}
360impl_from_and_into_var! {
361    fn from(angle: AngleRadian) -> LinearGradientAxis {
362        LinearGradientAxis::Angle(angle)
363    }
364    fn from(angle: AngleDegree) -> LinearGradientAxis {
365        LinearGradientAxis::Angle(angle.into())
366    }
367    fn from(angle: AngleTurn) -> LinearGradientAxis {
368        LinearGradientAxis::Angle(angle.into())
369    }
370    fn from(angle: AngleGradian) -> LinearGradientAxis {
371        LinearGradientAxis::Angle(angle.into())
372    }
373    fn from(line: Line) -> LinearGradientAxis {
374        LinearGradientAxis::Line(line)
375    }
376}
377impl Transitionable for LinearGradientAxis {
378    /// Linear interpolates for same axis kinds, or changes in one step between axis kinds.
379    fn lerp(self, to: &Self, step: EasingStep) -> Self {
380        use LinearGradientAxis::*;
381        match (self, to) {
382            (Angle(s), Angle(t)) => Angle(s.lerp(*t, step)),
383            (Line(s), Line(t)) => Line(s.lerp(t, step)),
384            (s, t) => {
385                if step <= 1.fct() {
386                    s
387                } else {
388                    t.clone()
389                }
390            }
391        }
392    }
393}
394
395/// A color stop in a gradient.
396#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
397pub struct ColorStop {
398    /// The color.
399    pub color: Rgba,
400    /// Offset point where the [`color`] is fully visible.
401    ///
402    /// Relative lengths are calculated on the length of the gradient line. The [`Length::Default`] value
403    /// indicates this color stop [is positional].
404    ///
405    /// [`color`]: ColorStop::color
406    /// [is positional]: ColorStop::is_positional
407    /// [`Length::Default`]: zng_layout::unit::Length::Default
408    pub offset: Length,
409}
410impl fmt::Debug for ColorStop {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        if f.alternate() {
413            f.debug_struct("ColorStop")
414                .field("color", &self.color)
415                .field("offset", &self.offset)
416                .finish()
417        } else if self.is_positional() {
418            write!(f, "{:?}", self.color)
419        } else {
420            write!(f, "({:?}, {:?})", self.color, self.offset)
421        }
422    }
423}
424impl ColorStop {
425    /// New color stop with a defined offset.
426    pub fn new(color: impl Into<Rgba>, offset: impl Into<Length>) -> Self {
427        ColorStop {
428            color: color.into(),
429            offset: offset.into(),
430        }
431    }
432
433    /// New color stop with a undefined offset.
434    ///
435    /// See [`is_positional`] for more details.
436    ///
437    /// [`is_positional`]: Self::is_positional
438    pub fn new_positional(color: impl Into<Rgba>) -> Self {
439        ColorStop {
440            color: color.into(),
441            offset: Length::Default,
442        }
443    }
444
445    /// If this color stop offset is resolved relative to the position of the color stop in the stops list.
446    ///
447    /// A [`Length::Default`] offset indicates that the color stop is positional.
448    ///
449    /// # Layout
450    ///
451    /// When a [`GradientStops`] calculates layout, positional stops are resolved like this:
452    ///
453    /// * If it is the first stop, the offset is 0%.
454    /// * If it is the last stop, the offset is 100% or the previous stop offset whichever is greater.
455    /// * If it is surrounded by two stops with known offsets it is the mid-point between the two stops.
456    /// * If there is a sequence of positional stops, they split the available length that is defined by the two
457    ///   stops with known length that define the sequence.
458    ///
459    /// # Note
460    ///
461    /// Use [`ColorStop::is_layout_positional`] if you already have the layout offset.
462    ///
463    /// [`Length::Default`]: zng_layout::unit::Length::Default
464    pub fn is_positional(&self) -> bool {
465        self.offset.is_default()
466    }
467
468    /// If a calculated layout offset is [positional].
469    ///
470    /// Positive infinity ([`f32::INFINITY`]) is used to indicate that the color stop is
471    /// positional in webrender units.
472    ///
473    /// [positional]: Self::is_positional
474    pub fn is_layout_positional(layout_offset: f32) -> bool {
475        !f32::is_finite(layout_offset)
476    }
477
478    /// Compute a [`RenderGradientStop`] in the current [`LAYOUT`] context.
479    ///
480    /// The `axis` value is used to select the [`LAYOUT`] axis inside the offset length.
481    ///
482    /// Note that if this color stop [is positional] the returned offset is [`f32::INFINITY`].
483    /// You can use [`ColorStop::is_layout_positional`] to check a layout offset.
484    ///
485    /// [is positional]: Self::is_positional
486    /// [`LAYOUT`]: zng_layout::context::LAYOUT
487    pub fn layout(&self, axis: LayoutAxis) -> RenderGradientStop {
488        RenderGradientStop {
489            offset: if self.offset.is_default() {
490                f32::INFINITY
491            } else {
492                self.offset.layout_f32(axis)
493            },
494            color: self.color,
495        }
496    }
497}
498impl_from_and_into_var! {
499    fn from<C: Into<Rgba>, O: Into<Length>>((color, offset): (C, O)) -> ColorStop {
500        ColorStop::new(color, offset)
501    }
502
503    fn from(positional_color: Rgba) -> ColorStop {
504        ColorStop::new_positional(positional_color)
505    }
506
507    fn from(positional_color: Hsla) -> ColorStop {
508        ColorStop::new_positional(positional_color)
509    }
510
511    fn from(positional_color: Hsva) -> ColorStop {
512        ColorStop::new_positional(positional_color)
513    }
514}
515impl Transitionable for ColorStop {
516    fn lerp(self, to: &Self, step: EasingStep) -> Self {
517        Self {
518            color: self.color.lerp(&to.color, step),
519            offset: self.offset.lerp(&to.offset, step),
520        }
521    }
522}
523
524/// Computed [`GradientStop`].
525///
526/// The color offset is in the 0..=1 range.
527pub type RenderGradientStop = zng_view_api::GradientStop;
528
529/// A stop in a gradient.
530#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
531pub enum GradientStop {
532    /// Color stop.
533    Color(ColorStop),
534    /// Midway point between two colors.
535    ColorHint(Length),
536}
537impl_from_and_into_var! {
538    fn from<C: Into<Rgba>, O: Into<Length>>(color_offset: (C, O)) -> GradientStop {
539        GradientStop::Color(color_offset.into())
540    }
541
542    fn from(color_stop: ColorStop) -> GradientStop {
543        GradientStop::Color(color_stop)
544    }
545
546    fn from(color_hint: Length) -> GradientStop {
547        GradientStop::ColorHint(color_hint)
548    }
549
550    /// Conversion to [`Length::Factor`] color hint.
551    fn from(color_hint: FactorPercent) -> GradientStop {
552        GradientStop::ColorHint(color_hint.into())
553    }
554
555    /// Conversion to [`Length::Factor`] color hint.
556    fn from(color_hint: Factor) -> GradientStop {
557        GradientStop::ColorHint(color_hint.into())
558    }
559
560    /// Conversion to [`Length::Dip`] color hint.
561    fn from(color_hint: f32) -> GradientStop {
562        GradientStop::ColorHint(color_hint.into())
563    }
564
565    /// Conversion to [`Length::Dip`] color hint.
566    fn from(color_hint: i32) -> GradientStop {
567        GradientStop::ColorHint(color_hint.into())
568    }
569
570    /// Conversion to positional color.
571    fn from(positional_color: Rgba) -> GradientStop {
572        GradientStop::Color(ColorStop::new_positional(positional_color))
573    }
574
575    /// Conversion to positional color.
576    fn from(positional_color: Hsla) -> GradientStop {
577        GradientStop::Color(ColorStop::new_positional(positional_color))
578    }
579
580    /// Conversion to positional color.
581    fn from(positional_color: Hsva) -> GradientStop {
582        GradientStop::Color(ColorStop::new_positional(positional_color))
583    }
584}
585impl fmt::Debug for GradientStop {
586    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587        if f.alternate() {
588            match self {
589                GradientStop::Color(c) => f.debug_tuple("GradientStop::Color").field(c).finish(),
590                GradientStop::ColorHint(l) => f.debug_tuple("GradientStop::ColorHint").field(l).finish(),
591            }
592        } else {
593            match self {
594                GradientStop::Color(c) => write!(f, "{c:?}"),
595                GradientStop::ColorHint(l) => write!(f, "{l:?}"),
596            }
597        }
598    }
599}
600
601/// Stops in a gradient.
602///
603/// Use [`stops!`] to create a new instance, you can convert from arrays for simpler gradients.
604#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
605pub struct GradientStops {
606    /// First color stop.
607    pub start: ColorStop,
608
609    /// Optional stops between start and end.
610    pub middle: Vec<GradientStop>,
611
612    /// Last color stop.
613    pub end: ColorStop,
614}
615impl fmt::Debug for GradientStops {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        if f.alternate() {
618            f.debug_struct("GradientStops")
619                .field("start", &self.start)
620                .field("middle", &self.middle)
621                .field("end", &self.end)
622                .finish()
623        } else {
624            write!(f, "stops![{:?}, ", self.start)?;
625            for stop in &self.middle {
626                write!(f, "{stop:?}, ")?;
627            }
628            write!(f, "{:?}]", self.end)
629        }
630    }
631}
632#[expect(clippy::len_without_is_empty)] // cannot be empty
633impl GradientStops {
634    /// Gradients stops with two colors from `start` to `end`.
635    pub fn new(start: impl Into<Rgba>, end: impl Into<Rgba>) -> Self {
636        GradientStops {
637            start: ColorStop {
638                color: start.into(),
639                offset: Length::zero(),
640            },
641            middle: vec![],
642            end: ColorStop {
643                color: end.into(),
644                offset: 100.pct().into(),
645            },
646        }
647    }
648
649    fn start_missing() -> ColorStop {
650        ColorStop {
651            color: colors::BLACK.transparent(),
652            offset: Length::zero(),
653        }
654    }
655
656    fn end_missing(start_color: Rgba) -> ColorStop {
657        ColorStop {
658            color: start_color.transparent(),
659            offset: 100.pct().into(),
660        }
661    }
662
663    /// Gradient stops from colors spaced equally.
664    ///
665    /// The stops look like a sequence of positional only color stops but
666    /// the proportional distribution is pre-calculated.
667    ///
668    /// If less than 2 colors are given, the missing stops are filled with transparent color.
669    pub fn from_colors<C: Into<Rgba> + Copy>(colors: &[C]) -> Self {
670        if colors.is_empty() {
671            GradientStops {
672                start: Self::start_missing(),
673                middle: vec![],
674                end: Self::end_missing(colors::BLACK),
675            }
676        } else if colors.len() == 1 {
677            let color = colors[0].into();
678            GradientStops {
679                start: ColorStop {
680                    color,
681                    offset: Length::zero(),
682                },
683                middle: vec![],
684                end: Self::end_missing(color),
685            }
686        } else {
687            let last = colors.len() - 1;
688            let mut offset = 1.0 / colors.len() as f32;
689            let offset_step = offset;
690            GradientStops {
691                start: ColorStop {
692                    color: colors[0].into(),
693                    offset: Length::zero(),
694                },
695                middle: colors[1..last]
696                    .iter()
697                    .map(|&c| {
698                        GradientStop::Color(ColorStop {
699                            color: c.into(),
700                            offset: {
701                                let r = offset;
702                                offset += offset_step;
703                                r.fct().into()
704                            },
705                        })
706                    })
707                    .collect(),
708                end: ColorStop {
709                    color: colors[last].into(),
710                    offset: 100.pct().into(),
711                },
712            }
713        }
714    }
715
716    /// Gradient stops from colors forming stripes of same length.
717    ///
718    /// The `transition` parameter controls relative length of the transition between two stripes.
719    /// `1.0` or `100.pct()` is the length of a stripe, set to `0.0` to get hard-lines.
720    pub fn from_stripes<C: Into<Rgba> + Copy, T: Into<Factor>>(colors: &[C], transition: T) -> Self {
721        let tran = transition.into().0;
722        let tran = if tran.is_nan() || tran < 0.0 {
723            0.0
724        } else if tran > 1.0 {
725            1.0
726        } else {
727            tran
728        };
729
730        if colors.is_empty() {
731            GradientStops {
732                start: Self::start_missing(),
733                middle: vec![],
734                end: Self::end_missing(colors::BLACK),
735            }
736        } else if colors.len() == 1 {
737            let tran = 0.5 * tran;
738
739            let color = colors[0].into();
740            let end = Self::end_missing(color);
741            GradientStops {
742                start: ColorStop {
743                    color,
744                    offset: Length::zero(),
745                },
746                middle: vec![
747                    GradientStop::Color(ColorStop {
748                        color,
749                        offset: Length::Factor(Factor(0.5 - tran)),
750                    }),
751                    GradientStop::Color(ColorStop {
752                        color: end.color,
753                        offset: Length::Factor(Factor(0.5 + tran)),
754                    }),
755                ],
756                end,
757            }
758        } else {
759            let last = colors.len() - 1;
760            let mut offset = 1.0 / colors.len() as f32;
761            let stripe_width = offset;
762            let tran = stripe_width * tran;
763
764            let start = ColorStop {
765                color: colors[0].into(),
766                offset: Length::zero(),
767            };
768            let mut middle = vec![
769                ColorStop {
770                    color: start.color,
771                    offset: (offset - tran).fct().into(),
772                }
773                .into(),
774            ];
775
776            for &color in &colors[1..last] {
777                let color = color.into();
778                middle.push(
779                    ColorStop {
780                        color,
781                        offset: (offset + tran).fct().into(),
782                    }
783                    .into(),
784                );
785                offset += stripe_width;
786                middle.push(
787                    ColorStop {
788                        color,
789                        offset: (offset - tran).fct().into(),
790                    }
791                    .into(),
792                );
793            }
794
795            let end = ColorStop {
796                color: colors[last].into(),
797                offset: Length::Factor(Factor(1.0)),
798            };
799            middle.push(
800                ColorStop {
801                    color: end.color,
802                    offset: offset.fct().into(),
803                }
804                .into(),
805            );
806
807            GradientStops { start, middle, end }
808        }
809    }
810
811    /// Gradient stops from color stops.
812    ///
813    /// If less than 2 colors are given, the missing stops are filled with transparent color.
814    pub fn from_stops<C: Into<ColorStop> + Copy>(stops: &[C]) -> Self {
815        if stops.is_empty() {
816            GradientStops {
817                start: Self::start_missing(),
818                middle: vec![],
819                end: Self::end_missing(colors::BLACK),
820            }
821        } else if stops.len() == 1 {
822            let start = stops[0].into();
823            GradientStops {
824                end: Self::end_missing(start.color),
825                start,
826                middle: vec![],
827            }
828        } else {
829            let last = stops.len() - 1;
830            GradientStops {
831                start: stops[0].into(),
832                middle: stops[1..last].iter().map(|&c| GradientStop::Color(c.into())).collect(),
833                end: stops[last].into(),
834            }
835        }
836    }
837
838    /// Set the alpha of all colors in the gradient.
839    pub fn set_alpha<A: Into<RgbaComponent>>(&mut self, alpha: A) {
840        let alpha = alpha.into();
841        self.start.color.set_alpha(alpha);
842        for mid in &mut self.middle {
843            if let GradientStop::Color(c) = mid {
844                c.color.set_alpha(alpha);
845            }
846        }
847        self.end.color.set_alpha(alpha);
848    }
849
850    /// Computes the linear gradient in the current [`LAYOUT`] context.
851    ///
852    /// The `axis` value selects the layout axis the offsets layout on.
853    ///
854    /// The `render_stops` content is replaced with stops with offset in the `0..=1` range.
855    ///
856    /// The `line` points are moved to accommodate input offsets outside the line bounds.
857    ///
858    /// [`LAYOUT`]: zng_layout::context::LAYOUT
859    pub fn layout_linear(&self, axis: LayoutAxis, extend_mode: ExtendMode, line: &mut PxLine, render_stops: &mut Vec<RenderGradientStop>) {
860        let (start_offset, end_offset) = self.layout(axis, extend_mode, render_stops);
861
862        let mut l_start = line.start.cast::<f32>();
863        let mut l_end = line.end.cast::<f32>();
864
865        let v = l_end - l_start;
866        let v = v / LAYOUT.constraints_for(axis).fill().0 as f32;
867
868        l_end = l_start + v * end_offset;
869        l_start += v * start_offset;
870
871        line.start = l_start.cast::<Px>();
872        line.end = l_end.cast::<Px>();
873    }
874
875    /// Computes the layout for a radial gradient.
876    ///
877    /// The `render_stops` content is replace with stops with offset in the `0..=1` range.
878    pub fn layout_radial(&self, axis: LayoutAxis, extend_mode: ExtendMode, render_stops: &mut Vec<RenderGradientStop>) {
879        self.layout(axis, extend_mode, render_stops);
880    }
881
882    /// Computes the actual color stops.
883    ///
884    /// Returns offsets of the first and last stop in the `length` line.
885    fn layout(&self, axis: LayoutAxis, extend_mode: ExtendMode, render_stops: &mut Vec<RenderGradientStop>) -> (f32, f32) {
886        // In this method we need to:
887        // 1 - Convert all Length values to LayoutLength.
888        // 2 - Adjust offsets so they are always after or equal to the previous offset.
889        // 3 - Convert GradientStop::ColorHint to RenderGradientStop.
890        // 4 - Manually extend a reflection for ExtendMode::Reflect.
891        // 5 - Normalize stop offsets to be all between 0.0..=1.0.
892        // 6 - Return the first and last stop offset in pixels.
893
894        fn is_positional(o: f32) -> bool {
895            ColorStop::is_layout_positional(o)
896        }
897
898        render_stops.clear();
899
900        if extend_mode == ExtendMode::Reflect {
901            render_stops.reserve((self.middle.len() + 2) * 2);
902        } else {
903            render_stops.reserve(self.middle.len() + 2);
904        }
905
906        let mut start = self.start.layout(axis); // 1
907        if is_positional(start.offset) {
908            start.offset = 0.0;
909        }
910        render_stops.push(start);
911
912        let mut prev_offset = start.offset;
913        let mut hints = vec![];
914        let mut positional_start = None;
915
916        for gs in self.middle.iter() {
917            match gs {
918                GradientStop::Color(s) => {
919                    let mut stop = s.layout(axis); // 1
920                    if is_positional(stop.offset) {
921                        if positional_start.is_none() {
922                            positional_start = Some(render_stops.len());
923                        }
924                        render_stops.push(stop);
925                    } else {
926                        if stop.offset < prev_offset {
927                            stop.offset = prev_offset; // 2
928                        }
929                        prev_offset = stop.offset;
930
931                        render_stops.push(stop);
932
933                        if let Some(start) = positional_start.take() {
934                            // finished positional sequence.
935                            // 1
936                            Self::calculate_positional(start..render_stops.len(), render_stops, &hints);
937                        }
938                    }
939                }
940                GradientStop::ColorHint(_) => {
941                    hints.push(render_stops.len());
942                    render_stops.push(RenderGradientStop {
943                        // offset and color will be calculated later.
944                        offset: 0.0,
945                        color: colors::BLACK,
946                    })
947                }
948            }
949        }
950
951        let mut stop = self.end.layout(axis); // 1
952        if is_positional(stop.offset) {
953            stop.offset = LAYOUT.constraints_for(axis).fill().0 as f32;
954        }
955        if stop.offset < prev_offset {
956            stop.offset = prev_offset; // 2
957        }
958        render_stops.push(stop);
959
960        if let Some(start) = positional_start.take() {
961            // finished positional sequence.
962            // 1
963            Self::calculate_positional(start..render_stops.len(), render_stops, &hints);
964        }
965
966        // 3
967        for &i in hints.iter() {
968            let prev = render_stops[i - 1];
969            let after = render_stops[i + 1];
970            let length = after.offset - prev.offset;
971            if length > 0.00001 {
972                if let GradientStop::ColorHint(offset) = &self.middle[i - 1] {
973                    let mut offset = LAYOUT.with_constraints_for(
974                        axis,
975                        LAYOUT.constraints_for(axis).with_new_max(Px(length as i32)).with_fill(true),
976                        || offset.layout_f32(axis),
977                    );
978                    if is_positional(offset) {
979                        offset = length / 2.0;
980                    } else {
981                        offset = offset.clamp(prev.offset, after.offset);
982                    }
983                    offset += prev.offset;
984
985                    let color = prev.color.lerp(&after.color, (100.0 / length / 2.0).fct());
986
987                    let stop = &mut render_stops[i];
988                    stop.color = color;
989                    stop.offset = offset;
990                } else {
991                    unreachable!()
992                }
993            } else {
994                render_stops[i] = prev;
995            }
996        }
997
998        // 4
999        if extend_mode == ExtendMode::Reflect {
1000            let last_offset = render_stops[render_stops.len() - 1].offset;
1001            for i in (0..render_stops.len()).rev() {
1002                let mut stop = render_stops[i];
1003                stop.offset = last_offset + last_offset - stop.offset;
1004                render_stops.push(stop);
1005            }
1006        }
1007
1008        let first = render_stops[0];
1009        let last = render_stops[render_stops.len() - 1];
1010
1011        let actual_length = last.offset - first.offset;
1012
1013        if actual_length >= 1.0 {
1014            // 5
1015            for stop in render_stops {
1016                stop.offset = (stop.offset - first.offset) / actual_length;
1017            }
1018
1019            (first.offset, last.offset) // 5
1020        } else {
1021            // 5 - all stops are at the same offset (within 1px)
1022            match extend_mode {
1023                ExtendMode::Clamp => {
1024                    // we want the first and last color to fill their side
1025                    // any other middle colors can be removed.
1026                    render_stops.clear();
1027                    render_stops.push(first);
1028                    render_stops.push(first);
1029                    render_stops.push(last);
1030                    render_stops.push(last);
1031                    render_stops[0].offset = 0.0;
1032                    render_stops[1].offset = 0.48; // not exactly 0.5 to avoid aliasing.
1033                    render_stops[2].offset = 0.52;
1034                    render_stops[3].offset = 1.0;
1035
1036                    // 6 - stretch the line a bit.
1037                    let offset = last.offset;
1038                    (offset - 10.0, offset + 10.0)
1039                }
1040                ExtendMode::Repeat | ExtendMode::Reflect => {
1041                    // fill with the average of all colors.
1042                    let len = render_stops.len() as f32;
1043                    let color = Rgba::new(
1044                        render_stops.iter().map(|s| s.color.red).sum::<f32>() / len,
1045                        render_stops.iter().map(|s| s.color.green).sum::<f32>() / len,
1046                        render_stops.iter().map(|s| s.color.blue).sum::<f32>() / len,
1047                        render_stops.iter().map(|s| s.color.alpha).sum::<f32>() / len,
1048                    );
1049                    render_stops.clear();
1050                    render_stops.push(RenderGradientStop { offset: 0.0, color });
1051                    render_stops.push(RenderGradientStop { offset: 1.0, color });
1052
1053                    (0.0, 10.0) // 6
1054                }
1055            }
1056        }
1057    }
1058
1059    fn calculate_positional(range: Range<usize>, render_stops: &mut [RenderGradientStop], hints: &[usize]) {
1060        // count of stops in the positional sequence that are not hints.
1061        let sequence_count = range.len() - hints.iter().filter(|i| range.contains(i)).count();
1062        debug_assert!(sequence_count > 1);
1063
1064        // length that must be split between positional stops.
1065        let (start_offset, layout_length) = {
1066            // index of stop after the sequence that has a calculated offset.
1067            let sequence_ender = (range.end..render_stops.len())
1068                .find(|i| !hints.contains(i))
1069                .unwrap_or(range.end - 1);
1070            // index of stop before the sequence that has a calculated offset.
1071            let sequence_starter = (0..range.start).rev().find(|i| !hints.contains(i)).unwrap_or(range.start);
1072
1073            let start_offset = render_stops[sequence_starter].offset;
1074            let length = render_stops[sequence_ender].offset - start_offset;
1075            (start_offset, length)
1076        };
1077
1078        let d = layout_length / (sequence_count + 1) as f32;
1079        let mut offset = start_offset;
1080
1081        for i in range {
1082            if ColorStop::is_layout_positional(render_stops[i].offset) {
1083                offset += d;
1084                render_stops[i].offset = offset;
1085            }
1086        }
1087    }
1088
1089    /// Number of stops.
1090    pub fn len(&self) -> usize {
1091        self.middle.len() + 2
1092    }
1093}
1094impl_from_and_into_var! {
1095    /// [`GradientStops::from_colors`]
1096    fn from(colors: &[Rgba]) -> GradientStops {
1097        GradientStops::from_colors(colors)
1098    }
1099
1100    /// [`GradientStops::from_colors`]
1101    fn from(colors: &[Hsva]) -> GradientStops {
1102        GradientStops::from_colors(colors)
1103    }
1104
1105    /// [`GradientStops::from_colors`]
1106    fn from(colors: &[Hsla]) -> GradientStops {
1107        GradientStops::from_colors(colors)
1108    }
1109
1110    /// [`GradientStops::from_stops`]
1111    fn from<L: Into<Length> + Copy>(stops: &[(Rgba, L)]) -> GradientStops {
1112        GradientStops::from_stops(stops)
1113    }
1114    /// [`GradientStops::from_stops`]
1115    fn from<L: Into<Length> + Copy>(stops: &[(Hsla, L)]) -> GradientStops {
1116        GradientStops::from_stops(stops)
1117    }
1118    /// [`GradientStops::from_stops`]
1119    fn from<L: Into<Length> + Copy>(stops: &[(Hsva, L)]) -> GradientStops {
1120        GradientStops::from_stops(stops)
1121    }
1122
1123    /// [`GradientStops::from_colors`]
1124    fn from<const N: usize>(colors: &[Rgba; N]) -> GradientStops {
1125        GradientStops::from_colors(colors)
1126    }
1127
1128    /// [`GradientStops::from_colors`]
1129    fn from<const N: usize>(colors: &[Hsla; N]) -> GradientStops {
1130        GradientStops::from_colors(colors)
1131    }
1132
1133    /// [`GradientStops::from_colors`]
1134    fn from<const N: usize>(colors: &[Hsva; N]) -> GradientStops {
1135        GradientStops::from_colors(colors)
1136    }
1137
1138    /// [`GradientStops::from_stops`]
1139    fn from<L: Into<Length> + Copy, const N: usize>(stops: &[(Rgba, L); N]) -> GradientStops {
1140        GradientStops::from_stops(stops)
1141    }
1142    /// [`GradientStops::from_stops`]
1143    fn from<L: Into<Length> + Copy, const N: usize>(stops: &[(Hsva, L); N]) -> GradientStops {
1144        GradientStops::from_stops(stops)
1145    }
1146    /// [`GradientStops::from_stops`]
1147    fn from<L: Into<Length> + Copy, const N: usize>(stops: &[(Hsla, L); N]) -> GradientStops {
1148        GradientStops::from_stops(stops)
1149    }
1150
1151    /// [`GradientStops::from_colors`]
1152    fn from<const N: usize>(colors: [Rgba; N]) -> GradientStops {
1153        GradientStops::from_colors(&colors)
1154    }
1155    /// [`GradientStops::from_colors`]
1156    fn from<const N: usize>(colors: [Hsva; N]) -> GradientStops {
1157        GradientStops::from_colors(&colors)
1158    }
1159    /// [`GradientStops::from_colors`]
1160    fn from<const N: usize>(colors: [Hsla; N]) -> GradientStops {
1161        GradientStops::from_colors(&colors)
1162    }
1163
1164    /// [`GradientStops::from_stops`]
1165    fn from<L: Into<Length> + Copy, const N: usize>(stops: [(Rgba, L); N]) -> GradientStops {
1166        GradientStops::from_stops(&stops)
1167    }
1168    /// [`GradientStops::from_stops`]
1169    fn from<L: Into<Length> + Copy, const N: usize>(stops: [(Hsva, L); N]) -> GradientStops {
1170        GradientStops::from_stops(&stops)
1171    }
1172    /// [`GradientStops::from_stops`]
1173    fn from<L: Into<Length> + Copy, const N: usize>(stops: [(Hsla, L); N]) -> GradientStops {
1174        GradientStops::from_stops(&stops)
1175    }
1176}
1177
1178#[doc(hidden)]
1179#[macro_export]
1180macro_rules! __stops {
1181    // match single color stop at the $start, plus $color with 2 stops plus other stops, e.g.:
1182    // stops![colors::RED, (colors::GREEN, 14, 20), colors::BLUE]
1183    // OR
1184    // $next_middle that is a $color with 2 stops, plus other stops, e.g.:
1185    // .. (colors::GREEN, 14, 20), colors::BLUE]
1186    (
1187        start: $start:expr,
1188        middle: [$($middle:expr),*],
1189        tail: ($color:expr, $stop0:expr, $stop1:expr), $($stops:tt)+
1190    ) => {
1191        $crate::__stops! {
1192            start: $start,
1193            middle: [$($middle,)* ($color, $stop0), ($color, $stop1)],
1194            tail: $($stops)+
1195        }
1196    };
1197    // match single color stop at the $start, plus single color stop in the $next_middle, plus other stops, e.g.:
1198    // stops![colors::RED, colors::GREEN, colors::BLUE]
1199    // OR
1200    // $next_middle that is a single color stop, plus other stops, e.g.:
1201    // .. colors::GREEN, colors::BLUE]
1202    (
1203        start: $start:expr,
1204        middle: [$($middle:expr),*],
1205        tail: $next_middle:expr, $($stops:tt)+
1206    ) => {
1207        $crate::__stops! {
1208            start: $start,
1209            middle: [$($middle,)* $next_middle],
1210            tail: $($stops)+
1211        }
1212    };
1213    // match single color stop at the $start, plus single $color with 2 stops, e.g.:
1214    // stops![colors::RED, (colors::GREEN, 15, 30)]
1215    // OR
1216    // match last entry as single $color with 2 stops, e.g.:
1217    // .. (colors::BLUE, 20, 30)]
1218    (
1219        start: $start:expr,
1220        middle: [$($middle:expr),*],
1221        tail: ($color:expr, $stop0:expr, $stop1:expr) $(,)?
1222    ) => {
1223        $crate::__stops! {
1224            start: $start,
1225            middle: [$($middle,)* ($color, $stop0)],
1226            tail: ($color, $stop1)
1227        }
1228    };
1229    // match single color stop at the $start, plus single color stop at the $end, e.g.:
1230    // stops![colors::RED, colors::GREEN]
1231    // OR
1232    // match last entry as single color stop, at the $end, e.g.:
1233    // .. colors::GREEN]
1234    (
1235        start: $start:expr,
1236        middle: [$($middle:expr),*],
1237        tail: $end:expr $(,)?
1238    ) => {
1239        $crate::gradient::GradientStops {
1240            start: $crate::gradient::ColorStop::from($start),
1241            middle: std::vec![$($crate::gradient::GradientStop::from($middle)),*],
1242            end: $crate::gradient::ColorStop::from($end),
1243        }
1244    };
1245}
1246/// Creates a [`GradientStops`] containing the arguments.
1247///
1248/// A minimum of two arguments are required, the first and last argument must be expressions that convert to [`ColorStop`],
1249/// the middle arguments mut be expressions that convert to [`GradientStop`].
1250///
1251/// # Examples
1252///
1253/// ```
1254/// # use zng_color::gradient::stops;
1255/// # use zng_color::colors;
1256/// # use zng_layout::unit::*;
1257/// // green 0%, red 30%, blue 100%.
1258/// let stops = stops![colors::GREEN, (colors::RED, 30.pct()), colors::BLUE];
1259///
1260/// // green to blue, the midway color is at 30%.
1261/// let stops = stops![colors::GREEN, 30.pct(), colors::BLUE];
1262/// ```
1263///
1264/// # Two Stops Per Color
1265///
1266/// The `stops!` macro also accepts a special 3 item *tuple* that represents a color followed by two offsets, this
1267/// expands to two color stops of the same color. The color type must implement `Into<Rgba> + Copy`. The offset types
1268/// must implement `Into<Length>`.
1269///
1270/// ## Examples
1271///
1272/// ```
1273/// # use zng_color::gradient::stops;
1274/// # use zng_color::colors;
1275/// # use zng_layout::unit::*;
1276/// let zebra_stops = stops![(colors::WHITE, 0, 20), (colors::BLACK, 20, 40)];
1277/// ```
1278#[macro_export]
1279macro_rules! stops {
1280    // match single entry that is a single color with 2 stops, e.g.:
1281    // stops![(colors::RED, 0, 20)]
1282    (($color:expr, $stop0:expr, $stop1:expr) $(,)?) => {
1283        $crate::__stops! {
1284            start: ($color, $stop0),
1285            middle: [],
1286            tail: ($color, $stop1)
1287        }
1288    };
1289    // match first entry as single color with 2 stops, plus other stops, e.g:
1290    // stops![(colors::RED, 0, 20), colors::WHITE]
1291    (($color:expr, $stop0:expr, $stop1:expr), $($stops:tt)+) => {
1292        $crate::__stops! {
1293            start: ($color, $stop0),
1294            middle: [($color, $stop1)],
1295            tail: $($stops)+
1296        }
1297    };
1298    ($start:expr, $($stops:tt)+) => {
1299        $crate::__stops! {
1300            start: $start,
1301            middle: [],
1302            tail: $($stops)+
1303        }
1304    };
1305}
1306#[doc(inline)]
1307pub use stops;
1308
1309#[cfg(test)]
1310mod tests {
1311    use zng_app_context::{AppId, LocalContext};
1312
1313    use super::*;
1314
1315    #[test]
1316    fn stops_simple_2() {
1317        let stops = stops![colors::BLACK, colors::WHITE];
1318
1319        assert!(stops.start.is_positional());
1320        assert_eq!(stops.start.color, colors::BLACK);
1321
1322        assert!(stops.middle.is_empty());
1323
1324        assert!(stops.end.is_positional());
1325        assert_eq!(stops.end.color, colors::WHITE);
1326    }
1327
1328    fn test_layout_stops(stops: GradientStops) -> Vec<RenderGradientStop> {
1329        let _app = LocalContext::start_app(AppId::new_unique());
1330
1331        let mut render_stops = vec![];
1332
1333        let metrics = LayoutMetrics::new(1.fct(), PxSize::new(Px(100), Px(100)), Px(0));
1334        LAYOUT.with_context(metrics, || {
1335            stops.layout_linear(
1336                LayoutAxis::X,
1337                ExtendMode::Clamp,
1338                &mut PxLine::new(PxPoint::zero(), PxPoint::new(Px(100), Px(100))),
1339                &mut render_stops,
1340            );
1341        });
1342
1343        render_stops
1344    }
1345
1346    #[test]
1347    fn positional_end_stops() {
1348        let stops = test_layout_stops(stops![colors::BLACK, colors::WHITE]);
1349        assert_eq!(stops.len(), 2);
1350
1351        assert_eq!(
1352            stops[0],
1353            RenderGradientStop {
1354                color: colors::BLACK,
1355                offset: 0.0
1356            }
1357        );
1358        assert_eq!(
1359            stops[1],
1360            RenderGradientStop {
1361                color: colors::WHITE,
1362                offset: 1.0
1363            }
1364        );
1365    }
1366
1367    #[test]
1368    fn single_color_2_stops_only() {
1369        let stops = stops![(colors::BLACK, 0, 100.pct())];
1370
1371        assert_eq!(stops.start, ColorStop::new(colors::BLACK, 0));
1372        assert!(stops.middle.is_empty());
1373        assert_eq!(stops.end, ColorStop::new(colors::BLACK, 100.pct()));
1374    }
1375
1376    #[test]
1377    fn single_color_2_stops_at_start() {
1378        let stops = stops![(colors::BLACK, 0, 50.pct()), colors::WHITE];
1379
1380        assert_eq!(stops.start, ColorStop::new(colors::BLACK, 0));
1381        assert_eq!(stops.middle.len(), 1);
1382        assert_eq!(stops.middle[0], GradientStop::Color(ColorStop::new(colors::BLACK, 50.pct())));
1383        assert_eq!(stops.end, ColorStop::new_positional(colors::WHITE));
1384    }
1385
1386    #[test]
1387    fn single_color_2_stops_at_middle() {
1388        let stops = stops![colors::BLACK, (colors::RED, 10.pct(), 90.pct()), colors::WHITE];
1389
1390        assert_eq!(stops.start, ColorStop::new_positional(colors::BLACK));
1391        assert_eq!(stops.middle.len(), 2);
1392        assert_eq!(stops.middle[0], GradientStop::Color(ColorStop::new(colors::RED, 10.pct())));
1393        assert_eq!(stops.middle[1], GradientStop::Color(ColorStop::new(colors::RED, 90.pct())));
1394        assert_eq!(stops.end, ColorStop::new_positional(colors::WHITE));
1395    }
1396
1397    #[test]
1398    fn single_color_2_stops_at_end() {
1399        let stops = stops![colors::BLACK, (colors::WHITE, 10.pct(), 50.pct())];
1400
1401        assert_eq!(stops.start, ColorStop::new_positional(colors::BLACK));
1402        assert_eq!(stops.middle.len(), 1);
1403        assert_eq!(stops.middle[0], GradientStop::Color(ColorStop::new(colors::WHITE, 10.pct())));
1404        assert_eq!(stops.end, ColorStop::new(colors::WHITE, 50.pct()));
1405    }
1406
1407    #[test]
1408    fn color_hint() {
1409        let stops = stops![colors::BLACK, 30.pct(), colors::WHITE];
1410        assert_eq!(stops.middle.len(), 1);
1411        assert_eq!(stops.middle[0], GradientStop::ColorHint(30.pct().into()));
1412    }
1413
1414    #[test]
1415    fn color_hint_layout() {
1416        let stops = test_layout_stops(stops![colors::BLACK, 30.pct(), colors::WHITE]);
1417        assert_eq!(stops.len(), 3);
1418        assert_eq!(
1419            stops[0],
1420            RenderGradientStop {
1421                color: colors::BLACK,
1422                offset: 0.0
1423            }
1424        );
1425        assert_eq!(
1426            stops[1],
1427            RenderGradientStop {
1428                color: Rgba::new(0.5, 0.5, 0.5, 1.0),
1429                offset: 30.0 / 100.0
1430            }
1431        );
1432        assert_eq!(
1433            stops[2],
1434            RenderGradientStop {
1435                color: colors::WHITE,
1436                offset: 1.0
1437            }
1438        );
1439    }
1440}