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    #[test]
240    fn an_axis_gradient_varies_along_its_own_axis_only() {
241        let vertical = Brush::vertical_gradient(red_to_blue(), 10.0, 90.0);
242        let (colors, stops, start, end, tile_mode) = linear_parts(&vertical);
243        assert_eq!(colors, &red_to_blue());
244        assert!(stops.is_none());
245        assert_eq!(start, Point { x: 0.0, y: 10.0 });
246        assert_eq!(end, Point { x: 0.0, y: 90.0 });
247        assert_eq!(tile_mode, TileMode::Clamp);
248
249        let horizontal = Brush::horizontal_gradient(red_to_blue(), 10.0, 90.0);
250        let (_, _, start, end, _) = linear_parts(&horizontal);
251        assert_eq!(start, Point { x: 10.0, y: 0.0 });
252        assert_eq!(end, Point { x: 90.0, y: 0.0 });
253    }
254
255    #[test]
256    fn a_default_axis_gradient_spans_whatever_it_is_painted_into() {
257        for brush in [
258            Brush::vertical_gradient_default(red_to_blue()),
259            Brush::horizontal_gradient_default(red_to_blue()),
260        ] {
261            let (_, _, start, end, tile_mode) = linear_parts(&brush);
262            assert_eq!(start, Point { x: 0.0, y: 0.0 });
263            assert!(end.x.is_infinite() || end.y.is_infinite());
264            assert_eq!(tile_mode, TileMode::Clamp);
265        }
266    }
267
268    #[test]
269    fn a_tiled_axis_gradient_carries_its_tile_mode() {
270        let vertical = Brush::vertical_gradient_tiled(red_to_blue(), 0.0, 8.0, TileMode::Repeated);
271        let (_, _, _, _, tile_mode) = linear_parts(&vertical);
272        assert_eq!(tile_mode, TileMode::Repeated);
273
274        let horizontal =
275            Brush::horizontal_gradient_tiled(red_to_blue(), 0.0, 8.0, TileMode::Mirror);
276        let (_, _, _, _, tile_mode) = linear_parts(&horizontal);
277        assert_eq!(tile_mode, TileMode::Mirror);
278    }
279
280    #[test]
281    fn explicit_stops_travel_beside_their_colours() {
282        let stops_in = vec![
283            (0.0, Color(1.0, 0.0, 0.0, 1.0)),
284            (0.8, Color(0.0, 1.0, 0.0, 1.0)),
285            (1.0, Color(0.0, 0.0, 1.0, 1.0)),
286        ];
287        let expected_colors: Vec<Color> = stops_in.iter().map(|(_, color)| *color).collect();
288        let expected_positions: Vec<f32> = stops_in.iter().map(|(at, _)| *at).collect();
289
290        let vertical =
291            Brush::vertical_gradient_stops(stops_in.clone(), 0.0, 100.0, TileMode::Clamp);
292        let (colors, stops, start, end, _) = linear_parts(&vertical);
293        assert_eq!(colors, &expected_colors);
294        assert_eq!(stops.as_ref(), Some(&expected_positions));
295        assert_eq!(start, Point { x: 0.0, y: 0.0 });
296        assert_eq!(end, Point { x: 0.0, y: 100.0 });
297
298        let horizontal =
299            Brush::horizontal_gradient_stops(stops_in.clone(), 0.0, 100.0, TileMode::Clamp);
300        let (colors, stops, start, end, _) = linear_parts(&horizontal);
301        assert_eq!(colors, &expected_colors);
302        assert_eq!(stops.as_ref(), Some(&expected_positions));
303        assert_eq!(start, Point { x: 0.0, y: 0.0 });
304        assert_eq!(end, Point { x: 100.0, y: 0.0 });
305
306        let center = Point { x: 5.0, y: 5.0 };
307        match Brush::radial_gradient_stops(stops_in.clone(), center, 20.0, TileMode::Repeated) {
308            Brush::RadialGradient {
309                colors,
310                stops,
311                center: at,
312                radius,
313                tile_mode,
314            } => {
315                assert_eq!(colors, expected_colors);
316                assert_eq!(stops, Some(expected_positions.clone()));
317                assert_eq!(at, center);
318                assert_eq!(radius, 20.0);
319                assert_eq!(tile_mode, TileMode::Repeated);
320            }
321            other => panic!("expected RadialGradient, got {other:?}"),
322        }
323
324        match Brush::sweep_gradient_stops(stops_in, center) {
325            Brush::SweepGradient {
326                colors,
327                stops,
328                center: at,
329            } => {
330                assert_eq!(colors, expected_colors);
331                assert_eq!(stops, Some(expected_positions));
332                assert_eq!(at, center);
333            }
334            other => panic!("expected SweepGradient, got {other:?}"),
335        }
336    }
337
338    #[test]
339    fn a_tiled_radial_gradient_carries_its_tile_mode_and_geometry() {
340        let center = Point { x: 12.0, y: 34.0 };
341        match Brush::radial_gradient_tiled(red_to_blue(), center, 7.5, TileMode::Mirror) {
342            Brush::RadialGradient {
343                colors,
344                stops,
345                center: at,
346                radius,
347                tile_mode,
348            } => {
349                assert_eq!(colors, red_to_blue());
350                assert!(stops.is_none());
351                assert_eq!(at, center);
352                assert_eq!(radius, 7.5);
353                assert_eq!(tile_mode, TileMode::Mirror);
354            }
355            other => panic!("expected RadialGradient, got {other:?}"),
356        }
357    }
358
359    #[test]
360    fn sweep_gradient_construction() {
361        let colors = vec![Color(1.0, 0.0, 0.0, 1.0), Color(0.0, 0.0, 1.0, 1.0)];
362        let center = Point { x: 50.0, y: 50.0 };
363        let brush = Brush::sweep_gradient(colors.clone(), center);
364        match brush {
365            Brush::SweepGradient {
366                colors: c,
367                stops,
368                center: p,
369            } => {
370                assert_eq!(c, colors);
371                assert_eq!(p, center);
372                assert!(stops.is_none());
373            }
374            _ => panic!("expected SweepGradient"),
375        }
376    }
377
378    #[test]
379    fn brush_clone_eq() {
380        let a = Brush::solid(Color(1.0, 0.0, 0.0, 1.0));
381        let b = a.clone();
382        assert_eq!(a, b);
383    }
384
385    #[test]
386    fn vertical_gradient_construction() {
387        let colors = vec![Color(0.0, 0.0, 0.0, 1.0), Color(0.0, 0.0, 0.0, 0.0)];
388        let brush = Brush::vertical_gradient(colors.clone(), 24.0, 64.0);
389        match brush {
390            Brush::LinearGradient {
391                colors: c,
392                stops,
393                start,
394                end,
395                tile_mode,
396            } => {
397                assert_eq!(c, colors);
398                assert!(stops.is_none());
399                assert_eq!(tile_mode, TileMode::Clamp);
400                assert_eq!(start, Point { x: 0.0, y: 24.0 });
401                assert_eq!(end, Point { x: 0.0, y: 64.0 });
402            }
403            _ => panic!("expected LinearGradient"),
404        }
405    }
406
407    #[test]
408    fn linear_gradient_defaults_to_infinite_end() {
409        let brush = Brush::linear_gradient(vec![Color::BLACK, Color::WHITE]);
410        match brush {
411            Brush::LinearGradient { start, end, .. } => {
412                assert_eq!(start, Point { x: 0.0, y: 0.0 });
413                assert!(end.x.is_infinite());
414                assert!(end.y.is_infinite());
415            }
416            _ => panic!("expected LinearGradient"),
417        }
418    }
419
420    #[test]
421    fn gradient_color_stops_are_stored() {
422        let brush = Brush::linear_gradient_stops(
423            vec![(0.0, Color::RED), (0.6, Color::GREEN), (1.0, Color::BLUE)],
424            Point { x: 0.0, y: 0.0 },
425            Point { x: 20.0, y: 10.0 },
426            TileMode::Mirror,
427        );
428
429        match brush {
430            Brush::LinearGradient {
431                colors,
432                stops,
433                tile_mode,
434                ..
435            } => {
436                assert_eq!(colors, vec![Color::RED, Color::GREEN, Color::BLUE]);
437                assert_eq!(stops, Some(vec![0.0, 0.6, 1.0]));
438                assert_eq!(tile_mode, TileMode::Mirror);
439            }
440            _ => panic!("expected LinearGradient"),
441        }
442    }
443}