compose_rust/asset.rs
1//! Pictures: registering the bytes once and drawing them by id.
2//!
3//! An asset is not part of the tree. It is bytes that have to reach the Renderer before
4//! anything can draw them and have to outlive the frame that does, which is the opposite
5//! of a node: a node is re-sent whenever it changes and points at nothing after the call
6//! that carried it.
7//!
8//! So an asset is registered and forgotten, the way a message is posted and forgotten. It
9//! rides out on the batch the current call produces, and everything after that is the id.
10
11use crate::schema::AssetKind;
12use std::cell::RefCell;
13
14/// One registration, waiting for the batch that carries it.
15pub(crate) struct PendingAsset {
16 pub(crate) asset_id: u32,
17 pub(crate) kind: AssetKind,
18 pub(crate) bytes: &'static [u8],
19}
20
21thread_local! {
22 /// What has been registered, so the same picture asked for twice is one registration.
23 ///
24 /// The kind is kept beside the bytes because a new Renderer has to be told all of this
25 /// again, and a registration is bytes, an id and what the bytes are.
26 static REGISTERED: RefCell<Vec<(&'static [u8], u32, AssetKind)>> =
27 const { RefCell::new(Vec::new()) };
28 static QUEUE: RefCell<Vec<PendingAsset>> = const { RefCell::new(Vec::new()) };
29}
30
31/// Registers one picture and returns the id an [`crate::Image`] draws it by.
32///
33/// ```ignore
34/// static HERO: &[u8] = include_bytes!("../assets/hero.svg");
35///
36/// rsx! { Image { asset_id: asset(AssetKind::Svg, HERO), height: 240.0 } }
37/// ```
38///
39/// Call it wherever the picture is drawn, including in a component body that runs every
40/// render. The second call with the same bytes returns the first call's id and queues
41/// nothing, so a screen that draws the same picture every frame registers it once.
42///
43/// The bytes are `'static` because a registration is carried by whichever batch the
44/// current call produces, which is not always the call this was made in, and because
45/// `include_bytes!` is what an application almost always has.
46pub fn asset(kind: AssetKind, bytes: &'static [u8]) -> u32 {
47 if let Some(id) = REGISTERED.with_borrow(|registered| registered_id(registered, bytes)) {
48 return id;
49 }
50 // Ids start at one. Zero is what a node with no asset property reads as, so handing it
51 // out would make "no picture" and "the first picture" the same value on the wire.
52 let asset_id = REGISTERED.with_borrow_mut(|registered| {
53 let asset_id = registered.len() as u32 + 1;
54 registered.push((bytes, asset_id, kind));
55 asset_id
56 });
57 QUEUE.with_borrow_mut(|queue| {
58 queue.push(PendingAsset {
59 asset_id,
60 kind,
61 bytes,
62 });
63 });
64 asset_id
65}
66
67/// Registers bytes this process built rather than bytes it was compiled with.
68///
69/// A gradient is worked out at run time, so there is no `include_bytes!` to point at. The
70/// bytes are handed to the registry to keep for the life of the process, which is what
71/// `'static` means here and what a registration is anyway: an asset has to outlive every
72/// frame that draws it, and nothing ever un-registers by dropping.
73///
74/// The content comparison runs first, so building the same gradient on every render keeps
75/// one copy. Only a brush nobody has registered before is kept.
76pub fn register_owned(kind: AssetKind, bytes: Vec<u8>) -> u32 {
77 if let Some(id) = REGISTERED.with_borrow(|registered| {
78 registered
79 .iter()
80 .find(|(known, _, _)| *known == bytes.as_slice())
81 .map(|(_, id, _)| *id)
82 }) {
83 return id;
84 }
85 asset(kind, Box::leak(bytes.into_boxed_slice()))
86}
87
88/// The id these bytes were registered under, by address first and by content after.
89///
90/// Address first because that is the answer almost every time and it costs a comparison.
91/// Content after because the same picture really can arrive at two addresses: a `const`
92/// holding a reference is inlined at each use, and each use can get an allocation of its
93/// own, so a catalogue written as a `const` array hands out two pointers to one file and
94/// the shop registers the same cover twice under two ids. The full comparison only runs
95/// when the address misses, which is once per new picture and never in the steady state.
96fn registered_id(
97 registered: &[(&'static [u8], u32, AssetKind)],
98 bytes: &'static [u8],
99) -> Option<u32> {
100 registered
101 .iter()
102 .find(|(known, _, _)| std::ptr::eq(*known, bytes))
103 .or_else(|| registered.iter().find(|(known, _, _)| *known == bytes))
104 .map(|(_, id, _)| *id)
105}
106
107/// Hands every queued registration to `emit` and empties the queue.
108///
109/// The buffer is moved out and put back, so the steady state, where nothing new has been
110/// registered, touches an empty `Vec` and allocates nothing.
111pub(crate) fn drain(mut emit: impl FnMut(&PendingAsset)) {
112 let mut taken = QUEUE.with_borrow_mut(std::mem::take);
113 if taken.is_empty() {
114 return;
115 }
116 for pending in &taken {
117 emit(pending);
118 }
119 taken.clear();
120 QUEUE.with_borrow_mut(|queue| {
121 if queue.is_empty() {
122 *queue = taken;
123 }
124 });
125}
126
127/// Forgets every registration and every id. Used between tests.
128#[doc(hidden)]
129pub fn reset_assets() {
130 REGISTERED.with_borrow_mut(Vec::clear);
131 QUEUE.with_borrow_mut(Vec::clear);
132}
133
134/// Queues every registration again, for a Renderer that has not been told any of them.
135///
136/// What a new `Host` does instead of forgetting them. A new Host means a Renderer whose
137/// cache is empty, so an id handed out before it names nothing over there, and the cure
138/// for that is to say all of them again rather than to drop them.
139///
140/// Dropping them was wrong in a way that only shows on the screen. A theme names its fonts
141/// by id and a window names its icon by id, and both are built by the caller before the
142/// Host is made out of them: the ids travelled and the bytes they named had been thrown
143/// away, so the window wore no icon and the screen was set in the machine's own face while
144/// every batch looked correct.
145///
146/// Ids do not move. The same bytes keep the id they were given, which is what lets a theme
147/// built before this still name the right thing after it.
148pub(crate) fn requeue_all() {
149 let all: Vec<PendingAsset> = REGISTERED.with_borrow(|registered| {
150 registered
151 .iter()
152 .map(|(bytes, asset_id, kind)| PendingAsset {
153 asset_id: *asset_id,
154 kind: *kind,
155 bytes,
156 })
157 .collect()
158 });
159 if all.is_empty() {
160 return;
161 }
162 QUEUE.with_borrow_mut(|queue| {
163 // Anything already waiting is in `all` as well, because everything queued was
164 // registered. Replacing rather than appending is what keeps it sent once.
165 *queue = all;
166 });
167}