Skip to main content

cranpose_ui_graphics/
brush.rs

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