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        EngineHandle::new(config, None, None, None, None)
23    }
24
25    /// Runs the engine through its complete lifecycle in a single async call.
26    ///
27    /// Equivalent to calling `Engine::new_handle(config)` followed by
28    /// `init_canvas` / `init_webgpu` (matching the chosen backend) and
29    /// `start(handler)`. The returned handle can still be used to stop
30    /// the loop later via `handle.stop()`.
31    ///
32    /// # Arguments
33    ///
34    /// - `EngineConfig` - The engine configuration.
35    /// - `TickHandlerRc` - The tick handler receiving update and render callbacks.
36    ///
37    /// # Returns
38    ///
39    /// - `EngineHandle` - A handle to the running engine. If renderer
40    ///   initialization failed the handle's renderer field will be `None`
41    ///   and `is_running` will be `false`. Initialization errors are not
42    ///   logged inside the engine; callers should call `init_webgpu`
43    ///   directly if they need access to the typed `WebGpuInitError`.
44    pub async fn run(config: EngineConfig, handler: TickHandlerRc) -> EngineHandle {
45        let mut handle: EngineHandle = Engine::new_handle(config);
46        match handle.get_config().get_render().get_backend() {
47            RenderBackendType::Canvas2D => {
48                handle.init_canvas();
49            }
50            RenderBackendType::WebGpu => {
51                let _: Result<WebGpuRenderer, WebGpuInitError> = handle.init_webgpu().await;
52            }
53            RenderBackendType::WebGl => {
54                let _: Result<WebGlRenderer, WebGlInitError> = handle.init_webgl();
55            }
56        }
57        handle.start(handler);
58        handle
59    }
60
61    /// Returns the default engine configuration.
62    ///
63    /// Uses `RenderConfig::default()` (Canvas 2D backend) and the default
64    /// scheduler configuration (60 Hz fixed timestep).
65    ///
66    /// # Returns
67    ///
68    /// - `EngineConfig` - The default engine configuration.
69    pub fn default_config() -> EngineConfig {
70        EngineConfig::default()
71    }
72
73    /// Creates a Canvas 2D renderer directly from a render configuration.
74    ///
75    /// Use this when you want a `CanvasRenderer` without going through
76    /// the full `EngineHandle` lifecycle (for example, in a custom render
77    /// loop that does not use the fixed-timestep scheduler).
78    ///
79    /// # Arguments
80    ///
81    /// - `&RenderConfig` - The rendering configuration.
82    ///
83    /// # Returns
84    ///
85    /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas element was not found.
86    pub fn canvas_renderer(config: &RenderConfig) -> Option<CanvasRenderer> {
87        CanvasRenderer::from_selector(
88            config.get_canvas_selector(),
89            config.get_width(),
90            config.get_height(),
91        )
92    }
93
94    /// Creates a WebGPU renderer directly from a render configuration.
95    ///
96    /// Async because GPU adapter and device acquisition returns JavaScript
97    /// Promises that must be awaited. Mirrors `Engine::canvas_renderer` for
98    /// callers that want the renderer without the full engine handle.
99    ///
100    /// Failures are surfaced as `WebGpuInitError` so the caller can decide
101    /// how to react (typically by logging via `Console::error` or by
102    /// falling back to the Canvas 2D backend).
103    ///
104    /// # Arguments
105    ///
106    /// - `&RenderConfig` - The rendering configuration.
107    ///
108    /// # Returns
109    ///
110    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer,
111    ///   or a typed error describing the specific failure.
112    pub async fn webgpu_renderer(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
113        WebGpuRenderer::init(config).await
114    }
115
116    /// Creates a WebGL 2 renderer directly from a render configuration.
117    ///
118    /// Unlike [`Engine::webgpu_renderer`] this is synchronous because WebGL
119    /// context acquisition is a plain DOM call with no Promises involved.
120    ///
121    /// # Arguments
122    ///
123    /// - `&RenderConfig` - The rendering configuration.
124    ///
125    /// # Returns
126    ///
127    /// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
128    ///   or a typed error describing the specific failure.
129    pub fn webgl_renderer(config: &RenderConfig) -> Result<WebGlRenderer, WebGlInitError> {
130        WebGlRenderer::init(config)
131    }
132}
133
134/// Implements lifecycle management for `EngineHandle`.
135///
136/// All getter methods (`get_config`, `get_canvas_renderer`, `get_webgpu_renderer`)
137/// are generated by the `Data` derive on the struct definition; this impl
138/// only contributes lifecycle and accessor behavior that is not expressible
139/// as a plain getter.
140impl EngineHandle {
141    /// Initializes the Canvas 2D rendering backend.
142    ///
143    /// On success, populates `canvas_renderer` and clears `webgpu_renderer`.
144    /// On failure, both renderer fields remain `None`.
145    ///
146    /// # Returns
147    ///
148    /// - `bool` - `true` if the renderer was created successfully.
149    pub fn init_canvas(&mut self) -> bool {
150        let render_config: &RenderConfig = &self.get_config().get_render();
151        let renderer: Option<CanvasRenderer> = CanvasRenderer::from_selector(
152            render_config.get_canvas_selector(),
153            render_config.get_width(),
154            render_config.get_height(),
155        );
156        match renderer {
157            Some(r) => {
158                self.set_canvas_renderer(Some(r));
159                self.set_webgpu_renderer(None);
160                self.set_webgl_renderer(None);
161                true
162            }
163            None => {
164                self.set_canvas_renderer(None);
165                self.set_webgpu_renderer(None);
166                self.set_webgl_renderer(None);
167                false
168            }
169        }
170    }
171
172    /// Initializes the WebGPU rendering backend.
173    ///
174    /// On success, populates `webgpu_renderer` and clears `canvas_renderer`.
175    /// On failure, both renderer fields remain `None` and the typed error is
176    /// returned so the caller can decide how to surface it (typically via
177    /// `Console::error`).
178    ///
179    /// # Returns
180    ///
181    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer,
182    ///   or a typed error describing the specific failure.
183    pub async fn init_webgpu(&mut self) -> Result<WebGpuRenderer, WebGpuInitError> {
184        let render_config: &RenderConfig = &self.get_config().get_render();
185        let renderer: WebGpuRenderer = WebGpuRenderer::init(render_config).await?;
186        self.set_webgpu_renderer(Some(renderer.clone()));
187        self.set_canvas_renderer(None);
188        self.set_webgl_renderer(None);
189        Ok(renderer)
190    }
191
192    /// Initializes the WebGL 2 rendering backend.
193    ///
194    /// On success, populates `webgl_renderer` and clears the other renderer
195    /// fields. On failure, all renderer fields remain `None` and the typed
196    /// error is returned so the caller can decide how to surface it.
197    ///
198    /// # Returns
199    ///
200    /// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
201    ///   or a typed error describing the specific failure.
202    pub fn init_webgl(&mut self) -> Result<WebGlRenderer, WebGlInitError> {
203        let render_config: &RenderConfig = &self.get_config().get_render();
204        let renderer: WebGlRenderer = WebGlRenderer::init(render_config)?;
205        self.set_webgl_renderer(Some(renderer.clone()));
206        self.set_canvas_renderer(None);
207        self.set_webgpu_renderer(None);
208        Ok(renderer)
209    }
210
211    /// Attaches DOM input listeners and stores the shared input state.
212    ///
213    /// Resolves the canvas element from the render configuration's
214    /// `canvas_selector` and routes DOM events into a shared [`InputState`]:
215    /// keyboard events bind to `window`, mouse and touch events bind to the
216    /// canvas. This works for both the Canvas 2D and WebGPU backends because
217    /// either way the same `<canvas>` element receives the pointer events.
218    ///
219    /// Returns `None` (and stores nothing) if the canvas selector does not
220    /// resolve at call time - for example when called before the canvas
221    /// element is mounted into the DOM.
222    ///
223    /// The registered listeners stay alive for the lifetime of the document
224    /// (mount-only convention; there is no matching `unregister_input`).
225    ///
226    /// # Returns
227    ///
228    /// - `Option<InputStateCell>` - The shared input state cell, or `None`
229    ///   if the canvas element was not found.
230    pub fn register_input(&mut self) -> Option<InputStateCell> {
231        if let Some(existing) = self.try_get_input_cell() {
232            return Some(existing.clone());
233        }
234        let window_value: Window = window().expect("no global window exists");
235        let document_value: Document = window_value.document().expect("should have a document");
236        let render_config: &RenderConfig = &self.get_config().get_render();
237        let canvas_selector: String = render_config.get_canvas_selector();
238        let element: Element = document_value
239            .query_selector(canvas_selector.as_ref())
240            .ok()
241            .flatten()?;
242        let target: EventTarget = element.into();
243        let cell: InputStateCell = Input::attach(
244            Rc::new(EngineCell::new(InputState::default())),
245            &window_value,
246            &target,
247        );
248        self.set_input_cell(Some(cell.clone()));
249        Some(cell)
250    }
251
252    /// Starts the game loop with the given tick handler.
253    ///
254    /// The scheduler configuration from `EngineConfig` controls the fixed
255    /// timestep and maximum frame time. The scheduler handle is stored
256    /// internally and can be stopped via `stop`.
257    ///
258    /// # Arguments
259    ///
260    /// - `TickHandlerRc` - The tick handler receiving update and render callbacks.
261    pub fn start(&mut self, handler: TickHandlerRc) {
262        let scheduler_config: SchedulerConfig = self.get_config().get_scheduler();
263        self.set_scheduler_handle(Some(SchedulerHandle::start(scheduler_config, handler)));
264    }
265
266    /// Stops the game loop and cancels any pending animation frame request.
267    pub fn stop(&self) {
268        if let Some(handle) = self.try_get_scheduler_handle().as_ref() {
269            handle.stop();
270        }
271    }
272
273    /// Returns whether the game loop is currently running.
274    ///
275    /// # Returns
276    ///
277    /// - `bool` - `true` if the scheduler is running.
278    pub fn is_running(&self) -> bool {
279        self.try_get_scheduler_handle()
280            .as_ref()
281            .is_some_and(|handle: &SchedulerHandle| handle.is_running())
282    }
283}