cranpose_ui_graphics/brush.rs
1//! Brush definitions for painting (solid colors, gradients, etc.)
2
3use std::sync::Arc;
4
5use crate::{
6 brush_sampler::BrushSampler,
7 color::Color,
8 geometry::{Point, Rect},
9 render_effect::TileMode,
10};
11
12/// Describes how a shape, path or text is filled: a flat color or a gradient. Compose's `Brush`.
13#[derive(Clone, Debug, PartialEq)]
14pub enum Brush {
15 /// A single flat color. Compose's `SolidColor`.
16 Solid(Color),
17 /// Colors blend along the line from `start` to `end`. Compose's `Brush.linearGradient`.
18 LinearGradient {
19 /// Colors to blend between, in order, from `start` to `end`.
20 colors: Arc<[Color]>,
21 /// Positions of `colors` along the line from `start` to `end`, from 0.0 to 1.0; `None`
22 /// spaces the colors evenly.
23 stops: Option<Arc<[f32]>>,
24 /// Where the first color is reached, in logical pixels relative to the top-left corner of
25 /// the painted area. An infinite coordinate means the far edge of that area.
26 start: Point,
27 /// Where the last color is reached, in logical pixels relative to the top-left corner of
28 /// the painted area. An infinite coordinate means the far edge of that area.
29 end: Point,
30 /// How the gradient continues beyond `start` and `end`.
31 tile_mode: TileMode,
32 },
33 /// Colors blend with distance from `center`. Compose's `Brush.radialGradient`.
34 RadialGradient {
35 /// Colors to blend between, in order, from `center` outward.
36 colors: Arc<[Color]>,
37 /// Positions of `colors` from 0.0 to 1.0; `None` spaces the colors evenly.
38 stops: Option<Arc<[f32]>>,
39 /// Gradient center in logical pixels, relative to the top-left corner of the painted area.
40 center: Point,
41 /// Distance in logical pixels from `center` at which the last color is reached.
42 radius: f32,
43 /// How the gradient continues beyond `radius`.
44 tile_mode: TileMode,
45 },
46 /// Colors blend by angle around `center`, the first at 3 o'clock and the
47 /// rest clockwise from it. Compose's `Brush.sweepGradient`.
48 SweepGradient {
49 /// Colors to blend between, in order.
50 colors: Arc<[Color]>,
51 /// Positions of `colors` from 0.0 to 1.0; `None` spaces the colors evenly.
52 stops: Option<Arc<[f32]>>,
53 /// Gradient center in logical pixels, relative to the top-left corner of the painted area.
54 center: Point,
55 },
56}
57
58fn split_color_stops(color_stops: Vec<(f32, Color)>) -> (Vec<Color>, Vec<f32>) {
59 let mut colors = Vec::with_capacity(color_stops.len());
60 let mut stops = Vec::with_capacity(color_stops.len());
61 for (stop, color) in color_stops {
62 colors.push(color);
63 stops.push(stop);
64 }
65 (colors, stops)
66}
67
68/// A colour paints as a solid brush wherever a brush is taken, as Compose
69/// takes a `Color` beside each `Brush` overload.
70impl From<Color> for Brush {
71 fn from(color: Color) -> Self {
72 Self::Solid(color)
73 }
74}
75
76impl Brush {
77 /// Creates a brush that paints one flat color.
78 pub fn solid(color: Color) -> Self {
79 Self::Solid(color)
80 }
81
82 /// The brush's colours: its one colour, or a gradient's stops in order.
83 pub fn colors(&self) -> &[Color] {
84 match self {
85 Self::Solid(color) => std::slice::from_ref(color),
86 Self::LinearGradient { colors, .. }
87 | Self::RadialGradient { colors, .. }
88 | Self::SweepGradient { colors, .. } => colors,
89 }
90 }
91
92 /// The brush with each colour through `paint`. A gradient keeps its colour
93 /// list, shared, when `paint` leaves every colour's bits as they are.
94 pub fn map_colors(self, paint: impl Fn(Color) -> Color) -> Self {
95 let map = |colors: Arc<[Color]>| {
96 let bits = |color: Color| [color.0, color.1, color.2, color.3].map(f32::to_bits);
97 if colors
98 .iter()
99 .all(|&color| bits(paint(color)) == bits(color))
100 {
101 colors
102 } else {
103 colors.iter().map(|&color| paint(color)).collect()
104 }
105 };
106 match self {
107 Self::Solid(color) => Self::Solid(paint(color)),
108 Self::LinearGradient {
109 colors,
110 stops,
111 start,
112 end,
113 tile_mode,
114 } => Self::LinearGradient {
115 colors: map(colors),
116 stops,
117 start,
118 end,
119 tile_mode,
120 },
121 Self::RadialGradient {
122 colors,
123 stops,
124 center,
125 radius,
126 tile_mode,
127 } => Self::RadialGradient {
128 colors: map(colors),
129 stops,
130 center,
131 radius,
132 tile_mode,
133 },
134 Self::SweepGradient {
135 colors,
136 stops,
137 center,
138 } => Self::SweepGradient {
139 colors: map(colors),
140 stops,
141 center,
142 },
143 }
144 }
145
146 /// Whether the brush paints anything: some colour of it is not
147 /// transparent.
148 pub fn paints(&self) -> bool {
149 self.colors().iter().any(|color| color.3 > 0.0)
150 }
151
152 /// Creates a linear gradient that defaults to Compose semantics:
153 /// start at `(0,0)`, end at `(+inf,+inf)`, and `TileMode::Clamp`.
154 pub fn linear_gradient(colors: Vec<Color>) -> Self {
155 Self::linear_gradient_with_tile_mode(
156 colors,
157 Point { x: 0.0, y: 0.0 },
158 Point {
159 x: f32::INFINITY,
160 y: f32::INFINITY,
161 },
162 TileMode::Clamp,
163 )
164 }
165
166 /// Creates a linear gradient that spreads `colors` evenly from `start` to `end`, clamping
167 /// outside that line.
168 pub fn linear_gradient_range(colors: Vec<Color>, start: Point, end: Point) -> Self {
169 Self::linear_gradient_with_tile_mode(colors, start, end, TileMode::Clamp)
170 }
171
172 /// Creates a linear gradient that spreads `colors` evenly from `start` to `end`, repeating or
173 /// clamping outside that line as `tile_mode` says.
174 pub fn linear_gradient_with_tile_mode(
175 colors: Vec<Color>,
176 start: Point,
177 end: Point,
178 tile_mode: TileMode,
179 ) -> Self {
180 Self::LinearGradient {
181 colors: colors.into(),
182 stops: None,
183 start,
184 end,
185 tile_mode,
186 }
187 }
188
189 /// Creates a linear gradient from `(position, color)` pairs, with positions from 0.0 at `start`
190 /// to 1.0 at `end`.
191 pub fn linear_gradient_stops(
192 color_stops: Vec<(f32, Color)>,
193 start: Point,
194 end: Point,
195 tile_mode: TileMode,
196 ) -> Self {
197 let (colors, stops) = split_color_stops(color_stops);
198 Self::LinearGradient {
199 colors: colors.into(),
200 stops: Some(stops.into()),
201 start,
202 end,
203 tile_mode,
204 }
205 }
206
207 /// Creates a vertical gradient that spreads `colors` evenly from `start_y` to `end_y`, clamping
208 /// outside that range.
209 pub fn vertical_gradient(colors: Vec<Color>, start_y: f32, end_y: f32) -> Self {
210 Self::vertical_gradient_tiled(colors, start_y, end_y, TileMode::Clamp)
211 }
212
213 /// Creates a vertical gradient that spreads `colors` evenly from `start_y` to `end_y`,
214 /// repeating or clamping outside that range as `tile_mode` says.
215 pub fn vertical_gradient_tiled(
216 colors: Vec<Color>,
217 start_y: f32,
218 end_y: f32,
219 tile_mode: TileMode,
220 ) -> Self {
221 Self::linear_gradient_with_tile_mode(
222 colors,
223 Point { x: 0.0, y: start_y },
224 Point { x: 0.0, y: end_y },
225 tile_mode,
226 )
227 }
228
229 /// Creates a vertical gradient across the full height of the painted area, clamping at both
230 /// ends.
231 pub fn vertical_gradient_default(colors: Vec<Color>) -> Self {
232 Self::vertical_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
233 }
234
235 /// Creates a vertical gradient from `(position, color)` pairs, with positions from 0.0 at
236 /// `start_y` to 1.0 at `end_y`.
237 pub fn vertical_gradient_stops(
238 color_stops: Vec<(f32, Color)>,
239 start_y: f32,
240 end_y: f32,
241 tile_mode: TileMode,
242 ) -> Self {
243 Self::linear_gradient_stops(
244 color_stops,
245 Point { x: 0.0, y: start_y },
246 Point { x: 0.0, y: end_y },
247 tile_mode,
248 )
249 }
250
251 /// Creates a horizontal gradient that spreads `colors` evenly from `start_x` to `end_x`,
252 /// clamping outside that range.
253 pub fn horizontal_gradient(colors: Vec<Color>, start_x: f32, end_x: f32) -> Self {
254 Self::horizontal_gradient_tiled(colors, start_x, end_x, TileMode::Clamp)
255 }
256
257 /// Creates a horizontal gradient that spreads `colors` evenly from `start_x` to `end_x`,
258 /// repeating or clamping outside that range as `tile_mode` says.
259 pub fn horizontal_gradient_tiled(
260 colors: Vec<Color>,
261 start_x: f32,
262 end_x: f32,
263 tile_mode: TileMode,
264 ) -> Self {
265 Self::linear_gradient_with_tile_mode(
266 colors,
267 Point { x: start_x, y: 0.0 },
268 Point { x: end_x, y: 0.0 },
269 tile_mode,
270 )
271 }
272
273 /// Creates a horizontal gradient across the full width of the painted area, clamping at both
274 /// ends.
275 pub fn horizontal_gradient_default(colors: Vec<Color>) -> Self {
276 Self::horizontal_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
277 }
278
279 /// Creates a horizontal gradient from `(position, color)` pairs, with positions from 0.0 at
280 /// `start_x` to 1.0 at `end_x`.
281 pub fn horizontal_gradient_stops(
282 color_stops: Vec<(f32, Color)>,
283 start_x: f32,
284 end_x: f32,
285 tile_mode: TileMode,
286 ) -> Self {
287 Self::linear_gradient_stops(
288 color_stops,
289 Point { x: start_x, y: 0.0 },
290 Point { x: end_x, y: 0.0 },
291 tile_mode,
292 )
293 }
294
295 /// Creates a radial gradient that spreads `colors` evenly from `center` out to `radius`,
296 /// clamping beyond it.
297 pub fn radial_gradient(colors: Vec<Color>, center: Point, radius: f32) -> Self {
298 Self::radial_gradient_tiled(colors, center, radius, TileMode::Clamp)
299 }
300
301 /// Creates a radial gradient that spreads `colors` evenly from `center` out to `radius`,
302 /// repeating or clamping beyond it as `tile_mode` says.
303 pub fn radial_gradient_tiled(
304 colors: Vec<Color>,
305 center: Point,
306 radius: f32,
307 tile_mode: TileMode,
308 ) -> Self {
309 Self::RadialGradient {
310 colors: colors.into(),
311 stops: None,
312 center,
313 radius,
314 tile_mode,
315 }
316 }
317
318 /// Creates a radial gradient from `(position, color)` pairs, with positions from 0.0 at the
319 /// center to 1.0 at `radius`.
320 pub fn radial_gradient_stops(
321 color_stops: Vec<(f32, Color)>,
322 center: Point,
323 radius: f32,
324 tile_mode: TileMode,
325 ) -> Self {
326 let (colors, stops) = split_color_stops(color_stops);
327 Self::RadialGradient {
328 colors: colors.into(),
329 stops: Some(stops.into()),
330 center,
331 radius,
332 tile_mode,
333 }
334 }
335
336 /// Creates a sweep gradient that spreads `colors` evenly around `center`.
337 pub fn sweep_gradient(colors: Vec<Color>, center: Point) -> Self {
338 Self::SweepGradient {
339 colors: colors.into(),
340 stops: None,
341 center,
342 }
343 }
344
345 /// Creates a sweep gradient from `(position, color)` pairs, with positions from 0.0 to 1.0
346 /// around `center`.
347 pub fn sweep_gradient_stops(color_stops: Vec<(f32, Color)>, center: Point) -> Self {
348 let (colors, stops) = split_color_stops(color_stops);
349 Self::SweepGradient {
350 colors: colors.into(),
351 stops: Some(stops.into()),
352 center,
353 }
354 }
355
356 /// The color this brush paints at `point` when it fills `rect`, before
357 /// any dithering. Gradient coordinates are offsets from `rect`'s
358 /// top-left corner, and an infinite one is `rect`'s far edge. Where a
359 /// [`TileMode::Decal`] gradient paints nothing the color is transparent.
360 /// [`BrushSampler`] gives the same colors for many points of one rect.
361 pub fn color_at(&self, rect: Rect, point: Point) -> Color {
362 BrushSampler::new(self, rect).color_at(point)
363 }
364}
365
366#[cfg(test)]
367#[path = "tests/brush_tests.rs"]
368mod tests;