Skip to main content

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}