euv_engine/asset/impl.rs
1use super::*;
2
3/// Implements cache query and management for `AssetCache`.
4impl AssetCache {
5 /// Returns the state of the asset with the given URL, or `None` if not cached.
6 ///
7 /// # Arguments
8 ///
9 /// - `U: AsRef<str>` - The asset URL.
10 ///
11 /// # Returns
12 ///
13 /// - `Option<AssetState>` - The asset state, or `None`.
14 pub fn get_state<U>(&self, url: U) -> Option<AssetState>
15 where
16 U: AsRef<str>,
17 {
18 self.get_entries()
19 .get(url.as_ref())
20 .map(|entry: &AssetEntry| entry.get_state())
21 }
22
23 /// Returns the loaded image for the given URL, or `None` if not loaded.
24 ///
25 /// # Arguments
26 ///
27 /// - `U: AsRef<str>` - The asset URL.
28 ///
29 /// # Returns
30 ///
31 /// - `Option<HtmlImageElement>` - The loaded image, or `None`.
32 pub fn get_image<U>(&self, url: U) -> Option<HtmlImageElement>
33 where
34 U: AsRef<str>,
35 {
36 let entry: &AssetEntry = self.get_entries().get(url.as_ref())?;
37 if entry.get_state() != AssetState::Loaded {
38 return None;
39 }
40 entry.get_image()
41 }
42
43 /// Returns `true` if all assets in the cache have finished loading.
44 ///
45 /// # Returns
46 ///
47 /// - `bool` - True if no assets are in the `Loading` state.
48 pub fn is_all_loaded(&self) -> bool {
49 self.get_entries()
50 .values()
51 .all(|entry: &AssetEntry| entry.get_state() != AssetState::Loading)
52 }
53
54 /// Returns the number of assets that have been successfully loaded.
55 ///
56 /// # Returns
57 ///
58 /// - `usize` - The count of loaded assets.
59 pub fn loaded_count(&self) -> usize {
60 self.get_entries()
61 .values()
62 .filter(|entry: &&AssetEntry| entry.get_state() == AssetState::Loaded)
63 .count()
64 }
65
66 /// Removes all entries from the cache.
67 pub fn clear(&mut self) {
68 self.get_mut_entries().clear();
69 }
70}
71
72/// Implements `Default` for `AssetCache` as a new empty cache.
73impl Default for AssetCache {
74 /// Constructs a default [`AssetCache`] value.
75 ///
76 /// # Returns
77 ///
78 /// - `AssetCache` - A default-constructed instance with the documented initial state.
79 fn default() -> AssetCache {
80 AssetCache::new()
81 }
82}
83
84/// Implements asynchronous asset loading for `AssetLoader`.
85impl AssetLoader {
86 /// Begins loading an image asset from the given URL.
87 ///
88 /// Creates an `HtmlImageElement`, sets its `src`, and registers `onload`/`onerror`
89 /// callbacks to update the shared cache state. The image loads asynchronously.
90 ///
91 /// Both callbacks decrement the shared pending counter and mark their own
92 /// closure slot settled. The closures themselves are released later by
93 /// [`AssetLoader::collect`], which is the only place a `Closure` may be
94 /// dropped safely — see [`AssetClosures`] for why a load callback cannot
95 /// free itself.
96 ///
97 /// # Arguments
98 ///
99 /// - `String` - The URL of the image to load.
100 pub fn load_image(&mut self, url: String) {
101 let Ok(image) = HtmlImageElement::new() else {
102 return;
103 };
104 let entry: AssetEntry = AssetEntry::new(
105 AssetType::Image,
106 AssetState::Loading,
107 Some(image.clone()),
108 url.clone(),
109 );
110 self.get_cache()
111 .get_mut()
112 .get_mut_entries()
113 .insert(url.clone(), entry);
114 *self.get_pending().get_mut() += 1;
115 let onload_slot: usize = self.get_closures().get().slots.len();
116 let onerror_slot: usize = onload_slot + 1;
117 let pending_shared: AssetPending = self.get_pending().clone();
118 let pending: AssetPending = pending_shared.clone();
119 let store_shared: Weak<EngineCell<AssetClosureStore>> = Rc::downgrade(self.get_closures());
120 let store_weak: Weak<EngineCell<AssetClosureStore>> = store_shared.clone();
121 let cache_clone: Rc<EngineCell<AssetCache>> = self.get_cache().clone();
122 let url_for_onload: String = url.clone();
123 let onload_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
124 {
125 let cache_ref: &mut AssetCache = cache_clone.get_mut();
126 if let Some(mut entry) = cache_ref.get_entries().get(&url_for_onload).cloned() {
127 entry.set_state(AssetState::Loaded);
128 cache_ref
129 .get_mut_entries()
130 .insert(url_for_onload.clone(), entry);
131 }
132 }
133 *pending.get_mut() = pending.get().saturating_sub(1);
134 mark_asset_closure_settled(&store_weak, onload_slot);
135 }));
136 let cache_clone_err: Rc<EngineCell<AssetCache>> = self.get_cache().clone();
137 let pending_err: AssetPending = pending_shared.clone();
138 let store_weak_err: Weak<EngineCell<AssetClosureStore>> = store_shared.clone();
139 let url_for_onerror: String = url.clone();
140 let onerror_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
141 {
142 let cache_ref: &mut AssetCache = cache_clone_err.get_mut();
143 if let Some(mut entry) = cache_ref.get_entries().get(&url_for_onerror).cloned() {
144 entry.set_state(AssetState::Error);
145 cache_ref
146 .get_mut_entries()
147 .insert(url_for_onerror.clone(), entry);
148 }
149 }
150 *pending_err.get_mut() = pending_err.get().saturating_sub(1);
151 mark_asset_closure_settled(&store_weak_err, onerror_slot);
152 }));
153 image.set_onload(Some(onload_closure.as_ref().unchecked_ref()));
154 image.set_onerror(Some(onerror_closure.as_ref().unchecked_ref()));
155 image.set_src(&url);
156 let store: &mut AssetClosureStore = self.get_closures().get_mut();
157 store.slots.push(Some(onload_closure));
158 store.settled.push(false);
159 let store: &mut AssetClosureStore = self.get_closures().get_mut();
160 store.slots.push(Some(onerror_closure));
161 store.settled.push(false);
162 }
163
164 /// Returns the number of loads that have been requested but not settled.
165 ///
166 /// # Returns
167 ///
168 /// - `u32` - The in-flight load count.
169 pub fn pending_count(&self) -> u32 {
170 *self.get_pending().get()
171 }
172
173 /// Advances the loader by one engine update step.
174 ///
175 /// Implements [`Updatable`] so an `AssetLoader` can be registered with the
176 /// scheduler's [`TaskRegistry`] and driven on every fixed step. The only
177 /// work is [`AssetLoader::collect`], which is the safe point to release
178 /// load callbacks that have already run.
179 ///
180 /// # Arguments
181 ///
182 /// - `f64` - The fixed delta time in seconds, unused.
183 pub fn update(&mut self, delta_time: f64) {
184 let _ = delta_time;
185 self.collect();
186 }
187
188 /// Releases the load callbacks that have already run.
189 ///
190 /// A `wasm_bindgen::Closure` must not be dropped while JavaScript is
191 /// executing it, so [`AssetLoader::load_image`] callbacks only mark their
192 /// own slot settled. This method is the safe drop point: it must be called
193 /// from a context that is not inside a load callback, such as the
194 /// engine's update step.
195 ///
196 /// Slots that have not settled belong to loads still in flight and are
197 /// kept alive, so only settled slots are released. The `onload` and
198 /// `onerror` closures of the same asset settle independently — a
199 /// successful load settles only its `onload` slot — so the paired slot is
200 /// released once both halves have run.
201 pub fn collect(&mut self) {
202 let store: &mut AssetClosureStore = self.get_closures().get_mut();
203 let mut index: usize = 0;
204 while index < store.slots.len() {
205 let settled: bool = store.settled.get(index).copied().unwrap_or(false);
206 if settled {
207 store.slots[index] = None;
208 store.settled[index] = false;
209 }
210 index += 1;
211 }
212 }
213
214 /// Returns whether all requested assets have finished loading.
215 ///
216 /// # Returns
217 ///
218 /// - `bool` - True if no assets are pending.
219 pub fn is_all_loaded(&self) -> bool {
220 self.get_cache().get().is_all_loaded()
221 }
222
223 /// Returns the loaded image for the given URL.
224 ///
225 /// # Arguments
226 ///
227 /// - `U: AsRef<str>` - The asset URL.
228 ///
229 /// # Returns
230 ///
231 /// - `Option<HtmlImageElement>` - The loaded image, or `None`.
232 pub fn get_image<U>(&self, url: U) -> Option<HtmlImageElement>
233 where
234 U: AsRef<str>,
235 {
236 self.get_cache().get().get_image(url.as_ref())
237 }
238
239 /// Returns the progress ratio of loaded assets.
240 ///
241 /// # Returns
242 ///
243 /// - `f64` - The ratio in the range 0.0 to 1.0.
244 pub fn progress(&self) -> f64 {
245 let cache_ref: &AssetCache = self.get_cache().get();
246 let total: usize = cache_ref.get_entries().len();
247 if total == 0 {
248 return 1.0;
249 }
250 cache_ref.loaded_count() as f64 / total as f64
251 }
252}
253
254/// Forwards `AssetLoader::update` through the [`Updatable`] trait so a loader
255/// can be registered with the scheduler's [`TaskRegistry`] and collect its
256/// finished load callbacks on every fixed step. The inherent
257/// [`AssetLoader::update`] method is the canonical implementation; this impl
258/// exists purely for trait dispatch.
259impl Updatable for AssetLoader {
260 /// Advances the loader by `delta_time` seconds.
261 ///
262 /// # Arguments
263 ///
264 /// - `f64` - Seconds elapsed since the previous update.
265 fn update(&mut self, delta_time: f64) {
266 AssetLoader::update(self, delta_time);
267 }
268}
269
270/// Implements `Default` for `AssetLoader` as a new empty loader.
271impl Default for AssetLoader {
272 /// Constructs a default [`AssetLoader`] value.
273 ///
274 /// # Returns
275 ///
276 /// - `AssetLoader` - A default-constructed instance with the documented initial state.
277 fn default() -> AssetLoader {
278 let mut loader: AssetLoader = AssetLoader::new();
279 loader.set_cache(Rc::new(EngineCell::new(AssetCache::default())));
280 loader.set_pending(Rc::new(EngineCell::new(0)));
281 loader.set_closures(Rc::new(EngineCell::new(AssetClosureStore::default())));
282 loader
283 }
284}
285
286/// Implements static asset creation utilities for `AssetLoader`.
287impl AssetLoader {
288 /// Creates an `HtmlImageElement` from the given URL without caching.
289 ///
290 /// The image loads asynchronously. Returns immediately with the image element
291 /// whose `src` is set but may not have finished loading yet.
292 ///
293 /// # Arguments
294 ///
295 /// - `U: AsRef<str>` - The image URL.
296 ///
297 /// # Returns
298 ///
299 /// - `Option<HtmlImageElement>` - The image element, or `None` if creation failed.
300 pub fn create_image_element<U>(url: U) -> Option<HtmlImageElement>
301 where
302 U: AsRef<str>,
303 {
304 let image: HtmlImageElement = HtmlImageElement::new().ok()?;
305 image.set_src(url.as_ref());
306 Some(image)
307 }
308}