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    /// Creates a linear gradient that defaults to Compose semantics:
80    /// start at `(0,0)`, end at `(+inf,+inf)`, and `TileMode::Clamp`.
81    pub fn linear_gradient(colors: Vec<Color>) -> Self {
82        Self::linear_gradient_with_tile_mode(
83            colors,
84            Point { x: 0.0, y: 0.0 },
85            Point {
86                x: f32::INFINITY,
87                y: f32::INFINITY,
88            },
89            TileMode::Clamp,
90        )
91    }
92
93    /// Creates a linear gradient that spreads `colors` evenly from `start` to `end`, clamping
94    /// outside that line.
95    pub fn linear_gradient_range(colors: Vec<Color>, start: Point, end: Point) -> Self {
96        Self::linear_gradient_with_tile_mode(colors, start, end, TileMode::Clamp)
97    }
98
99    /// Creates a linear gradient that spreads `colors` evenly from `start` to `end`, repeating or
100    /// clamping outside that line as `tile_mode` says.
101    pub fn linear_gradient_with_tile_mode(
102        colors: Vec<Color>,
103        start: Point,
104        end: Point,
105        tile_mode: TileMode,
106    ) -> Self {
107        Brush::LinearGradient {
108            colors,
109            stops: None,
110            start,
111            end,
112            tile_mode,
113        }
114    }
115
116    /// Creates a linear gradient from `(position, color)` pairs, with positions from 0.0 at `start`
117    /// to 1.0 at `end`.
118    pub fn linear_gradient_stops(
119        color_stops: Vec<(f32, Color)>,
120        start: Point,
121        end: Point,
122        tile_mode: TileMode,
123    ) -> Self {
124        let (colors, stops) = split_color_stops(color_stops);
125        Brush::LinearGradient {
126            colors,
127            stops: Some(stops),
128            start,
129            end,
130            tile_mode,
131        }
132    }
133
134    /// Creates a vertical gradient that spreads `colors` evenly from `start_y` to `end_y`, clamping
135    /// outside that range.
136    pub fn vertical_gradient(colors: Vec<Color>, start_y: f32, end_y: f32) -> Self {
137        Self::vertical_gradient_tiled(colors, start_y, end_y, TileMode::Clamp)
138    }
139
140    /// Creates a vertical gradient that spreads `colors` evenly from `start_y` to `end_y`,
141    /// repeating or clamping outside that range as `tile_mode` says.
142    pub fn vertical_gradient_tiled(
143        colors: Vec<Color>,
144        start_y: f32,
145        end_y: f32,
146        tile_mode: TileMode,
147    ) -> Self {
148        Self::linear_gradient_with_tile_mode(
149            colors,
150            Point { x: 0.0, y: start_y },
151            Point { x: 0.0, y: end_y },
152            tile_mode,
153        )
154    }
155
156    /// Creates a vertical gradient across the full height of the painted area, clamping at both
157    /// ends.
158    pub fn vertical_gradient_default(colors: Vec<Color>) -> Self {
159        Self::vertical_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
160    }
161
162    /// Creates a vertical gradient from `(position, color)` pairs, with positions from 0.0 at
163    /// `start_y` to 1.0 at `end_y`.
164    pub fn vertical_gradient_stops(
165        color_stops: Vec<(f32, Color)>,
166        start_y: f32,
167        end_y: f32,
168        tile_mode: TileMode,
169    ) -> Self {
170        Self::linear_gradient_stops(
171            color_stops,
172            Point { x: 0.0, y: start_y },
173            Point { x: 0.0, y: end_y },
174            tile_mode,
175        )
176    }
177
178    /// Creates a horizontal gradient that spreads `colors` evenly from `start_x` to `end_x`,
179    /// clamping outside that range.
180    pub fn horizontal_gradient(colors: Vec<Color>, start_x: f32, end_x: f32) -> Self {
181        Self::horizontal_gradient_tiled(colors, start_x, end_x, TileMode::Clamp)
182    }
183
184    /// Creates a horizontal gradient that spreads `colors` evenly from `start_x` to `end_x`,
185    /// repeating or clamping outside that range as `tile_mode` says.
186    pub fn horizontal_gradient_tiled(
187        colors: Vec<Color>,
188        start_x: f32,
189        end_x: f32,
190        tile_mode: TileMode,
191    ) -> Self {
192        Self::linear_gradient_with_tile_mode(
193            colors,
194            Point { x: start_x, y: 0.0 },
195            Point { x: end_x, y: 0.0 },
196            tile_mode,
197        )
198    }
199
200    /// Creates a horizontal gradient across the full width of the painted area, clamping at both
201    /// ends.
202    pub fn horizontal_gradient_default(colors: Vec<Color>) -> Self {
203        Self::horizontal_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
204    }
205
206    /// Creates a horizontal gradient from `(position, color)` pairs, with positions from 0.0 at
207    /// `start_x` to 1.0 at `end_x`.
208    pub fn horizontal_gradient_stops(
209        color_stops: Vec<(f32, Color)>,
210        start_x: f32,
211        end_x: f32,
212        tile_mode: TileMode,
213    ) -> Self {
214        Self::linear_gradient_stops(
215            color_stops,
216            Point { x: start_x, y: 0.0 },
217            Point { x: end_x, y: 0.0 },
218            tile_mode,
219        )
220    }
221
222    /// Creates a radial gradient that spreads `colors` evenly from `center` out to `radius`,
223    /// clamping beyond it.
224    pub fn radial_gradient(colors: Vec<Color>, center: Point, radius: f32) -> Self {
225        Self::radial_gradient_tiled(colors, center, radius, TileMode::Clamp)
226    }
227
228    /// Creates a radial gradient that spreads `colors` evenly from `center` out to `radius`,
229    /// repeating or clamping beyond it as `tile_mode` says.
230    pub fn radial_gradient_tiled(
231        colors: Vec<Color>,
232        center: Point,
233        radius: f32,
234        tile_mode: TileMode,
235    ) -> Self {
236        Brush::RadialGradient {
237            colors,
238            stops: None,
239            center,
240            radius,
241            tile_mode,
242        }
243    }
244
245    /// Creates a radial gradient from `(position, color)` pairs, with positions from 0.0 at the
246    /// center to 1.0 at `radius`.
247    pub fn radial_gradient_stops(
248        color_stops: Vec<(f32, Color)>,
249        center: Point,
250        radius: f32,
251        tile_mode: TileMode,
252    ) -> Self {
253        let (colors, stops) = split_color_stops(color_stops);
254        Brush::RadialGradient {
255            colors,
256            stops: Some(stops),
257            center,
258            radius,
259            tile_mode,
260        }
261    }
262
263    /// Creates a sweep gradient that spreads `colors` evenly around `center`.
264    pub fn sweep_gradient(colors: Vec<Color>, center: Point) -> Self {
265        Brush::SweepGradient {
266            colors,
267            stops: None,
268            center,
269        }
270    }
271
272    /// Creates a sweep gradient from `(position, color)` pairs, with positions from 0.0 to 1.0
273    /// around `center`.
274    pub fn sweep_gradient_stops(color_stops: Vec<(f32, Color)>, center: Point) -> Self {
275        let (colors, stops) = split_color_stops(color_stops);
276        Brush::SweepGradient {
277            colors,
278            stops: Some(stops),
279            center,
280        }
281    }
282}
283
284impl Brush {
285    /// The color this brush paints at `point` when it fills `rect`, before
286    /// any dithering. Gradient coordinates are offsets from `rect`'s
287    /// top-left corner, and an infinite one is `rect`'s far edge. Where a
288    /// [`TileMode::Decal`] gradient paints nothing the color is transparent.
289    pub fn color_at(&self, rect: Rect, point: Point) -> Color {
290        let (x, y) = (point.x, point.y);
291        let (colors, stops, t, tile_mode) = match self {
292            Brush::Solid(color) => return *color,
293            Brush::LinearGradient {
294                colors,
295                stops,
296                start,
297                end,
298                tile_mode,
299            } => {
300                let sx = resolve_gradient_point(rect.x, rect.width, start.x);
301                let sy = resolve_gradient_point(rect.y, rect.height, start.y);
302                let ex = resolve_gradient_point(rect.x, rect.width, end.x);
303                let ey = resolve_gradient_point(rect.y, rect.height, end.y);
304                let (dx, dy) = (ex - sx, ey - sy);
305                let denom = (dx * dx + dy * dy).max(f32::EPSILON);
306                let t = ((x - sx) * dx + (y - sy) * dy) / denom;
307                (colors, stops, t, *tile_mode)
308            }
309            Brush::RadialGradient {
310                colors,
311                stops,
312                center,
313                radius,
314                tile_mode,
315            } => {
316                let dx = x - (rect.x + center.x);
317                let dy = y - (rect.y + center.y);
318                let t = (dx * dx + dy * dy).sqrt() / radius.max(f32::EPSILON);
319                (colors, stops, t, *tile_mode)
320            }
321            Brush::SweepGradient {
322                colors,
323                stops,
324                center,
325            } => {
326                let angle = (y - (rect.y + center.y)).atan2(x - (rect.x + center.x));
327                // From 3 o'clock, clockwise, as Compose's sweep.
328                let t = (angle / std::f32::consts::TAU).rem_euclid(1.0);
329                (colors, stops, t, TileMode::Clamp)
330            }
331        };
332        match tiled_gradient_t(t, tile_mode) {
333            Some(t) => interpolate_colors(colors, stops.as_deref(), t),
334            None => TRANSPARENT,
335        }
336    }
337}
338
339const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
340
341fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
342    if value.is_finite() {
343        origin + value
344    } else if value.is_sign_positive() {
345        origin + extent
346    } else {
347        origin
348    }
349}
350
351/// Where `t` lands in the gradient's one span under `tile_mode`; `None`
352/// past a decal gradient's ends.
353fn tiled_gradient_t(t: f32, tile_mode: TileMode) -> Option<f32> {
354    match tile_mode {
355        TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
356        TileMode::Decal => (0.0..=1.0).contains(&t).then_some(t),
357        TileMode::Repeated => Some(t.rem_euclid(1.0)),
358        TileMode::Mirror => {
359            let wrapped = t.rem_euclid(2.0);
360            Some(if wrapped <= 1.0 {
361                wrapped
362            } else {
363                2.0 - wrapped
364            })
365        }
366    }
367}
368
369fn interpolate_colors(colors: &[Color], stops: Option<&[f32]>, t: f32) -> Color {
370    let (Some(&first), Some(&last)) = (colors.first(), colors.last()) else {
371        return TRANSPARENT;
372    };
373    if colors.len() == 1 {
374        return first;
375    }
376    let clamped = t.clamp(0.0, 1.0);
377    if let Some(stops) = stops.filter(|stops| stops.len() == colors.len()) {
378        if stops.first().is_some_and(|&start| clamped <= start) {
379            return first;
380        }
381        for (pair, span) in colors.windows(2).zip(stops.windows(2)) {
382            if clamped <= span[1] {
383                let width = (span[1] - span[0]).max(f32::EPSILON);
384                return lerp_color(
385                    pair[0],
386                    pair[1],
387                    ((clamped - span[0]) / width).clamp(0.0, 1.0),
388                );
389            }
390        }
391        return last;
392    }
393    let scaled = clamped * (colors.len() - 1) as f32;
394    let index = scaled.floor() as usize;
395    match colors.get(index..index + 2) {
396        Some(&[from, to]) => lerp_color(from, to, scaled - index as f32),
397        _ => last,
398    }
399}
400
401fn lerp_color(a: Color, b: Color, t: f32) -> Color {
402    let lerp = |start: f32, end: f32| start + (end - start) * t;
403    Color(
404        lerp(a.0, b.0),
405        lerp(a.1, b.1),
406        lerp(a.2, b.2),
407        lerp(a.3, b.3),
408    )
409}
410
411#[cfg(test)]
412#[path = "tests/brush_tests.rs"]
413mod tests;