Skip to main content

cranpose_ui_graphics/
brush.rs

1//! Brush definitions for painting (solid colors, gradients, etc.)
2
3use crate::color::Color;
4use crate::geometry::Point;
5use crate::render_effect::TileMode;
6
7#[derive(Clone, Debug, PartialEq)]
8pub enum Brush {
9    Solid(Color),
10    LinearGradient {
11        colors: Vec<Color>,
12        stops: Option<Vec<f32>>,
13        start: Point,
14        end: Point,
15        tile_mode: TileMode,
16    },
17    RadialGradient {
18        colors: Vec<Color>,
19        stops: Option<Vec<f32>>,
20        center: Point,
21        radius: f32,
22        tile_mode: TileMode,
23    },
24    SweepGradient {
25        colors: Vec<Color>,
26        stops: Option<Vec<f32>>,
27        center: Point,
28    },
29}
30
31fn split_color_stops(color_stops: Vec<(f32, Color)>) -> (Vec<Color>, Vec<f32>) {
32    let mut colors = Vec::with_capacity(color_stops.len());
33    let mut stops = Vec::with_capacity(color_stops.len());
34    for (stop, color) in color_stops {
35        colors.push(color);
36        stops.push(stop);
37    }
38    (colors, stops)
39}
40
41impl Brush {
42    pub fn solid(color: Color) -> Self {
43        Brush::Solid(color)
44    }
45
46    /// Creates a linear gradient that defaults to Compose semantics:
47    /// start at `(0,0)`, end at `(+inf,+inf)`, and `TileMode::Clamp`.
48    pub fn linear_gradient(colors: Vec<Color>) -> Self {
49        Self::linear_gradient_with_tile_mode(
50            colors,
51            Point { x: 0.0, y: 0.0 },
52            Point {
53                x: f32::INFINITY,
54                y: f32::INFINITY,
55            },
56            TileMode::Clamp,
57        )
58    }
59
60    pub fn linear_gradient_range(colors: Vec<Color>, start: Point, end: Point) -> Self {
61        Self::linear_gradient_with_tile_mode(colors, start, end, TileMode::Clamp)
62    }
63
64    pub fn linear_gradient_with_tile_mode(
65        colors: Vec<Color>,
66        start: Point,
67        end: Point,
68        tile_mode: TileMode,
69    ) -> Self {
70        Brush::LinearGradient {
71            colors,
72            stops: None,
73            start,
74            end,
75            tile_mode,
76        }
77    }
78
79    pub fn linear_gradient_stops(
80        color_stops: Vec<(f32, Color)>,
81        start: Point,
82        end: Point,
83        tile_mode: TileMode,
84    ) -> Self {
85        let (colors, stops) = split_color_stops(color_stops);
86        Brush::LinearGradient {
87            colors,
88            stops: Some(stops),
89            start,
90            end,
91            tile_mode,
92        }
93    }
94
95    pub fn vertical_gradient(colors: Vec<Color>, start_y: f32, end_y: f32) -> Self {
96        Self::vertical_gradient_tiled(colors, start_y, end_y, TileMode::Clamp)
97    }
98
99    pub fn vertical_gradient_tiled(
100        colors: Vec<Color>,
101        start_y: f32,
102        end_y: f32,
103        tile_mode: TileMode,
104    ) -> Self {
105        Self::linear_gradient_with_tile_mode(
106            colors,
107            Point { x: 0.0, y: start_y },
108            Point { x: 0.0, y: end_y },
109            tile_mode,
110        )
111    }
112
113    pub fn vertical_gradient_default(colors: Vec<Color>) -> Self {
114        Self::vertical_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
115    }
116
117    pub fn vertical_gradient_stops(
118        color_stops: Vec<(f32, Color)>,
119        start_y: f32,
120        end_y: f32,
121        tile_mode: TileMode,
122    ) -> Self {
123        Self::linear_gradient_stops(
124            color_stops,
125            Point { x: 0.0, y: start_y },
126            Point { x: 0.0, y: end_y },
127            tile_mode,
128        )
129    }
130
131    pub fn horizontal_gradient(colors: Vec<Color>, start_x: f32, end_x: f32) -> Self {
132        Self::horizontal_gradient_tiled(colors, start_x, end_x, TileMode::Clamp)
133    }
134
135    pub fn horizontal_gradient_tiled(
136        colors: Vec<Color>,
137        start_x: f32,
138        end_x: f32,
139        tile_mode: TileMode,
140    ) -> Self {
141        Self::linear_gradient_with_tile_mode(
142            colors,
143            Point { x: start_x, y: 0.0 },
144            Point { x: end_x, y: 0.0 },
145            tile_mode,
146        )
147    }
148
149    pub fn horizontal_gradient_default(colors: Vec<Color>) -> Self {
150        Self::horizontal_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
151    }
152
153    pub fn horizontal_gradient_stops(
154        color_stops: Vec<(f32, Color)>,
155        start_x: f32,
156        end_x: f32,
157        tile_mode: TileMode,
158    ) -> Self {
159        Self::linear_gradient_stops(
160            color_stops,
161            Point { x: start_x, y: 0.0 },
162            Point { x: end_x, y: 0.0 },
163            tile_mode,
164        )
165    }
166
167    pub fn radial_gradient(colors: Vec<Color>, center: Point, radius: f32) -> Self {
168        Self::radial_gradient_tiled(colors, center, radius, TileMode::Clamp)
169    }
170
171    pub fn radial_gradient_tiled(
172        colors: Vec<Color>,
173        center: Point,
174        radius: f32,
175        tile_mode: TileMode,
176    ) -> Self {
177        Brush::RadialGradient {
178            colors,
179            stops: None,
180            center,
181            radius,
182            tile_mode,
183        }
184    }
185
186    pub fn radial_gradient_stops(
187        color_stops: Vec<(f32, Color)>,
188        center: Point,
189        radius: f32,
190        tile_mode: TileMode,
191    ) -> Self {
192        let (colors, stops) = split_color_stops(color_stops);
193        Brush::RadialGradient {
194            colors,
195            stops: Some(stops),
196            center,
197            radius,
198            tile_mode,
199        }
200    }
201
202    pub fn sweep_gradient(colors: Vec<Color>, center: Point) -> Self {
203        Brush::SweepGradient {
204            colors,
205            stops: None,
206            center,
207        }
208    }
209
210    pub fn sweep_gradient_stops(color_stops: Vec<(f32, Color)>, center: Point) -> Self {
211        let (colors, stops) = split_color_stops(color_stops);
212        Brush::SweepGradient {
213            colors,
214            stops: Some(stops),
215            center,
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    fn red_to_blue() -> Vec<Color> {
225        vec![Color(1.0, 0.0, 0.0, 1.0), Color(0.0, 0.0, 1.0, 1.0)]
226    }
227
228    fn linear_parts(brush: &Brush) -> (&Vec<Color>, &Option<Vec<f32>>, Point, Point, TileMode) {
229        match brush {
230            Brush::LinearGradient {
231                colors,
232                stops,
233                start,
234                end,
235                tile_mode,
236            } => (colors, stops, *start, *end, *tile_mode),
237            other => panic!("expected LinearGradient, got {other:?}"),
238        }
239    }
240
241    /// An axis-aligned gradient is the common case, and getting the axis wrong
242    /// is invisible until a two-colour fill runs the wrong way. Each helper has
243    /// to pin its own axis to zero and vary only the other.
244    #[test]
245    fn an_axis_gradient_varies_along_its_own_axis_only() {
246        let vertical = Brush::vertical_gradient(red_to_blue(), 10.0, 90.0);
247        let (colors, stops, start, end, tile_mode) = linear_parts(&vertical);
248        assert_eq!(colors, &red_to_blue());
249        assert!(stops.is_none());
250        assert_eq!(start, Point { x: 0.0, y: 10.0 });
251        assert_eq!(end, Point { x: 0.0, y: 90.0 });
252        assert_eq!(tile_mode, TileMode::Clamp);
253
254        let horizontal = Brush::horizontal_gradient(red_to_blue(), 10.0, 90.0);
255        let (_, _, start, end, _) = linear_parts(&horizontal);
256        assert_eq!(start, Point { x: 10.0, y: 0.0 });
257        assert_eq!(end, Point { x: 90.0, y: 0.0 });
258    }
259
260    /// The default form has no length of its own: it runs from the origin to
261    /// infinity so the renderer fits it to whatever it is painted into.
262    #[test]
263    fn a_default_axis_gradient_spans_whatever_it_is_painted_into() {
264        for brush in [
265            Brush::vertical_gradient_default(red_to_blue()),
266            Brush::horizontal_gradient_default(red_to_blue()),
267        ] {
268            let (_, _, start, end, tile_mode) = linear_parts(&brush);
269            assert_eq!(start, Point { x: 0.0, y: 0.0 });
270            assert!(end.x.is_infinite() || end.y.is_infinite());
271            assert_eq!(tile_mode, TileMode::Clamp);
272        }
273    }
274
275    /// A tiled gradient repeats instead of holding its end colours, which is
276    /// how a repeating stripe or a shimmer is drawn.
277    #[test]
278    fn a_tiled_axis_gradient_carries_its_tile_mode() {
279        let vertical = Brush::vertical_gradient_tiled(red_to_blue(), 0.0, 8.0, TileMode::Repeated);
280        let (_, _, _, _, tile_mode) = linear_parts(&vertical);
281        assert_eq!(tile_mode, TileMode::Repeated);
282
283        let horizontal =
284            Brush::horizontal_gradient_tiled(red_to_blue(), 0.0, 8.0, TileMode::Mirror);
285        let (_, _, _, _, tile_mode) = linear_parts(&horizontal);
286        assert_eq!(tile_mode, TileMode::Mirror);
287    }
288
289    /// Explicit stops are what an uneven gradient needs — most of the run in
290    /// one colour and a band at the end. The positions travel beside the
291    /// colours rather than being inferred from how many there are.
292    #[test]
293    fn explicit_stops_travel_beside_their_colours() {
294        let stops_in = vec![
295            (0.0, Color(1.0, 0.0, 0.0, 1.0)),
296            (0.8, Color(0.0, 1.0, 0.0, 1.0)),
297            (1.0, Color(0.0, 0.0, 1.0, 1.0)),
298        ];
299        let expected_colors: Vec<Color> = stops_in.iter().map(|(_, color)| *color).collect();
300        let expected_positions: Vec<f32> = stops_in.iter().map(|(at, _)| *at).collect();
301
302        let vertical =
303            Brush::vertical_gradient_stops(stops_in.clone(), 0.0, 100.0, TileMode::Clamp);
304        let (colors, stops, start, end, _) = linear_parts(&vertical);
305        assert_eq!(colors, &expected_colors);
306        assert_eq!(stops.as_ref(), Some(&expected_positions));
307        assert_eq!(start, Point { x: 0.0, y: 0.0 });
308        assert_eq!(end, Point { x: 0.0, y: 100.0 });
309
310        let horizontal =
311            Brush::horizontal_gradient_stops(stops_in.clone(), 0.0, 100.0, TileMode::Clamp);
312        let (colors, stops, start, end, _) = linear_parts(&horizontal);
313        assert_eq!(colors, &expected_colors);
314        assert_eq!(stops.as_ref(), Some(&expected_positions));
315        assert_eq!(start, Point { x: 0.0, y: 0.0 });
316        assert_eq!(end, Point { x: 100.0, y: 0.0 });
317
318        let center = Point { x: 5.0, y: 5.0 };
319        match Brush::radial_gradient_stops(stops_in.clone(), center, 20.0, TileMode::Repeated) {
320            Brush::RadialGradient {
321                colors,
322                stops,
323                center: at,
324                radius,
325                tile_mode,
326            } => {
327                assert_eq!(colors, expected_colors);
328                assert_eq!(stops, Some(expected_positions.clone()));
329                assert_eq!(at, center);
330                assert_eq!(radius, 20.0);
331                assert_eq!(tile_mode, TileMode::Repeated);
332            }
333            other => panic!("expected RadialGradient, got {other:?}"),
334        }
335
336        match Brush::sweep_gradient_stops(stops_in, center) {
337            Brush::SweepGradient {
338                colors,
339                stops,
340                center: at,
341            } => {
342                assert_eq!(colors, expected_colors);
343                assert_eq!(stops, Some(expected_positions));
344                assert_eq!(at, center);
345            }
346            other => panic!("expected SweepGradient, got {other:?}"),
347        }
348    }
349
350    #[test]
351    fn a_tiled_radial_gradient_carries_its_tile_mode_and_geometry() {
352        let center = Point { x: 12.0, y: 34.0 };
353        match Brush::radial_gradient_tiled(red_to_blue(), center, 7.5, TileMode::Mirror) {
354            Brush::RadialGradient {
355                colors,
356                stops,
357                center: at,
358                radius,
359                tile_mode,
360            } => {
361                assert_eq!(colors, red_to_blue());
362                assert!(stops.is_none());
363                assert_eq!(at, center);
364                assert_eq!(radius, 7.5);
365                assert_eq!(tile_mode, TileMode::Mirror);
366            }
367            other => panic!("expected RadialGradient, got {other:?}"),
368        }
369    }
370
371    #[test]
372    fn sweep_gradient_construction() {
373        let colors = vec![Color(1.0, 0.0, 0.0, 1.0), Color(0.0, 0.0, 1.0, 1.0)];
374        let center = Point { x: 50.0, y: 50.0 };
375        let brush = Brush::sweep_gradient(colors.clone(), center);
376        match brush {
377            Brush::SweepGradient {
378                colors: c,
379                stops,
380                center: p,
381            } => {
382                assert_eq!(c, colors);
383                assert_eq!(p, center);
384                assert!(stops.is_none());
385            }
386            _ => panic!("expected SweepGradient"),
387        }
388    }
389
390    #[test]
391    fn brush_clone_eq() {
392        let a = Brush::solid(Color(1.0, 0.0, 0.0, 1.0));
393        let b = a.clone();
394        assert_eq!(a, b);
395    }
396
397    #[test]
398    fn vertical_gradient_construction() {
399        let colors = vec![Color(0.0, 0.0, 0.0, 1.0), Color(0.0, 0.0, 0.0, 0.0)];
400        let brush = Brush::vertical_gradient(colors.clone(), 24.0, 64.0);
401        match brush {
402            Brush::LinearGradient {
403                colors: c,
404                stops,
405                start,
406                end,
407                tile_mode,
408            } => {
409                assert_eq!(c, colors);
410                assert!(stops.is_none());
411                assert_eq!(tile_mode, TileMode::Clamp);
412                assert_eq!(start, Point { x: 0.0, y: 24.0 });
413                assert_eq!(end, Point { x: 0.0, y: 64.0 });
414            }
415            _ => panic!("expected LinearGradient"),
416        }
417    }
418
419    #[test]
420    fn linear_gradient_defaults_to_infinite_end() {
421        let brush = Brush::linear_gradient(vec![Color::BLACK, Color::WHITE]);
422        match brush {
423            Brush::LinearGradient { start, end, .. } => {
424                assert_eq!(start, Point { x: 0.0, y: 0.0 });
425                assert!(end.x.is_infinite());
426                assert!(end.y.is_infinite());
427            }
428            _ => panic!("expected LinearGradient"),
429        }
430    }
431
432    #[test]
433    fn gradient_color_stops_are_stored() {
434        let brush = Brush::linear_gradient_stops(
435            vec![(0.0, Color::RED), (0.6, Color::GREEN), (1.0, Color::BLUE)],
436            Point { x: 0.0, y: 0.0 },
437            Point { x: 20.0, y: 10.0 },
438            TileMode::Mirror,
439        );
440
441        match brush {
442            Brush::LinearGradient {
443                colors,
444                stops,
445                tile_mode,
446                ..
447            } => {
448                assert_eq!(colors, vec![Color::RED, Color::GREEN, Color::BLUE]);
449                assert_eq!(stops, Some(vec![0.0, 0.6, 1.0]));
450                assert_eq!(tile_mode, TileMode::Mirror);
451            }
452            _ => panic!("expected LinearGradient"),
453        }
454    }
455}