limnus_window/lib.rs
1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/limnus
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use crate::dpi::PhysicalSize;
6use std::sync::Arc;
7use tracing::debug;
8use winit::application::ApplicationHandler;
9use winit::dpi;
10use winit::dpi::PhysicalPosition;
11use winit::error::EventLoopError;
12use winit::event::{
13 DeviceEvent, DeviceId, ElementState, InnerSizeWriter, MouseButton, MouseScrollDelta, Touch,
14 TouchPhase, WindowEvent,
15};
16use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
17use winit::keyboard::PhysicalKey;
18use winit::window::{Fullscreen, Window, WindowAttributes, WindowId, WindowLevel};
19
20#[cfg(target_arch = "wasm32")]
21use tracing::trace;
22
23#[cfg(target_arch = "wasm32")]
24use winit::platform::web::WindowAttributesExtWebSys;
25
26#[cfg(target_arch = "wasm32")]
27use web_sys::window;
28
29#[cfg(target_arch = "wasm32")]
30use web_sys::wasm_bindgen::JsCast;
31use winit::window::Fullscreen::Borderless;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum WindowMode {
35 WindowedFullscreen,
36 Windowed,
37 WindowedAlwaysOnTop,
38}
39
40/// `AppHandler` - Handle window, cursor, mouse and keyboard events, designed for games and graphical applications.
41///
42/// Think of `AppHandler` as your app’s backstage crew, handling everything
43/// from window setup to keyboard and mouse inputs, and making sure each frame
44/// redraws smoothly.
45pub trait AppHandler {
46 // Query functions
47
48 /// Returns the minimum window size (width, height) in pixels that the application requires.
49 ///
50 /// This can be used to enforce a minimum size on the window, preventing it from
51 /// being resized below this dimension.
52 fn min_size(&self) -> (u16, u16);
53
54 fn window_mode(&self) -> WindowMode;
55
56 /// Returns the starting window size (width, height) in pixels when the application launches.
57 ///
58 /// This size will be used to set the initial window dimensions on startup.
59 fn start_size(&self) -> (u16, u16);
60
61 fn cursor_should_be_visible(&self) -> bool;
62
63 // Window Events
64
65 /// Called to trigger a redraw of the application’s content.
66 ///
67 /// This method is generally called when the window needs to refresh its
68 /// contents, such as after a resize or focus change.
69 /// Return false if application should close
70 fn redraw(&mut self) -> bool;
71
72 /// Called when the application window gains focus.
73 ///
74 /// This can be used to resume or activate specific behaviors when the window
75 /// becomes active.
76 fn got_focus(&mut self);
77
78 /// Called when the application window loses focus.
79 ///
80 /// Useful for suspending actions or input handling when the application
81 /// window is not in the foreground.
82 fn lost_focus(&mut self);
83
84 /// Called after the application window has been created and is ready to use.
85 ///
86 /// Use this method to perform any initialization that requires access to the window,
87 /// such as setting up rendering contexts.
88 ///
89 /// # Parameters
90 /// - `window`: A reference-counted pointer to the application window.
91 fn window_created(&mut self, window: Arc<Window>);
92
93 /// Called whenever the window is resized, providing the new physical size.
94 ///
95 /// This method should handle adjustments to the application’s layout and content
96 /// based on the window’s new dimensions.
97 ///
98 /// # Parameters
99 /// - `size`: The new size of the window in physical pixels.
100 fn resized(&mut self, size: PhysicalSize<u32>);
101
102 // Keyboard Events
103
104 /// Processes keyboard input events, such as key presses and releases.
105 ///
106 /// # Parameters
107 /// - `element_state`: Indicates whether the key is pressed or released.
108 /// - `physical_key`: The physical key that was pressed or released.
109 fn keyboard_input(&mut self, element_state: ElementState, physical_key: PhysicalKey);
110
111 // Cursor (Pointer) Events
112
113 /// Called when the cursor enters the window.
114 ///
115 /// This can trigger visual changes or status updates when the cursor moves
116 /// into the application window area.
117 fn cursor_entered(&mut self);
118
119 /// Called when the cursor leaves the window.
120 ///
121 /// This can be used to revert visual changes or trigger actions when the
122 /// cursor exits the application window.
123 fn cursor_left(&mut self);
124
125 /// Handles cursor movement within the window, providing the new position.
126 ///
127 /// # Parameters
128 /// - `physical_position`: The current position of the cursor in physical
129 /// screen coordinates.
130 fn cursor_moved(&mut self, physical_position: PhysicalPosition<u32>);
131
132 // Mouse Events
133
134 /// Handles mouse button input events, such as presses and releases.
135 ///
136 /// # Parameters
137 /// - `element_state`: Indicates whether the mouse button is pressed or released.
138 /// - `button`: The mouse button that was pressed or released.
139 fn mouse_input(&mut self, element_state: ElementState, button: MouseButton);
140
141 /// Processes mouse wheel events, which indicate scrolling actions.
142 ///
143 /// # Parameters
144 /// - `delta`: The amount of scroll, which may be specified in lines or pixels.
145 /// - `touch_phase`: The phase of the scroll gesture, which can indicate
146 /// the start, movement, or end of the gesture.
147 fn mouse_wheel(&mut self, delta: MouseScrollDelta, touch_phase: TouchPhase);
148
149 fn pinch_gesture(&mut self, delta: f64, touch_phase: TouchPhase);
150
151 /// Handles mouse motion. the delta follows no standard, so it is up to the game to apply
152 /// a factor as it sees fit.
153 fn mouse_motion(&mut self, delta: (f64, f64));
154
155 // Touch Events
156
157 /// Handles touch input events, such as screen touches and gestures.
158 ///
159 /// # Parameters
160 /// - `touch`: Describes the touch event, including position, phase, and other
161 /// touch-specific information.
162 fn touch(&mut self, touch: Touch);
163
164 // Environment or Screen Events
165
166 /// Handles changes to the display scale factor, usually due to monitor DPI changes.
167 ///
168 /// This method receives the new scale factor and a writer to update the inner
169 /// size of the application.
170 ///
171 /// # Parameters
172 /// - `scale_factor`: The new scale factor, which may be applied to adjust
173 /// rendering.
174 /// - `inner_size_writer`: A writer to update the inner size.
175 fn scale_factor_changed(&mut self, scale_factor: f64, inner_size_writer: InnerSizeWriter);
176}
177
178struct App<'a> {
179 window: Option<Arc<Window>>,
180 handler: &'a mut (dyn AppHandler),
181 is_focused: bool,
182 cursor_is_visible: bool,
183 title: String,
184
185 // TODO: Move these
186 min_physical_size: PhysicalSize<u32>,
187 start_physical_size: PhysicalSize<u32>,
188 mode: WindowMode,
189 last_set_inner_size: PhysicalSize<u32>,
190}
191
192impl<'a> App<'a> {
193 pub fn new(
194 handler: &'a mut dyn AppHandler,
195 title: &str,
196 min_size: (u16, u16),
197 start_size: (u16, u16),
198 mode: WindowMode,
199 ) -> Self {
200 let min_physical_size = PhysicalSize::new(min_size.0 as u32, min_size.1 as u32);
201 let start_physical_size = PhysicalSize::new(start_size.0 as u32, start_size.1 as u32);
202
203 Self {
204 handler,
205 window: None,
206 is_focused: false,
207 cursor_is_visible: true,
208 mode,
209 title: title.to_string(),
210 min_physical_size,
211 start_physical_size,
212 last_set_inner_size: start_physical_size,
213 }
214 }
215
216 pub fn set_mode(&mut self, mode: &WindowMode) {
217 let window_ref = self.window.as_ref().unwrap();
218 match mode {
219 WindowMode::WindowedFullscreen => {
220 window_ref.set_window_level(WindowLevel::Normal);
221 window_ref.set_fullscreen(Some(Borderless(None)));
222 }
223 WindowMode::Windowed => {
224 window_ref.set_window_level(WindowLevel::Normal);
225 window_ref.set_fullscreen(None);
226 }
227 WindowMode::WindowedAlwaysOnTop => {
228 window_ref.set_fullscreen(None);
229 window_ref.set_window_level(WindowLevel::AlwaysOnTop);
230 }
231 }
232 self.mode = mode.clone();
233 }
234}
235
236impl ApplicationHandler for App<'_> {
237 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
238 if self.window.is_none() {
239 debug!("creating new window");
240
241 let window_attributes: WindowAttributes;
242
243 #[cfg(not(target_arch = "wasm32"))]
244 {
245 let mut calculated_window_attributes = WindowAttributes::default()
246 .with_title(self.title.as_str())
247 .with_resizable(true)
248 .with_inner_size(self.start_physical_size)
249 .with_min_inner_size(self.min_physical_size);
250
251 if let WindowMode::WindowedFullscreen = self.mode {
252 calculated_window_attributes = calculated_window_attributes
253 .with_fullscreen(Some(Fullscreen::Borderless(None)));
254 }
255
256 window_attributes = calculated_window_attributes;
257 }
258
259 #[cfg(target_arch = "wasm32")]
260 {
261 // Create the window attributes
262 let canvas = window()
263 .unwrap()
264 .document()
265 .unwrap()
266 .get_element_by_id("limnus_canvas")
267 .expect("should have a 'limnus_canvas' canvas in the html (dom)")
268 .dyn_into::<web_sys::HtmlCanvasElement>()
269 .unwrap();
270
271 {
272 trace!(
273 ?canvas,
274 "found canvas {}x{}",
275 canvas.width(),
276 canvas.height()
277 );
278 window_attributes = WindowAttributes::default().with_canvas(Some(canvas));
279 }
280 }
281
282 let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
283
284 self.window = Some(window.clone());
285
286 #[cfg(not(target_arch = "wasm32"))]
287 if let WindowMode::WindowedAlwaysOnTop = &self.mode {
288 window.set_window_level(WindowLevel::AlwaysOnTop);
289 }
290
291 self.handler.window_created(window);
292
293 // This tells winit that we want another frame after this one
294 self.window.as_ref().unwrap().request_redraw();
295 }
296 }
297
298 fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
299 if self.window.is_none() {
300 return;
301 }
302 if id != self.window.as_ref().unwrap().id() {
303 return;
304 }
305
306 match event {
307 WindowEvent::CloseRequested => {
308 event_loop.exit();
309 }
310 WindowEvent::Destroyed => {
311 self.window = None;
312 }
313 WindowEvent::Resized(physical_size) => {
314 self.handler.resized(physical_size);
315
316 // This tells winit that we want another frame after this one
317 self.window.as_ref().unwrap().request_redraw();
318 }
319 WindowEvent::RedrawRequested => {
320 // This tells winit that we want another frame after this one
321 self.window.as_ref().unwrap().request_redraw();
322
323 let window = self.window.as_mut().unwrap();
324 let cursor_visible_request = self.handler.cursor_should_be_visible();
325 if cursor_visible_request != self.cursor_is_visible {
326 window.set_cursor_visible(cursor_visible_request);
327 self.cursor_is_visible = cursor_visible_request;
328 }
329
330 let requested_mode = self.handler.window_mode();
331 if self.mode != requested_mode {
332 self.set_mode(&requested_mode);
333 }
334
335 if let Some(found_window) = &self.window {
336 let requested_size_tuple = self.handler.start_size();
337 let requested_new_size = PhysicalSize::new(
338 u32::from(requested_size_tuple.0),
339 u32::from(requested_size_tuple.1),
340 );
341
342 if requested_new_size.width != self.last_set_inner_size.width
343 || requested_new_size.height != self.last_set_inner_size.height
344 {
345 debug!(?requested_new_size, "new window inner size requested");
346 let _ = found_window.request_inner_size(requested_new_size);
347 self.last_set_inner_size = requested_new_size;
348 }
349
350 let wants_to_keep_going = self.handler.redraw();
351 if !wants_to_keep_going {
352 event_loop.exit();
353 }
354 }
355 }
356 WindowEvent::Focused(is_focus) => {
357 self.is_focused = is_focus;
358 if is_focus {
359 self.handler.got_focus();
360 } else {
361 // usually you might want to stop or lower audio, maybe lower rendering frequency, etc
362 self.handler.lost_focus();
363 }
364 }
365 WindowEvent::KeyboardInput { event, .. } => {
366 if event.repeat {
367 return;
368 }
369 self.handler.keyboard_input(event.state, event.physical_key);
370 }
371
372 WindowEvent::CursorMoved { position, .. } => self.handler.cursor_moved(
373 dpi::PhysicalPosition::<u32>::new(position.x as u32, position.y as u32),
374 ),
375
376 WindowEvent::CursorEntered { .. } => self.handler.cursor_entered(),
377
378 WindowEvent::CursorLeft { .. } => self.handler.cursor_left(),
379
380 WindowEvent::MouseWheel { delta, phase, .. } => self.handler.mouse_wheel(delta, phase),
381
382 WindowEvent::MouseInput { state, button, .. } => {
383 self.handler.mouse_input(state, button);
384 }
385
386 WindowEvent::Touch(touch_data) => self.handler.touch(touch_data),
387
388 WindowEvent::ScaleFactorChanged {
389 scale_factor,
390 inner_size_writer,
391 } =>
392 // Changing the display’s resolution.
393 // Changing the display’s scale factor (e.g. in Control Panel on Windows).
394 // Moving the window to a display with a different scale factor.
395 {
396 self.handler
397 .scale_factor_changed(scale_factor, inner_size_writer)
398 }
399
400 WindowEvent::PinchGesture { delta, phase, .. } => {
401 // Opinionated: pinch in feels like a positive movement
402 let correct_delta = -delta;
403 self.handler.pinch_gesture(correct_delta, phase);
404 }
405
406 // --------------------------------------------
407
408 // WindowEvent::Ime(_) => {} // IME is outside the scope of events, and not supported on all platforms, e.g. Web.
409
410 // Gestures could be relevant, but we leave them for future versions
411 //WindowEvent::PinchGesture { .. } => {}
412 //WindowEvent::PanGesture { .. } => {}
413 //WindowEvent::DoubleTapGesture { .. } => {}
414 //WindowEvent::RotationGesture { .. } => {}
415 // WindowEvent::TouchpadPressure { .. } => {} // only on some macbooks and similar, not relevant for multiplatform games.
416 //WindowEvent::AxisMotion { .. } => {} // intentionally not supported, since we want to use platform-specific api:s for gamepad input
417 // WindowEvent::ThemeChanged(_) => {} // mostly unsupported and not really related to games.
418 // WindowEvent::Occluded(_) => {} not available on most platforms anyway
419 // WindowEvent::ActivationTokenDone { .. } => {} winit handles this normally, so no need to implement it.
420 // WindowEvent::Moved(_) => {} // since this is not supported on all platforms, it should not be exposed in this library
421 // WindowEvent::Destroyed => {} // this is handled internally
422 // since this crate is mostly for games, this file operations are outside the scope.
423 //WindowEvent::DroppedFile(_) => {}
424 //WindowEvent::HoveredFile(_) => {}
425 //WindowEvent::HoveredFileCancelled => {}
426 _ => {}
427 }
428 }
429
430 fn device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, event: DeviceEvent) {
431 if let DeviceEvent::MouseMotion { delta } = event {
432 if self.is_focused {
433 self.handler.mouse_motion(delta);
434 }
435 }
436 /*
437 match event {
438 // DeviceEvent::MouseWheel { .. } => {},
439 //DeviceEvent::Button { .. } => { }
440 //DeviceEvent::Added => {}
441 //DeviceEvent::Removed => {}
442 //DeviceEvent::Motion { .. } => { }
443 //DeviceEvent::Key(_) => {}
444 _ => {}
445 }
446 */
447 }
448
449 fn suspended(&mut self, _: &ActiveEventLoop) {}
450
451 fn exiting(&mut self, _: &ActiveEventLoop) {}
452}
453
454/// A struct responsible for managing the application window lifecycle.
455///
456/// The `WindowRunner` struct provides functionality to run an application
457/// that utilizes an event loop for window management. It abstracts the details
458/// of creating and running the event loop, making it easier to integrate window
459/// handling into your game application.
460pub struct WindowRunner;
461
462impl WindowRunner {
463 /// Runs the application with the provided handler.
464 ///
465 /// This method initializes an event loop and starts the application by
466 /// executing the provided `AppHandler`. The event loop runs in a polling
467 /// mode, allowing for responsive event handling. It is not guaranteed to ever return.
468 ///
469 /// # Parameters
470 ///
471 /// - `handler`: A mutable reference to an object implementing the `AppHandler`
472 /// trait, which defines the behavior of the application in response to events.
473 ///
474 /// # Returns
475 ///
476 /// This method returns a `Result<(), EventLoopError>`.
477 /// If an error occurs during event loop creation, it returns an `EventLoopError`.
478 ///
479 /// # Note
480 ///
481 /// It is not guaranteed to ever return, as the event loop will run indefinitely
482 /// until the application is terminated.
483 pub fn run_app(handler: &mut dyn AppHandler, title: &str) -> Result<(), EventLoopError> {
484 let event_loop = EventLoop::new()?;
485 event_loop.set_control_flow(ControlFlow::Poll);
486 let min_size = handler.min_size();
487 let start_size = handler.start_size();
488 let mode = handler.window_mode();
489 let mut app = App::new(handler, title, min_size, start_size, mode);
490 let _ = event_loop.run_app(&mut app);
491 Ok(())
492 }
493}