1use 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 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 #[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 #[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 #[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 #[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}