Skip to main content

euv_engine/engine/
impl.rs

1use super::*;
2
3/// Implements the top-level static engine entry points on the `Engine` namespace.
4///
5/// Mirrors the role of `euv::App` - every public engine operation begins
6/// with an `Engine::xxx` call, and `Engine` itself holds no state.
7impl Engine {
8    /// Creates a new engine handle bound to the given configuration.
9    ///
10    /// The handle is uninitialized; call `init_canvas` / `init_webgpu` and
11    /// `start` on the returned handle to begin the game loop, or use
12    /// `Engine::run` for the one-line equivalent.
13    ///
14    /// # Arguments
15    ///
16    /// - `EngineConfig` - The engine configuration.
17    ///
18    /// # Returns
19    ///
20    /// - `EngineHandle` - The new uninitialized engine handle.
21    pub fn new_handle(config: EngineConfig) -> EngineHandle {
22        let mut handle: EngineHandle = EngineHandle::new(config, None, None, None, None);
23        handle.set_tasks(Rc::new(EngineCell::new(TaskRegistry::default())));
24        handle
25    }
26
27    /// Runs the engine through its complete lifecycle in a single async call.
28    ///
29    /// Equivalent to calling `Engine::new_handle(config)` followed by
30    /// `init_canvas` / `init_webgpu` (matching the chosen backend) and
31    /// `start(handler)`. The returned handle can still be used to stop
32    /// the loop later via `handle.stop()`.
33    ///
34    /// # Arguments
35    ///
36    /// - `EngineConfig` - The engine configuration.
37    /// - `TickHandlerRc` - The tick handler receiving update and render callbacks.
38    ///
39    /// # Returns
40    ///
41    /// - `EngineHandle` - A handle to the running engine. If renderer
42    ///   initialization failed the handle's renderer field will be `None`
43    ///   and `is_running` will be `false`. Initialization errors are not
44    ///   logged inside the engine; callers should call `init_webgpu`
45    ///   directly if they need access to the typed `WebGpuInitError`.
46    pub async fn run(config: EngineConfig, handler: TickHandlerRc) -> EngineHandle {
47        let mut handle: EngineHandle = Engine::new_handle(config);
48        match handle.get_config().get_render().get_backend() {
49            RenderBackendType::Canvas2D => {
50                handle.init_canvas();
51            }
52            RenderBackendType::WebGpu => {
53                let _: Result<WebGpuRenderer, WebGpuInitError> = handle.init_webgpu().await;
54            }
55            RenderBackendType::WebGl => {
56                let _: Result<WebGlRenderer, WebGlInitError> = handle.init_webgl();
57            }
58        }
59        handle.start(handler);
60        handle
61    }
62
63    /// Returns the default engine configuration.
64    ///
65    /// Uses `RenderConfig::default()` (Canvas 2D backend) and the default
66    /// scheduler configuration (60 Hz fixed timestep).
67    ///
68    /// # Returns
69    ///
70    /// - `EngineConfig` - The default engine configuration.
71    pub fn default_config() -> EngineConfig {
72        EngineConfig::default()
73    }
74
75    /// Creates a Canvas 2D renderer directly from a render configuration.
76    ///
77    /// Use this when you want a `CanvasRenderer` without going through
78    /// the full `EngineHandle` lifecycle (for example, in a custom render
79    /// loop that does not use the fixed-timestep scheduler).
80    ///
81    /// # Arguments
82    ///
83    /// - `&RenderConfig` - The rendering configuration.
84    ///
85    /// # Returns
86    ///
87    /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas element was not found.
88    pub fn canvas_renderer(config: &RenderConfig) -> Option<CanvasRenderer> {
89        CanvasRenderer::from_selector(
90            config.get_canvas_selector(),
91            config.get_width(),
92            config.get_height(),
93        )
94    }
95
96    /// Creates a WebGPU renderer directly from a render configuration.
97    ///
98    /// Async because GPU adapter and device acquisition returns JavaScript
99    /// Promises that must be awaited. Mirrors `Engine::canvas_renderer` for
100    /// callers that want the renderer without the full engine handle.
101    ///
102    /// Failures are surfaced as `WebGpuInitError` so the caller can decide
103    /// how to react (typically by logging via `Console::error` or by
104    /// falling back to the Canvas 2D backend).
105    ///
106    /// # Arguments
107    ///
108    /// - `&RenderConfig` - The rendering configuration.
109    ///
110    /// # Returns
111    ///
112    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer,
113    ///   or a typed error describing the specific failure.
114    pub async fn webgpu_renderer(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
115        WebGpuRenderer::init(config).await
116    }
117
118    /// Creates a WebGL 2 renderer directly from a render configuration.
119    ///
120    /// Unlike [`Engine::webgpu_renderer`] this is synchronous because WebGL
121    /// context acquisition is a plain DOM call with no Promises involved.
122    ///
123    /// # Arguments
124    ///
125    /// - `&RenderConfig` - The rendering configuration.
126    ///
127    /// # Returns
128    ///
129    /// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
130    ///   or a typed error describing the specific failure.
131    pub fn webgl_renderer(config: &RenderConfig) -> Result<WebGlRenderer, WebGlInitError> {
132        WebGlRenderer::init(config)
133    }
134}
135
136/// Implements lifecycle management for `EngineHandle`.
137///
138/// All getter methods (`get_config`, `get_canvas_renderer`, `get_webgpu_renderer`)
139/// are generated by the `Data` derive on the struct definition; this impl
140/// only contributes lifecycle and accessor behavior that is not expressible
141/// as a plain getter.
142impl EngineHandle {
143    /// Initializes the Canvas 2D rendering backend.
144    ///
145    /// On success, populates `canvas_renderer` and clears `webgpu_renderer`.
146    /// On failure, both renderer fields remain `None`.
147    ///
148    /// # Returns
149    ///
150    /// - `bool` - `true` if the renderer was created successfully.
151    pub fn init_canvas(&mut self) -> bool {
152        let render_config: &RenderConfig = &self.get_config().get_render();
153        let renderer: Option<CanvasRenderer> = CanvasRenderer::from_selector(
154            render_config.get_canvas_selector(),
155            render_config.get_width(),
156            render_config.get_height(),
157        );
158        match renderer {
159            Some(r) => {
160                self.set_canvas_renderer(Some(r));
161                self.set_webgpu_renderer(None);
162                self.set_webgl_renderer(None);
163                true
164            }
165            None => {
166                self.set_canvas_renderer(None);
167                self.set_webgpu_renderer(None);
168                self.set_webgl_renderer(None);
169                false
170            }
171        }
172    }
173
174    /// Initializes the WebGPU rendering backend.
175    ///
176    /// On success, populates `webgpu_renderer` and clears `canvas_renderer`.
177    /// On failure, both renderer fields remain `None` and the typed error is
178    /// returned so the caller can decide how to surface it (typically via
179    /// `Console::error`).
180    ///
181    /// # Returns
182    ///
183    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer,
184    ///   or a typed error describing the specific failure.
185    pub async fn init_webgpu(&mut self) -> Result<WebGpuRenderer, WebGpuInitError> {
186        let render_config: &RenderConfig = &self.get_config().get_render();
187        let renderer: WebGpuRenderer = WebGpuRenderer::init(render_config).await?;
188        self.set_webgpu_renderer(Some(renderer.clone()));
189        self.set_canvas_renderer(None);
190        self.set_webgl_renderer(None);
191        Ok(renderer)
192    }
193
194    /// Initializes the WebGL 2 rendering backend.
195    ///
196    /// On success, populates `webgl_renderer` and clears the other renderer
197    /// fields. On failure, all renderer fields remain `None` and the typed
198    /// error is returned so the caller can decide how to surface it.
199    ///
200    /// # Returns
201    ///
202    /// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
203    ///   or a typed error describing the specific failure.
204    pub fn init_webgl(&mut self) -> Result<WebGlRenderer, WebGlInitError> {
205        let render_config: &RenderConfig = &self.get_config().get_render();
206        let renderer: WebGlRenderer = WebGlRenderer::init(render_config)?;
207        self.set_webgl_renderer(Some(renderer.clone()));
208        self.set_canvas_renderer(None);
209        self.set_webgpu_renderer(None);
210        Ok(renderer)
211    }
212
213    /// Attaches DOM input listeners and stores the shared input state.
214    ///
215    /// Resolves the canvas element from the render configuration's
216    /// `canvas_selector` and routes DOM events into a shared [`InputState`]:
217    /// keyboard events bind to `window`, mouse and touch events bind to the
218    /// canvas. This works for both the Canvas 2D and WebGPU backends because
219    /// either way the same `<canvas>` element receives the pointer events.
220    ///
221    /// Returns `None` (and stores nothing) if the canvas selector does not
222    /// resolve at call time - for example when called before the canvas
223    /// element is mounted into the DOM.
224    ///
225    /// The registered listeners stay alive for the lifetime of the document
226    /// (mount-only convention; there is no matching `unregister_input`).
227    ///
228    /// # Returns
229    ///
230    /// - `Option<InputStateCell>` - The shared input state cell, or `None`
231    ///   if the canvas element was not found.
232    pub fn register_input(&mut self) -> Option<InputStateCell> {
233        if let Some(existing) = self.try_get_input_cell() {
234            return Some(existing.clone());
235        }
236        let window_value: Window = window()?;
237        let document_value: Document = window_value.document()?;
238        let render_config: &RenderConfig = &self.get_config().get_render();
239        let canvas_selector: String = render_config.get_canvas_selector();
240        let element: Element = document_value
241            .query_selector(canvas_selector.as_ref())
242            .ok()
243            .flatten()?;
244        let target: EventTarget = element.into();
245        let cell: InputStateCell = Input::attach(
246            Rc::new(EngineCell::new(InputState::default())),
247            &window_value,
248            &target,
249        );
250        self.set_input_cell(Some(cell.clone()));
251        Some(cell)
252    }
253
254    /// Starts the game loop with the given tick handler.
255    ///
256    /// The scheduler configuration from `EngineConfig` controls the fixed
257    /// timestep and maximum frame time. The scheduler handle is stored
258    /// internally and can be stopped via `stop`. If `register_input` has
259    /// attached DOM listeners, the shared input state is closed out at the
260    /// end of every frame so edge-triggered queries (`keys_pressed`,
261    /// `keys_released`, `mouse_buttons_pressed`, `touch_started`) only
262    /// report the frame they occurred in. Tasks registered through
263    /// [`EngineHandle::register_task`] are advanced once per fixed step,
264    /// immediately after the handler's `on_update` callback.
265    ///
266    /// # Arguments
267    ///
268    /// - `TickHandlerRc` - The tick handler receiving update and render callbacks.
269    pub fn start(&mut self, handler: TickHandlerRc) {
270        let scheduler_config: SchedulerConfig = self.get_config().get_scheduler();
271        let input_cell: Option<&InputStateCell> = self.try_get_input_cell().as_ref();
272        let tasks: &TaskRegistryRc = self.get_tasks();
273        self.set_scheduler_handle(Some(SchedulerHandle::start(
274            scheduler_config,
275            handler,
276            Some(tasks),
277            input_cell,
278        )));
279    }
280
281    /// Returns the shared task registry, for direct inspection or
282    /// registration outside [`EngineHandle::register_task`].
283    ///
284    /// # Returns
285    ///
286    /// - `&TaskRegistryRc` - The engine's task registry.
287    pub fn tasks(&self) -> &TaskRegistryRc {
288        self.get_tasks()
289    }
290
291    /// Registers a task to be advanced on every fixed step.
292    ///
293    /// The task is driven by [`SchedulerState::tick`] immediately after the
294    /// handler's `on_update` callback, with the configured
295    /// [`SchedulerConfig::get_fixed_timestep`] delta. This is the entry
296    /// point that gives `Timer`, `Tween`, `ParticleEmitter`, `Entity`,
297    /// `Animator`, `SceneManager`, and the physics worlds a heartbeat — all
298    /// of them implement [`Updatable`] but none were driven before the
299    /// registry existed.
300    ///
301    /// Registration order is preserved: tasks are updated in the order they
302    /// were registered.
303    ///
304    /// # Arguments
305    ///
306    /// - `T` - The updater to register. Must implement [`Updatable`].
307    ///
308    /// # Returns
309    ///
310    /// - `TaskHandle` - A handle used to unregister the task.
311    pub fn register_task<T>(&self, task: T) -> TaskHandle
312    where
313        T: Updatable + 'static,
314    {
315        self.get_tasks().get_mut().register(task)
316    }
317
318    /// Removes a previously registered task.
319    ///
320    /// # Arguments
321    ///
322    /// - `&TaskHandle` - The handle returned by [`EngineHandle::register_task`].
323    ///
324    /// # Returns
325    ///
326    /// - `bool` - `true` if a task was removed, `false` if the handle did
327    ///   not match any registered task.
328    pub fn unregister_task(&self, handle: &TaskHandle) -> bool {
329        self.get_tasks().get_mut().unregister(handle)
330    }
331
332    /// Creates the engine's asset loader and drives it from the game loop.
333    ///
334    /// The loader implements [`Updatable`], so it is registered with the
335    /// scheduler's [`TaskRegistry`] rather than polled by hand. On every
336    /// fixed step it releases the `onload` / `onerror` callbacks of loads
337    /// that have already settled — a `wasm_bindgen::Closure` cannot be
338    /// dropped from inside the callback that is running, so this engine
339    /// update is the only safe drop point. Without this the loader would
340    /// pin two closures per asset forever and `is_all_loaded` would never
341    /// observe the decrement of the pending count.
342    ///
343    /// Calling this more than once returns the already-created loader rather
344    /// than replacing it, so pending loads are not orphaned.
345    ///
346    /// # Returns
347    ///
348    /// - `AssetLoader` - The engine's asset loader.
349    pub fn register_assets(&mut self) -> AssetLoader {
350        if let Some(existing) = self.try_get_asset_loader() {
351            return existing.clone();
352        }
353        let loader: AssetLoader = AssetLoader::default();
354        self.set_asset_loader(Some(loader.clone()));
355        let _ = self.register_task(loader.clone());
356        loader
357    }
358
359    /// Returns the engine's asset loader, if one has been created.
360    ///
361    /// # Returns
362    ///
363    /// - `Option<AssetLoader>` - The loader, or `None` when
364    ///   [`EngineHandle::register_assets`] has not been called.
365    pub fn asset_loader(&self) -> Option<AssetLoader> {
366        self.try_get_asset_loader().as_ref().cloned()
367    }
368
369    /// Stops the game loop and cancels any pending animation frame request.
370    pub fn stop(&self) {
371        if let Some(handle) = self.try_get_scheduler_handle().as_ref() {
372            handle.stop();
373        }
374    }
375
376    /// Returns whether the game loop is currently running.
377    ///
378    /// # Returns
379    ///
380    /// - `bool` - `true` if the scheduler is running.
381    pub fn is_running(&self) -> bool {
382        self.try_get_scheduler_handle()
383            .as_ref()
384            .is_some_and(|handle: &SchedulerHandle| handle.is_running())
385    }
386}