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