Skip to main content

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}