Skip to main content

cranpose_ui_graphics/
brush.rs

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