Skip to main content

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;