win_loop/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2#![warn(missing_docs)]
3
4use handler::AppHandler;
5use web_time::Duration;
6use winit::{
7    event::Event,
8    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
9};
10
11mod handler;
12mod input;
13pub use input::{Input, InputState};
14
15pub use anyhow;
16pub use winit;
17
18/// Update context.
19#[derive(Debug)]
20pub struct Context {
21    target_frame_time: Duration,
22    max_frame_time: Duration,
23    exit: bool,
24    delta_time: Duration,
25    /// Input handler.
26    pub input: Input,
27}
28
29impl Context {
30    /// Create a new context.
31    #[inline]
32    pub(crate) fn new(fps: u32, max_frame_time: Duration) -> Self {
33        Self {
34            target_frame_time: Duration::from_secs_f64(1. / fps as f64),
35            max_frame_time,
36            delta_time: Duration::ZERO,
37            exit: false,
38            input: Input::new(),
39        }
40    }
41
42    /// Time between previous and current update.
43    #[inline]
44    pub fn frame_time(&self) -> Duration {
45        self.delta_time
46    }
47
48    /// Set the desired (minimum) time between application updates.
49    /// Implemented based on <https://gafferongames.com/post/fix_your_timestep>.
50    #[inline]
51    pub fn set_target_frame_time(&mut self, time: Duration) {
52        self.target_frame_time = time;
53    }
54
55    /// Set the desired FPS. Overrides `target_frame_time` since they are inversions of each other.
56    #[inline]
57    pub fn set_target_fps(&mut self, fps: u32) {
58        self.target_frame_time = Duration::from_secs_f64(1. / fps as f64);
59    }
60
61    /// Set the maximum time between application updates.
62    /// The real frame time can be longer, but [`Context::frame_time()`] will not exceed this value.
63    /// Implemented based on <https://gafferongames.com/post/fix_your_timestep>.
64    #[inline]
65    pub fn set_max_frame_time(&mut self, time: Duration) {
66        self.max_frame_time = time;
67    }
68
69    /// Exit the application.
70    #[inline]
71    pub fn exit(&mut self) {
72        self.exit = true;
73    }
74}
75
76/// Application trait.
77pub trait App {
78    /// Application update.
79    /// Rate of updates can be set using [`Context`].
80    fn update(&mut self, ctx: &mut Context) -> anyhow::Result<()>;
81
82    /// Application render.
83    /// Will be called once every frame.
84    fn render(&mut self, blending_factor: f64) -> anyhow::Result<()>;
85
86    /// Custom event handler if needed.
87    #[inline]
88    fn handle(&mut self, _event: Event<()>) -> anyhow::Result<()> {
89        Ok(())
90    }
91}
92
93/// App initialization function.
94pub type AppInitFunc<A> = dyn FnOnce(&ActiveEventLoop) -> anyhow::Result<A>;
95
96/// Start the application.
97/// `app_init` will be called once during the next [`Event::Resumed`](https://docs.rs/winit/latest/winit/event/enum.Event.html#variant.Resumed).
98///
99/// Depending on the platform, this function may not return (see <https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run_app>).
100/// On web uses <https://docs.rs/winit/latest/wasm32-unknown-unknown/winit/platform/web/trait.EventLoopExtWebSys.html#tymethod.spawn_app> instead of `run_app()`.
101pub fn start<A>(
102    fps: u32,
103    max_frame_time: Duration,
104    app_init: Box<AppInitFunc<A>>,
105) -> anyhow::Result<()>
106where
107    A: App + 'static,
108{
109    let event_loop = EventLoop::new()?;
110
111    event_loop.set_control_flow(ControlFlow::Poll);
112
113    #[cfg_attr(any(target_arch = "wasm32", target_arch = "wasm64"), allow(unused_mut))]
114    let mut handler = AppHandler::new(app_init, fps, max_frame_time);
115
116    #[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
117    event_loop.run_app(&mut handler)?;
118
119    #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))]
120    winit::platform::web::EventLoopExtWebSys::spawn_app(event_loop, handler);
121
122    Ok(())
123}