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.
46 radius: f32,
47 stops: Vec<Stop>,
48 tile: TileMode,
49 },
50 /// A registered picture, laid out as a fill.
51 Image { asset: u32, tile: TileMode },
52}
53
54const KIND_LINEAR: u16 = 1;
55const KIND_RADIAL: u16 = 2;
56const KIND_IMAGE: u16 = 3;
57
58impl Brush {
59 /// Top to bottom, which is the gradient almost every screen wants.
60 pub fn vertical(stops: Vec<Stop>) -> Self {
61 Self::Linear {
62 start: (0.5, 0.0),
63 end: (0.5, 1.0),
64 stops,
65 tile: TileMode::Clamp,
66 }
67 }
68
69 /// Left to right.
70 pub fn horizontal(stops: Vec<Stop>) -> Self {
71 Self::Linear {
72 start: (0.0, 0.5),
73 end: (1.0, 0.5),
74 stops,
75 tile: TileMode::Clamp,
76 }
77 }
78
79 /// Out from the middle.
80 pub fn radial(stops: Vec<Stop>) -> Self {
81 Self::Radial {
82 center: (0.5, 0.5),
83 radius: 0.5,
84 stops,
85 tile: TileMode::Clamp,
86 }
87 }
88
89 /// The bytes the Renderer reads, in this module's own layout.
90 ///
91 /// Fixed fields throughout: a kind, a stop count, four coordinates, and then eight
92 /// bytes per stop. Nothing is skipped for a brush that does not use a coordinate,
93 /// because a reader that has to know the kind before it knows where the stops start
94 /// is a reader that can be walked off the end of a short buffer.
95 pub fn to_bytes(&self) -> Vec<u8> {
96 let (kind, coordinates, stops, tile) = match self {
97 Self::Linear {
98 start,
99 end,
100 stops,
101 tile,
102 } => (
103 KIND_LINEAR,
104 [start.0, start.1, end.0, end.1],
105 stops.as_slice(),
106 *tile,
107 ),
108 Self::Radial {
109 center,
110 radius,
111 stops,
112 tile,
113 } => (
114 KIND_RADIAL,
115 [center.0, center.1, *radius, 0.0],
116 stops.as_slice(),
117 *tile,
118 ),
119 // The picture's id rides in the first coordinate's bits. It is a u32 in a
120 // four-byte field, and giving the image case a layout of its own would mean
121 // two layouts to read instead of one.
122 Self::Image { asset, tile } => (
123 KIND_IMAGE,
124 [f32::from_bits(*asset), 0.0, 0.0, 0.0],
125 &[][..],
126 *tile,
127 ),
128 };
129 let mut bytes = Vec::with_capacity(HEADER_LEN + stops.len() * STOP_LEN);
130 bytes.extend_from_slice(&kind.to_le_bytes());
131 bytes.extend_from_slice(&(stops.len() as u16).to_le_bytes());
132 bytes.extend_from_slice(&(tile as u16).to_le_bytes());
133 bytes.extend_from_slice(&0u16.to_le_bytes());
134 for coordinate in coordinates {
135 bytes.extend_from_slice(&coordinate.to_bits().to_le_bytes());
136 }
137 for stop in stops {
138 bytes.extend_from_slice(&stop.at.to_bits().to_le_bytes());
139 bytes.extend_from_slice(&stop.color.0.to_le_bytes());
140 }
141 bytes
142 }
143}
144
145/// A kind, a stop count, a tile mode, two bytes of padding and four coordinates.
146pub const HEADER_LEN: usize = 24;
147/// A position and a colour.
148pub const STOP_LEN: usize = 8;
149
150/// Registers a brush and returns the `Paint` that names it.
151///
152/// Call it wherever the brush is used, including in a component body that runs every
153/// render: the second call with the same brush returns the first call's id and registers
154/// nothing, so a screen painted with the same gradient every frame registers it once and
155/// allocates nothing after that.
156///
157/// ```ignore
158/// let sky = brush(Brush::vertical(vec![
159/// Stop::new(0.0, Color::rgb(0x4a90d9)),
160/// Stop::new(1.0, Color::rgb(0xffffff)),
161/// ]));
162/// rsx! { Surface { background: sky, .. } }
163/// ```
164pub fn brush(brush: Brush) -> Paint {
165 Paint::Asset(register_owned(AssetKind::Brush, brush.to_bytes()))
166}