Skip to main content

astrelis_winit/
app.rs

1use std::collections::HashMap;
2pub use winit::error::OsError;
3use winit::event_loop::ActiveEventLoop;
4use winit::window::WindowId;
5
6use astrelis_core::profiling::{profile_function, profile_scope};
7
8use crate::{
9    event::{Event, EventBatch, EventQueue, HandleStatus},
10    time::{FrameTime, TimeTracker},
11    window::{Window, WindowDescriptor},
12};
13
14// Re-export FrameTime for convenience
15pub use crate::time::FrameTime as Time;
16
17struct WindowResources {
18    events: EventQueue,
19    scale_factor: f64,
20}
21
22pub struct AppCtx<'a> {
23    event_loop: &'a ActiveEventLoop,
24    windows: &'a mut HashMap<WindowId, WindowResources>,
25}
26
27impl AppCtx<'_> {
28    pub fn create_window(&mut self, descriptor: WindowDescriptor) -> Result<Window, OsError> {
29        let window = Window::new(self.event_loop, descriptor)?;
30
31        self.windows.insert(
32            window.id(),
33            WindowResources {
34                events: EventQueue::new(),
35                scale_factor: window.scale_factor_f64(),
36            },
37        );
38
39        Ok(window)
40    }
41
42    pub fn exit(&self) {
43        self.event_loop.exit();
44    }
45}
46
47pub trait App {
48    /// Called once when the app starts, before the first update.
49    ///
50    /// Use this for initialization that requires the event loop to be running.
51    #[allow(unused_variables)]
52    fn on_start(&mut self, ctx: &mut AppCtx) {}
53
54    /// Called at the beginning of each frame, before update().
55    ///
56    /// Use this for pre-frame setup like profiling markers.
57    #[allow(unused_variables)]
58    fn begin_frame(&mut self, ctx: &mut AppCtx, time: &FrameTime) {}
59
60    /// Called once per frame for global logic (game state, physics, etc.)
61    ///
62    /// This is the main game logic update. Frame-independent movement should
63    /// use `time.delta_seconds()` for consistent behavior across frame rates.
64    #[allow(unused_variables)]
65    fn update(&mut self, ctx: &mut AppCtx, time: &FrameTime) {}
66
67    /// Called at a fixed rate for physics simulation.
68    ///
69    /// Unlike `update()`, this is called zero or more times per frame to maintain
70    /// a consistent physics timestep. The `fixed_dt` parameter is the fixed timestep
71    /// duration in seconds.
72    ///
73    /// Note: The fixed timestep is controlled by the app, not the framework.
74    /// Apps that need fixed timestep should track their own accumulator.
75    #[allow(unused_variables)]
76    fn fixed_update(&mut self, ctx: &mut AppCtx, fixed_dt: f32) {}
77
78    /// Called at the end of each frame, after all rendering.
79    ///
80    /// Use this for post-frame cleanup or telemetry.
81    #[allow(unused_variables)]
82    fn end_frame(&mut self, ctx: &mut AppCtx, time: &FrameTime) {}
83
84    /// Called once per window that needs rendering, with window-specific input.
85    fn render(&mut self, ctx: &mut AppCtx, window_id: WindowId, events: &mut EventBatch);
86
87    /// Called when the app is about to exit.
88    ///
89    /// Use this for cleanup tasks like saving state, flushing logs, etc.
90    #[allow(unused_variables)]
91    fn on_exit(&mut self, ctx: &mut AppCtx) {}
92}
93
94pub type AppFactory = fn(ctx: &mut AppCtx) -> Box<dyn App>;
95
96struct AppProxy {
97    factory: AppFactory,
98    app: Option<Box<dyn App>>,
99    update_called_this_frame: bool,
100    windows: HashMap<WindowId, WindowResources>,
101    time_tracker: TimeTracker,
102    started: bool,
103    current_frame_time: Option<FrameTime>,
104}
105
106impl winit::application::ApplicationHandler for AppProxy {
107    fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
108        profile_function!();
109        if self.app.is_none() {
110            let mut ctx = AppCtx {
111                event_loop: _event_loop,
112                windows: &mut self.windows,
113            };
114            let mut app = (self.factory)(&mut ctx);
115
116            // Call on_start lifecycle hook
117            app.on_start(&mut ctx);
118            self.started = true;
119
120            self.app = Some(app);
121        }
122    }
123
124    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
125        profile_function!();
126        // Call end_frame after all events have been processed
127        if let (Some(app), Some(frame_time)) = (&mut self.app, &self.current_frame_time) {
128            let mut ctx = AppCtx {
129                event_loop,
130                windows: &mut self.windows,
131            };
132            app.end_frame(&mut ctx, frame_time);
133        }
134
135        // Mark that we need to call update() on next redraw
136        self.update_called_this_frame = false;
137        self.current_frame_time = None;
138    }
139
140    fn window_event(
141        &mut self,
142        event_loop: &ActiveEventLoop,
143        window_id: winit::window::WindowId,
144        event: winit::event::WindowEvent,
145    ) {
146        profile_function!();
147        use winit::event::WindowEvent;
148
149        let _app = match &mut self.app {
150            Some(app) => app,
151            None => return,
152        };
153
154        let mut ctx = AppCtx {
155            event_loop,
156            windows: &mut self.windows,
157        };
158
159        match event {
160            WindowEvent::RedrawRequested => {
161                let app = self.app.as_mut().unwrap();
162
163                // Call update() once per frame on first redraw
164                if !self.update_called_this_frame {
165                    // Update time tracker
166                    let frame_time = self.time_tracker.tick();
167
168                    // Call lifecycle hooks in order
169                    {
170                        profile_scope!("update");
171                        app.begin_frame(&mut ctx, &frame_time);
172                        app.update(&mut ctx, &frame_time);
173                    }
174                    // Note: fixed_update is called by the app itself if needed
175
176                    // Save frame time for end_frame call in about_to_wait
177                    self.current_frame_time = Some(frame_time);
178                    self.update_called_this_frame = true;
179                }
180
181                // Call render() for this window with its events
182                let window = ctx.windows.get_mut(&window_id).unwrap();
183                let mut events = window.events.drain();
184
185                {
186                    profile_scope!("render");
187                    app.render(&mut ctx, window_id, &mut events);
188                }
189
190                // Default event handling
191                events.dispatch(|event| match event {
192                    Event::CloseRequested => {
193                        tracing::info!("Close requested for window {:?}", window_id);
194
195                        // Call on_exit before exiting
196                        if let Some(app) = self.app.as_mut() {
197                            app.on_exit(&mut ctx);
198                        }
199
200                        ctx.event_loop.exit();
201                        HandleStatus::consumed()
202                    }
203                    _ => HandleStatus::ignored(),
204                });
205            }
206            event => {
207                let window = self.windows.get_mut(&window_id).unwrap();
208                if let WindowEvent::ScaleFactorChanged { scale_factor, .. } = event {
209                    window.scale_factor = scale_factor;
210                }
211                let astrelis_event = match Event::from_winit(event, window.scale_factor) {
212                    Some(event) => event,
213                    None => return,
214                };
215
216                window.events.push(astrelis_event);
217            }
218        }
219    }
220}
221
222/// Run the application with the given factory function.
223pub fn run_app(factory: AppFactory) {
224    use winit::event_loop::{ControlFlow, EventLoop};
225    let event_loop = EventLoop::new().expect("failed to create event loop");
226    event_loop.set_control_flow(ControlFlow::Wait);
227    let mut app_proxy = AppProxy {
228        factory,
229        app: None,
230        update_called_this_frame: false,
231        windows: HashMap::new(),
232        time_tracker: TimeTracker::new(),
233        started: false,
234        current_frame_time: None,
235    };
236    event_loop
237        .run_app(&mut app_proxy)
238        .expect("failed to run app");
239}