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}