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}