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