Skip to main content

compose_rust/
brush.rs

1//! Gradients and image fills: registered once, named by id wherever a colour can go.
2//!
3//! A brush does not fit where a colour fits. A `Paint` is two words on the wire and a
4//! gradient is a list of stops, so the brush travels the way a picture does: registered
5//! before anything draws it, kept by the Renderer, and named by id after that. That is
6//! the road [`crate::asset`] already built, and this is a second kind of traffic on it
7//! rather than a second road.
8//!
9//! The bytes are this module's own fixed layout and not a file format. No platform stores
10//! a list of stops as a document, so a format would be a parser to write, to keep, and to
11//! get wrong.
12
13use crate::asset::register_owned;
14use crate::schema::{AssetKind, Color, Paint, TileMode};
15
16/// A stop: where along the run it sits, and what colour it is there.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct Stop {
19    /// Zero at the start of the run, one at the end.
20    pub at: f32,
21    pub color: Color,
22}
23
24impl Stop {
25    pub const fn new(at: f32, color: Color) -> Self {
26        Self { at, color }
27    }
28}
29
30/// What a surface can be filled with beyond a flat colour.
31///
32/// The coordinates are fractions of the area being filled, never pixels. A gradient
33/// described in pixels would be a gradient that only looks right at one size, and nothing
34/// on this side of the boundary knows how large anything ended up.
35#[derive(Clone, Debug, PartialEq)]
36pub enum Brush {
37    Linear {
38        start: (f32, f32),
39        end: (f32, f32),
40        stops: Vec<Stop>,
41        tile: TileMode,
42    },
43    Radial {
44        center: (f32, f32),
45        /// As a fraction of the larger side, so a circle stays a circle. For an ellipse it
46        /// is the horizontal radius.
47        radius: f32,
48        /// The vertical radius of an ellipse, as a fraction of the larger side like
49        /// `radius`. Zero is a circle of `radius`, which is what CSS `radial-gradient(circle
50        /// ...)` draws; anything above zero is an ellipse `radius` across and `radius_y`
51        /// tall, which is CSS's default shape. A negative or NaN value is refused by the
52        /// Renderer, which reports it and draws the circle.
53        radius_y: f32,
54        stops: Vec<Stop>,
55        tile: TileMode,
56    },
57    /// A registered picture, laid out as a fill.
58    Image { asset: u32, tile: TileMode },
59}
60
61const KIND_LINEAR: u16 = 1;
62const KIND_RADIAL: u16 = 2;
63const KIND_IMAGE: u16 = 3;
64
65impl Brush {
66    /// Top to bottom, which is the gradient almost every screen wants.
67    pub fn vertical(stops: Vec<Stop>) -> Self {
68        Self::Linear {
69            start: (0.5, 0.0),
70            end: (0.5, 1.0),
71            stops,
72            tile: TileMode::Clamp,
73        }
74    }
75
76    /// Left to right.
77    pub fn horizontal(stops: Vec<Stop>) -> Self {
78        Self::Linear {
79            start: (0.0, 0.5),
80            end: (1.0, 0.5),
81            stops,
82            tile: TileMode::Clamp,
83        }
84    }
85
86    /// Out from the middle.
87    pub fn radial(stops: Vec<Stop>) -> Self {
88        Self::Radial {
89            center: (0.5, 0.5),
90            radius: 0.5,
91            radius_y: 0.0,
92            stops,
93            tile: TileMode::Clamp,
94        }
95    }
96
97    /// Out from the middle as an ellipse `radius_x` across and `radius_y` tall, each a
98    /// fraction of the larger side. CSS `radial-gradient(ellipse ...)`.
99    pub fn elliptical(radius_x: f32, radius_y: f32, stops: Vec<Stop>) -> Self {
100        Self::Radial {
101            center: (0.5, 0.5),
102            radius: radius_x,
103            radius_y,
104            stops,
105            tile: TileMode::Clamp,
106        }
107    }
108
109    /// The bytes the Renderer reads, in this module's own layout.
110    ///
111    /// Fixed fields throughout: a kind, a stop count, four coordinates, and then eight
112    /// bytes per stop. A radial brush's four are the centre, the radius and the vertical
113    /// radius, which is zero for a circle. Nothing is skipped for a brush that does not use a coordinate,
114    /// because a reader that has to know the kind before it knows where the stops start
115    /// is a reader that can be walked off the end of a short buffer.
116    pub fn to_bytes(&self) -> Vec<u8> {
117        let (kind, coordinates, stops, tile) = match self {
118            Self::Linear {
119                start,
120                end,
121                stops,
122                tile,
123            } => (
124                KIND_LINEAR,
125                [start.0, start.1, end.0, end.1],
126                stops.as_slice(),
127                *tile,
128            ),
129            Self::Radial {
130                center,
131                radius,
132                radius_y,
133                stops,
134                tile,
135            } => (
136                KIND_RADIAL,
137                [center.0, center.1, *radius, *radius_y],
138                stops.as_slice(),
139                *tile,
140            ),
141            // The picture's id rides in the first coordinate's bits. It is a u32 in a
142            // four-byte field, and giving the image case a layout of its own would mean
143            // two layouts to read instead of one.
144            Self::Image { asset, tile } => (
145                KIND_IMAGE,
146                [f32::from_bits(*asset), 0.0, 0.0, 0.0],
147                &[][..],
148                *tile,
149            ),
150        };
151        let mut bytes = Vec::with_capacity(HEADER_LEN + stops.len() * STOP_LEN);
152        bytes.extend_from_slice(&kind.to_le_bytes());
153        bytes.extend_from_slice(&(stops.len() as u16).to_le_bytes());
154        bytes.extend_from_slice(&(tile as u16).to_le_bytes());
155        bytes.extend_from_slice(&0u16.to_le_bytes());
156        for coordinate in coordinates {
157            bytes.extend_from_slice(&coordinate.to_bits().to_le_bytes());
158        }
159        for stop in stops {
160            bytes.extend_from_slice(&stop.at.to_bits().to_le_bytes());
161            bytes.extend_from_slice(&stop.color.0.to_le_bytes());
162        }
163        bytes
164    }
165}
166
167/// A kind, a stop count, a tile mode, two bytes of padding and four coordinates.
168pub const HEADER_LEN: usize = 24;
169/// A position and a colour.
170pub const STOP_LEN: usize = 8;
171
172/// Registers a brush and returns the `Paint` that names it.
173///
174/// Call it wherever the brush is used, including in a component body that runs every
175/// render: the second call with the same brush returns the first call's id and registers
176/// nothing, so a screen painted with the same gradient every frame registers it once and
177/// allocates nothing after that.
178///
179/// ```ignore
180/// let sky = brush(Brush::vertical(vec![
181///     Stop::new(0.0, Color::rgb(0x4a90d9)),
182///     Stop::new(1.0, Color::rgb(0xffffff)),
183/// ]));
184/// rsx! { Surface { background: sky, .. } }
185/// ```
186pub fn brush(brush: Brush) -> Paint {
187    Paint::Asset(register_owned(AssetKind::Brush, brush.to_bytes()))
188}
189
190/// Releases a brush the screen no longer uses, so the Renderer drops its copy.
191///
192/// Without this a screen whose gradients change (hover, animation, theme) grows the
193/// Renderer's brush table for the life of the app. Returns false when `paint` is not a
194/// registered brush. Releasing one still drawn is the caller's mistake and is reported
195/// by the Renderer as a protocol error, not a crash. Registering the same brush again
196/// afterwards gives a new id.
197pub fn release_brush(paint: Paint) -> bool {
198    match paint {
199        Paint::Asset(id) => crate::asset::release(id, AssetKind::Brush),
200        _ => false,
201    }
202}