1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::display_item as di;
use crate::units::*;


/// Construct a gradient to be used in display lists.
///
/// Each gradient needs at least two stops.
pub struct GradientBuilder {
    stops: Vec<di::GradientStop>,
}

impl GradientBuilder {
    /// Create a new gradient builder.
    pub fn new() -> Self {
        GradientBuilder {
            stops: Vec::new(),
        }
    }

    /// Create a gradient builder with a list of stops.
    pub fn with_stops(stops: Vec<di::GradientStop>) -> GradientBuilder {
        GradientBuilder { stops }
    }

    /// Push an additional stop for the gradient.
    pub fn push(&mut self, stop: di::GradientStop) {
        self.stops.push(stop);
    }

    /// Get a reference to the list of stops.
    pub fn stops(&self) -> &[di::GradientStop] {
        self.stops.as_ref()
    }

    /// Return the gradient stops vector.
    pub fn into_stops(self) -> Vec<di::GradientStop> {
        self.stops
    }

    /// Produce a linear gradient, normalize the stops.
    pub fn gradient(
        &mut self,
        start_point: LayoutPoint,
        end_point: LayoutPoint,
        extend_mode: di::ExtendMode,
    ) -> di::Gradient {
        let (start_offset, end_offset) = self.normalize(extend_mode);
        let start_to_end = end_point - start_point;

        di::Gradient {
            start_point: start_point + start_to_end * start_offset,
            end_point: start_point + start_to_end * end_offset,
            extend_mode,
        }
    }

    /// Produce a radial gradient, normalize the stops.
    ///
    /// Will replace the gradient with a single color
    /// if the radius negative.
    pub fn radial_gradient(
        &mut self,
        center: LayoutPoint,
        radius: LayoutSize,
        extend_mode: di::ExtendMode,
    ) -> di::RadialGradient {
        if radius.width <= 0.0 || radius.height <= 0.0 {
            // The shader cannot handle a non positive radius. So
            // reuse the stops vector and construct an equivalent
            // gradient.
            let last_color = self.stops.last().unwrap().color;

            self.stops.clear();
            self.stops.push(di::GradientStop { offset: 0.0, color: last_color, });
            self.stops.push(di::GradientStop { offset: 1.0, color: last_color, });

            return di::RadialGradient {
                center,
                radius: LayoutSize::new(1.0, 1.0),
                start_offset: 0.0,
                end_offset: 1.0,
                extend_mode,
            };
        }

        let (start_offset, end_offset) =
            self.normalize(extend_mode);

        di::RadialGradient {
            center,
            radius,
            start_offset,
            end_offset,
            extend_mode,
        }
    }

    /// Produce a conic gradient, normalize the stops.
    pub fn conic_gradient(
        &mut self,
        center: LayoutPoint,
        angle: f32,
        extend_mode: di::ExtendMode,
    ) -> di::ConicGradient {
        let (start_offset, end_offset) =
            self.normalize(extend_mode);

        di::ConicGradient {
            center,
            angle,
            start_offset,
            end_offset,
            extend_mode,
        }
    }

    /// Gradients can be defined with stops outside the range of [0, 1]
    /// when this happens the gradient needs to be normalized by adjusting
    /// the gradient stops and gradient line into an equivalent gradient
    /// with stops in the range [0, 1]. this is done by moving the beginning
    /// of the gradient line to where stop[0] and the end of the gradient line
    /// to stop[n-1]. this function adjusts the stops in place, and returns
    /// the amount to adjust the gradient line start and stop.
    fn normalize(&mut self, extend_mode: di::ExtendMode) -> (f32, f32) {
        let stops = &mut self.stops;
        assert!(stops.len() >= 2);

        let first = *stops.first().unwrap();
        let last = *stops.last().unwrap();

        let stops_delta = last.offset - first.offset;

        if stops_delta > 0.000001 {
            for stop in stops {
                stop.offset = (stop.offset - first.offset) / stops_delta;
            }

            (first.offset, last.offset)
        } else if stops_delta.is_nan() {
            // We have no good way to render a NaN offset, but make something
            // that is at least renderable.
            stops.clear();
            stops.push(di::GradientStop { color: last.color, offset: 0.0, });
            stops.push(di::GradientStop { color: last.color, offset: 1.0, });

            (0.0, 1.0)
        } else {
            // We have a degenerate gradient and can't accurately transform the stops
            // what happens here depends on the repeat behavior, but in any case
            // we reconstruct the gradient stops to something simpler and equivalent
            stops.clear();

            match extend_mode {
                di::ExtendMode::Clamp => {
                    // This gradient is two colors split at the offset of the stops,
                    // so create a gradient with two colors split at 0.5 and adjust
                    // the gradient line so 0.5 is at the offset of the stops
                    stops.push(di::GradientStop { color: first.color, offset: 0.0, });
                    stops.push(di::GradientStop { color: first.color, offset: 0.5, });
                    stops.push(di::GradientStop { color: last.color, offset: 0.5, });
                    stops.push(di::GradientStop { color: last.color, offset: 1.0, });

                    let offset = last.offset;

                    (offset - 0.5, offset + 0.5)
                }
                di::ExtendMode::Repeat => {
                    // A repeating gradient with stops that are all in the same
                    // position should just display the last color. I believe the
                    // spec says that it should be the average color of the gradient,
                    // but this matches what Gecko and Blink does
                    stops.push(di::GradientStop { color: last.color, offset: 0.0, });
                    stops.push(di::GradientStop { color: last.color, offset: 1.0, });

                    (0.0, 1.0)
                }
            }
        }
    }
}