1use std::{
6 cell::RefCell,
7 collections::HashMap,
8 hash::{Hash, Hasher},
9 io,
10 path::{Path, PathBuf},
11 sync::{Arc, Weak},
12};
13
14use crate::{State, StateOwner, local_runtime_resource};
15
16#[cfg(not(target_family = "wasm"))]
17use crate::runtime_resource;
18
19#[cfg(target_os = "android")]
20mod android;
21#[cfg(not(any(target_os = "android", target_family = "wasm")))]
22mod native;
23#[cfg(target_family = "wasm")]
24mod web;
25
26#[derive(Clone, Debug)]
28pub enum LoadState<T> {
29 Loading,
31 Ready(T),
33 Failed(Arc<io::Error>),
35}
36
37#[derive(Clone, Copy)]
39pub struct Asset {
40 path: &'static str,
41 bytes: Option<&'static [u8]>,
42}
43
44#[derive(Clone)]
46pub enum Source {
47 Asset(Asset),
49 Path(Arc<str>),
51 Bytes(Arc<[u8]>),
53}
54
55#[derive(Clone, Copy)]
57pub struct FontAsset {
58 pub asset: Asset,
60 pub family: &'static str,
62 pub fallback: &'static [(u32, u32)],
64 pub coverage: &'static [(u32, u32)],
66}
67
68#[derive(Clone)]
70pub struct Resource<T: Send + Sync + 'static>(Arc<ResourceEntry<T>>);
71
72struct ResourceEntry<T: Send + Sync + 'static> {
73 source: Source,
74 owner: StateOwner,
75 state: State<LoadState<T>>,
76}
77
78impl<T: Send + Sync + 'static> std::fmt::Debug for Resource<T> {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_tuple("Resource").field(&self.0.source).finish()
81 }
82}
83
84impl<T: Send + Sync + 'static> PartialEq for Resource<T> {
85 fn eq(&self, other: &Self) -> bool {
86 Arc::ptr_eq(&self.0, &other.0)
87 }
88}
89impl<T: Send + Sync + 'static> Eq for Resource<T> {}
90
91impl<T: Clone + Send + Sync + 'static> Resource<T> {
92 pub fn load(source: impl Into<Source>, decode: fn(Arc<[u8]>) -> io::Result<T>) -> Self {
99 let source = source.into();
100 let cache = local_runtime_resource::<ResourceCache<T>>();
101 let key = (source.clone(), decode as usize);
102 let resource = {
103 let mut entries = cache.0.borrow_mut();
104 if let Some(entry) = entries.get(&key).and_then(Weak::upgrade) {
105 return Resource(entry);
106 }
107 entries.retain(|_, entry| entry.strong_count() > 0);
108 let owner = StateOwner::new();
109 let state = owner.state_with_key((), || LoadState::Loading);
110 let entry = Arc::new(ResourceEntry {
111 source: source.clone(),
112 owner,
113 state,
114 });
115 entries.insert(key, Arc::downgrade(&entry));
116 Resource(entry)
117 };
118 let state = resource.0.state;
119 let label = format!("{source:?}");
120 let finish = move |result: io::Result<T>| {
121 state.set(match result {
122 Ok(value) => LoadState::Ready(value),
123 Err(error) => {
124 tracing::warn!(source = label, %error, "resource loading failed");
125 LoadState::Failed(Arc::new(error))
126 }
127 });
128 };
129 let dispatcher = resource.0.owner.dispatcher();
130 #[cfg(target_family = "wasm")]
131 wasm_bindgen_futures::spawn_local(async move {
132 let result = source.read().await;
133 if dispatcher.is_alive() {
134 let result = result.and_then(decode);
135 dispatcher.dispatch(move || finish(result));
136 }
137 });
138 #[cfg(not(target_family = "wasm"))]
139 {
140 let in_flight = runtime_resource::<InFlight>();
141 *in_flight.0.lock() += 1;
142 let loading = Loading(in_flight);
143 let _ = WORKERS.send(Box::new(move || {
144 let _loading = loading;
145 if dispatcher.is_alive() {
146 let result = source.read().and_then(decode);
147 dispatcher.dispatch(move || finish(result));
148 }
149 }));
150 }
151 resource
152 }
153
154 pub fn get(&self) -> LoadState<T> {
156 self.0.state.get()
157 }
158}
159
160struct ResourceCache<T: Send + Sync + 'static>(
161 RefCell<HashMap<(Source, usize), Weak<ResourceEntry<T>>>>,
162);
163
164impl<T: Send + Sync + 'static> Default for ResourceCache<T> {
165 fn default() -> Self {
166 Self(RefCell::default())
167 }
168}
169
170#[cfg(not(target_family = "wasm"))]
171type Work = Box<dyn FnOnce() + Send>;
172
173#[cfg(not(target_family = "wasm"))]
174static WORKERS: std::sync::LazyLock<std::sync::mpsc::Sender<Work>> =
175 std::sync::LazyLock::new(|| {
176 let (sender, receiver) = std::sync::mpsc::channel::<Work>();
177 let receiver = Arc::new(parking_lot::Mutex::new(receiver));
178 let threads = std::thread::available_parallelism().map_or(2, |n| n.get().clamp(2, 4));
179 for _ in 0..threads {
180 let receiver = Arc::clone(&receiver);
181 std::thread::spawn(move || {
182 loop {
183 let work = receiver.lock().recv();
184 match work {
185 Ok(work) => work(),
186 Err(_) => break,
187 }
188 }
189 });
190 }
191 sender
192 });
193
194#[cfg(not(target_family = "wasm"))]
195#[derive(Default)]
196struct InFlight(parking_lot::Mutex<usize>, parking_lot::Condvar);
197
198#[cfg(not(target_family = "wasm"))]
199struct Loading(Arc<InFlight>);
200
201#[cfg(not(target_family = "wasm"))]
202impl Drop for Loading {
203 fn drop(&mut self) {
204 *self.0.0.lock() -= 1;
205 self.0.1.notify_all();
206 }
207}
208
209#[cfg(all(any(test, feature = "testing"), not(target_family = "wasm")))]
210pub(crate) fn in_flight() -> bool {
211 *runtime_resource::<InFlight>().0.lock() > 0
212}
213
214#[cfg(all(
216 any(test, feature = "testing", feature = "headless"),
217 not(target_family = "wasm")
218))]
219pub(crate) fn settle() {
220 let in_flight = runtime_resource::<InFlight>();
221 let mut count = in_flight.0.lock();
222 while *count > 0 {
223 in_flight.1.wait(&mut count);
224 }
225}
226
227impl std::fmt::Debug for Asset {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 self.path.fmt(f)
230 }
231}
232
233impl PartialEq for Asset {
234 fn eq(&self, other: &Self) -> bool {
235 self.path == other.path
236 }
237}
238impl Eq for Asset {}
239
240impl Hash for Asset {
241 fn hash<H: Hasher>(&self, state: &mut H) {
242 self.path.hash(state);
243 }
244}
245
246impl Asset {
247 #[doc(hidden)]
249 pub const fn embedded(path: &'static str, bytes: &'static [u8]) -> Self {
250 Self {
251 path,
252 bytes: Some(bytes),
253 }
254 }
255
256 #[doc(hidden)]
258 pub const fn platform(path: &'static str) -> Self {
259 Self { path, bytes: None }
260 }
261
262 pub fn path(self) -> &'static str {
264 self.path
265 }
266}
267
268impl std::fmt::Debug for Source {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 match self {
271 Self::Asset(asset) => asset.fmt(f),
272 Self::Path(path) => path.fmt(f),
273 Self::Bytes(bytes) => write!(f, "{} bytes", bytes.len()),
274 }
275 }
276}
277
278impl PartialEq for Source {
279 fn eq(&self, other: &Self) -> bool {
280 match (self, other) {
281 (Self::Asset(a), Self::Asset(b)) => a == b,
282 (Self::Path(a), Self::Path(b)) => a == b,
283 (Self::Bytes(a), Self::Bytes(b)) => Arc::ptr_eq(a, b) || a == b,
284 _ => false,
285 }
286 }
287}
288impl Eq for Source {}
289
290impl Hash for Source {
291 fn hash<H: Hasher>(&self, state: &mut H) {
292 std::mem::discriminant(self).hash(state);
293 match self {
294 Self::Asset(asset) => asset.hash(state),
295 Self::Path(path) => path.hash(state),
296 Self::Bytes(bytes) => bytes.hash(state),
297 }
298 }
299}
300
301impl Source {
302 #[cfg(not(target_family = "wasm"))]
303 fn read(&self) -> io::Result<Arc<[u8]>> {
304 match self {
305 Self::Asset(Asset {
306 bytes: Some(bytes), ..
307 }) => Ok(Arc::from(*bytes)),
308 #[cfg(target_os = "android")]
309 Self::Asset(asset) => android::read(asset.path),
310 #[cfg(not(target_os = "android"))]
311 Self::Asset(asset) => native::path(asset.path)
312 .and_then(std::fs::read)
313 .map(Arc::from),
314 Self::Path(path) => std::fs::read(&**path).map(Arc::from),
315 Self::Bytes(bytes) => Ok(Arc::clone(bytes)),
316 }
317 }
318
319 #[cfg(target_family = "wasm")]
320 async fn read(&self) -> io::Result<Arc<[u8]>> {
321 match self {
322 Self::Asset(Asset {
323 bytes: Some(bytes), ..
324 }) => Ok(Arc::from(*bytes)),
325 Self::Asset(asset) => web::read(asset.path).await,
326 Self::Path(path) => web::read(path).await,
327 Self::Bytes(bytes) => Ok(Arc::clone(bytes)),
328 }
329 }
330}
331
332macro_rules! source_from {
333 ($($ty:ty => |$value:ident| $source:expr),* $(,)?) => {$(
334 impl From<$ty> for Source {
335 fn from($value: $ty) -> Self {
336 $source
337 }
338 }
339 )*};
340}
341
342source_from! {
343 Asset => |asset| Self::Asset(asset),
344 &str => |path| Self::Path(path.into()),
345 String => |path| Self::Path(path.into()),
346 Arc<str> => |path| Self::Path(path),
347 &Path => |path| Self::Path(path.to_string_lossy().into()),
348 PathBuf => |path| Self::Path(path.to_string_lossy().into()),
349 &[u8] => |bytes| Self::Bytes(bytes.into()),
350 Vec<u8> => |bytes| Self::Bytes(bytes.into()),
351 Arc<[u8]> => |bytes| Self::Bytes(bytes),
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 fn delivered<T: Clone + Send + Sync + 'static>(resource: &Resource<T>) -> LoadState<T> {
359 assert!(matches!(resource.get(), LoadState::Loading));
360 settle();
361 crate::dispatcher::flush();
362 resource.get()
363 }
364
365 fn upper(bytes: Arc<[u8]>) -> io::Result<Arc<[u8]>> {
366 assert!(std::thread::current().name().is_none());
367 Ok(Arc::from(bytes.to_ascii_uppercase()))
368 }
369
370 #[cfg(not(any(target_family = "wasm", target_os = "android")))]
371 #[test]
372 fn sources_decode_off_the_ui_thread_and_share_live_results() {
373 let file = tempfile::NamedTempFile::new().unwrap();
374 std::fs::write(file.path(), b"file").unwrap();
375 crate::RuntimeSession::new().run(|| {
376 let asset = Asset::embedded("test/shared", b"original");
377 let decoded = Resource::load(asset, upper);
378 let LoadState::Ready(bytes) = delivered(&decoded) else {
379 panic!("decoded asset")
380 };
381 assert_eq!(&*bytes, b"ORIGINAL");
382 assert_eq!(decoded, Resource::load(asset, upper));
383 let memory = Resource::load(b"bytes".to_vec(), upper);
384 assert_eq!(memory, Resource::load(&b"bytes"[..], upper));
385 assert!(matches!(delivered(&memory), LoadState::Ready(bytes) if &*bytes == b"BYTES"));
386 let LoadState::Ready(bytes) = delivered(&Resource::load(file.path(), Ok)) else {
387 panic!("file")
388 };
389 assert_eq!(&*bytes, b"file");
390 assert!(matches!(
391 delivered(&Resource::<()>::load(asset, |_| Err(io::Error::other(
392 "invalid"
393 )))),
394 LoadState::Failed(_)
395 ));
396 });
397 }
398}