Skip to main content

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