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(u32::from(min_size.0), u32::from(min_size.1));
201 let start_physical_size =
202 PhysicalSize::new(u32::from(start_size.0), u32::from(start_size.1));
203
204 Self {
205 handler,
206 window: None,
207 is_focused: false,
208 cursor_is_visible: true,
209 mode,
210 title: title.to_string(),
211 min_physical_size,
212 start_physical_size,
213 last_set_inner_size: start_physical_size,
214 }
215 }
216
217 pub fn set_mode(&mut self, mode: &WindowMode) {
218 let window_ref = self.window.as_ref().unwrap();
219 match mode {
220 WindowMode::WindowedFullscreen => {
221 window_ref.set_window_level(WindowLevel::Normal);
222 window_ref.set_fullscreen(Some(Borderless(None)));
223 }
224 WindowMode::Windowed => {
225 window_ref.set_window_level(WindowLevel::Normal);
226 window_ref.set_fullscreen(None);
227 }
228 WindowMode::WindowedAlwaysOnTop => {
229 window_ref.set_fullscreen(None);
230 window_ref.set_window_level(WindowLevel::AlwaysOnTop);
231 }
232 }
233 self.mode = mode.clone();
234 }
235
236 pub fn internal_resized(&mut self, physical_size: PhysicalSize<u32>) {
237 debug!("internal resized: {:?}", physical_size);
238 self.handler.resized(physical_size);
239 }
240
241 pub fn create_window(&mut self, active_event_loop: &ActiveEventLoop) {
242 assert!(self.window.is_none());
243 debug!("creating new window");
244
245 let window_attributes: WindowAttributes;
246
247 #[cfg(not(target_arch = "wasm32"))]
248 {
249 let mut calculated_window_attributes = WindowAttributes::default()
250 .with_title(self.title.as_str())
251 .with_resizable(true)
252 .with_inner_size(self.start_physical_size)
253 .with_min_inner_size(self.min_physical_size);
254
255 if self.mode == WindowMode::WindowedFullscreen {
256 calculated_window_attributes = calculated_window_attributes
257 .with_fullscreen(Some(Fullscreen::Borderless(None)));
258 }
259
260 window_attributes = calculated_window_attributes;
261 }
262
263 #[cfg(target_arch = "wasm32")]
264 {
265 // Create the window attributes
266 let canvas = window()
267 .unwrap()
268 .document()
269 .unwrap()
270 .get_element_by_id("limnus_canvas")
271 .expect("should have a 'limnus_canvas' canvas in the html (dom)")
272 .dyn_into::<web_sys::HtmlCanvasElement>()
273 .unwrap();
274
275 {
276 trace!(
277 ?canvas,
278 "found canvas {}x{}",
279 canvas.width(),
280 canvas.height()
281 );
282 window_attributes = WindowAttributes::default().with_canvas(Some(canvas));
283 }
284 }
285
286 let window = Arc::new(active_event_loop.create_window(window_attributes).unwrap());
287
288 self.window = Some(window.clone());
289
290 #[cfg(not(target_arch = "wasm32"))]
291 if matches!(&self.mode, WindowMode::WindowedAlwaysOnTop) {
292 window.set_window_level(WindowLevel::AlwaysOnTop);
293 }
294
295 self.handler.window_created(window);
296 }
297}
298
299impl ApplicationHandler for App<'_> {
300 // Resumed isn't called for all platforms
301 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
302 if self.window.is_none() {
303 self.create_window(event_loop);
304 // This tells winit that we want another frame after this one
305 self.window.as_ref().unwrap().request_redraw();
306 }
307 }
308
309 fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
310 if let Some(window) = &self.window
311 && id != window.id()
312 {
313 return;
314 }
315
316 // HACK: handle some events and try to create the window
317 if let WindowEvent::Resized(physical_size) = event
318 && self.window.is_none()
319 {
320 self.create_window(event_loop);
321 }
322
323 let Some(window) = self.window.as_mut() else {
324 return;
325 };
326
327 match event {
328 WindowEvent::CloseRequested => {
329 event_loop.exit();
330 }
331 WindowEvent::Destroyed => {
332 self.window = None;
333 }
334 WindowEvent::Resized(physical_size) => {
335 // This tells winit that we want another frame after this one
336 window.request_redraw();
337 self.internal_resized(physical_size);
338 }
339 WindowEvent::RedrawRequested => {
340 // This tells winit that we want another frame after this one
341 window.request_redraw();
342
343 //let window = self.window.as_mut().unwrap();
344 let cursor_visible_request = self.handler.cursor_should_be_visible();
345 if cursor_visible_request != self.cursor_is_visible {
346 window.set_cursor_visible(cursor_visible_request);
347 self.cursor_is_visible = cursor_visible_request;
348 }
349
350 let requested_mode = self.handler.window_mode();
351 if self.mode != requested_mode {
352 self.set_mode(&requested_mode);
353 }
354
355 if let Some(found_window) = &self.window {
356 let requested_size_tuple = self.handler.start_size();
357 let requested_new_size = PhysicalSize::new(
358 u32::from(requested_size_tuple.0),
359 u32::from(requested_size_tuple.1),
360 );
361
362 if requested_new_size.width != self.last_set_inner_size.width
363 || requested_new_size.height != self.last_set_inner_size.height
364 {
365 debug!(?requested_new_size, "new window inner size requested");
366 let _ = found_window.request_inner_size(requested_new_size);
367 self.last_set_inner_size = requested_new_size;
368 }
369
370 let wants_to_keep_going = self.handler.redraw();
371 if !wants_to_keep_going {
372 event_loop.exit();
373 }
374 }
375 }
376 WindowEvent::Focused(is_focus) => {
377 self.is_focused = is_focus;
378 if is_focus {
379 self.handler.got_focus();
380 } else {
381 // usually you might want to stop or lower audio, maybe lower rendering frequency, etc
382 self.handler.lost_focus();
383 }
384 }
385 WindowEvent::KeyboardInput { event, .. } => {
386 if event.repeat {
387 return;
388 }
389 self.handler.keyboard_input(event.state, event.physical_key);
390 }
391
392 WindowEvent::CursorMoved { position, .. } => self.handler.cursor_moved(
393 dpi::PhysicalPosition::<u32>::new(position.x as u32, position.y as u32),
394 ),
395
396 WindowEvent::CursorEntered { .. } => self.handler.cursor_entered(),
397
398 WindowEvent::CursorLeft { .. } => self.handler.cursor_left(),
399
400 WindowEvent::MouseWheel { delta, phase, .. } => self.handler.mouse_wheel(delta, phase),
401
402 WindowEvent::MouseInput { state, button, .. } => {
403 self.handler.mouse_input(state, button);
404 }
405
406 WindowEvent::Touch(touch_data) => self.handler.touch(touch_data),
407
408 WindowEvent::ScaleFactorChanged {
409 scale_factor,
410 inner_size_writer,
411 } =>
412 // Changing the display’s resolution.
413 // Changing the display’s scale factor (e.g. in Control Panel on Windows).
414 // Moving the window to a display with a different scale factor.
415 {
416 self.handler
417 .scale_factor_changed(scale_factor, inner_size_writer);
418 }
419
420 WindowEvent::PinchGesture { delta, phase, .. } => {
421 // Opinionated: pinch in feels like a positive movement
422 let correct_delta = -delta;
423 self.handler.pinch_gesture(correct_delta, phase);
424 }
425
426 // --------------------------------------------
427
428 // WindowEvent::Ime(_) => {} // IME is outside the scope of events, and not supported on all platforms, e.g. Web.
429
430 // Gestures could be relevant, but we leave them for future versions
431 //WindowEvent::PinchGesture { .. } => {}
432 //WindowEvent::PanGesture { .. } => {}
433 //WindowEvent::DoubleTapGesture { .. } => {}
434 //WindowEvent::RotationGesture { .. } => {}
435 // WindowEvent::TouchpadPressure { .. } => {} // only on some macbooks and similar, not relevant for multiplatform games.
436 //WindowEvent::AxisMotion { .. } => {} // intentionally not supported, since we want to use platform-specific api:s for gamepad input
437 // WindowEvent::ThemeChanged(_) => {} // mostly unsupported and not really related to games.
438 // WindowEvent::Occluded(_) => {} not available on most platforms anyway
439 // WindowEvent::ActivationTokenDone { .. } => {} winit handles this normally, so no need to implement it.
440 // WindowEvent::Moved(_) => {} // since this is not supported on all platforms, it should not be exposed in this library
441 // WindowEvent::Destroyed => {} // this is handled internally
442 // since this crate is mostly for games, this file operations are outside the scope.
443 //WindowEvent::DroppedFile(_) => {}
444 //WindowEvent::HoveredFile(_) => {}
445 //WindowEvent::HoveredFileCancelled => {}
446 _ => {}
447 }
448 }
449
450 fn device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, event: DeviceEvent) {
451 if let DeviceEvent::MouseMotion { delta } = event
452 && self.is_focused
453 {
454 self.handler.mouse_motion(delta);
455 }
456 /*
457 match event {
458 // DeviceEvent::MouseWheel { .. } => {},
459 //DeviceEvent::Button { .. } => { }
460 //DeviceEvent::Added => {}
461 //DeviceEvent::Removed => {}
462 //DeviceEvent::Motion { .. } => { }
463 //DeviceEvent::Key(_) => {}
464 _ => {}
465 }
466 */
467 }
468
469 fn suspended(&mut self, _: &ActiveEventLoop) {}
470
471 fn exiting(&mut self, _: &ActiveEventLoop) {}
472}
473
474/// A struct responsible for managing the application window lifecycle.
475///
476/// The `WindowRunner` struct provides functionality to run an application
477/// that utilizes an event loop for window management. It abstracts the details
478/// of creating and running the event loop, making it easier to integrate window
479/// handling into your game application.
480pub struct WindowRunner;
481
482impl WindowRunner {
483 /// Runs the application with the provided handler.
484 ///
485 /// This method initializes an event loop and starts the application by
486 /// executing the provided `AppHandler`. The event loop runs in a polling
487 /// mode, allowing for responsive event handling. It is not guaranteed to ever return.
488 ///
489 /// # Parameters
490 ///
491 /// - `handler`: A mutable reference to an object implementing the `AppHandler`
492 /// trait, which defines the behavior of the application in response to events.
493 ///
494 /// # Returns
495 ///
496 /// This method returns a `Result<(), EventLoopError>`.
497 /// If an error occurs during event loop creation, it returns an `EventLoopError`.
498 ///
499 /// # Note
500 ///
501 /// It is not guaranteed to ever return, as the event loop will run indefinitely
502 /// until the application is terminated.
503 pub fn run_app(handler: &mut dyn AppHandler, title: &str) -> Result<(), EventLoopError> {
504 let event_loop = EventLoop::new()?;
505 event_loop.set_control_flow(ControlFlow::Poll);
506 let min_size = handler.min_size();
507 let start_size = handler.start_size();
508 let mode = handler.window_mode();
509 let mut app = App::new(handler, title, min_size, start_size, mode);
510 let _ = event_loop.run_app(&mut app);
511 Ok(())
512 }
513}