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}