Skip to main content

blitz_shell/
application.rs

1use crate::event::{BlitzShellEvent, BlitzShellProxy};
2
3use anyrender::WindowRenderer;
4use std::collections::HashMap;
5use std::sync::mpsc::Receiver;
6use winit::application::ApplicationHandler;
7use winit::event::WindowEvent;
8use winit::event_loop::ActiveEventLoop;
9use winit::event_loop::ControlFlow;
10use winit::window::WindowId;
11
12#[cfg(target_os = "macos")]
13use winit::platform::macos::ApplicationHandlerExtMacOS;
14
15use crate::{View, WindowConfig};
16
17pub struct BlitzApplication<Rend: WindowRenderer> {
18    pub windows: HashMap<WindowId, View<Rend>>,
19    pub pending_windows: Vec<WindowConfig<Rend>>,
20    pub proxy: BlitzShellProxy,
21    pub event_queue: Receiver<BlitzShellEvent>,
22    #[cfg(feature = "debug-control")]
23    debug_controller: Option<blitz_script::DebugController>,
24}
25
26impl<Rend: WindowRenderer> BlitzApplication<Rend> {
27    pub fn new(proxy: BlitzShellProxy, event_queue: Receiver<BlitzShellEvent>) -> Self {
28        BlitzApplication {
29            windows: HashMap::new(),
30            pending_windows: Vec::new(),
31            proxy,
32            event_queue,
33            #[cfg(feature = "debug-control")]
34            debug_controller: None,
35        }
36    }
37
38    pub fn add_window(&mut self, window_config: WindowConfig<Rend>) {
39        self.pending_windows.push(window_config);
40    }
41
42    #[cfg(feature = "debug-control")]
43    pub fn set_debug_controller(&mut self, controller: blitz_script::DebugController) {
44        // The server thread wakes the loop when a request lands, the same way
45        // everything else here does. Before this the loop woke itself every
46        // 10ms to look: 100 wakeups a second at idle, up to 10ms of latency on
47        // each command, and a floor under every measurement taken with the
48        // driver attached, which is most of them.
49        let proxy = self.proxy.clone();
50        controller.set_waker(move || proxy.wake_up());
51        self.debug_controller = Some(controller);
52    }
53
54    #[cfg(feature = "debug-control")]
55    fn service_debug_controller(&mut self, event_loop: &dyn ActiveEventLoop) {
56        let (Some(controller), windows) = (self.debug_controller.as_mut(), &mut self.windows)
57        else {
58            return;
59        };
60        let Some((animation_time, document)) = windows.values_mut().find_map(|view| {
61            let animation_time = view.current_animation_time();
62            view.try_downcast_doc_mut::<blitz_script::ScriptDocument>()
63                .map(|document| (animation_time, document))
64        }) else {
65            // No document to run against yet. The request stays queued, and
66            // whatever creates the window brings the loop round again.
67            return;
68        };
69        controller.service_pending_at(document, animation_time);
70        if controller.exit_requested() {
71            event_loop.exit();
72        }
73    }
74
75    fn window_mut_by_doc_id(&mut self, doc_id: usize) -> Option<&mut View<Rend>> {
76        self.windows.values_mut().find(|w| w.doc.id() == doc_id)
77    }
78
79    pub fn handle_blitz_shell_event(
80        &mut self,
81        event_loop: &dyn ActiveEventLoop,
82        event: BlitzShellEvent,
83    ) {
84        match event {
85            BlitzShellEvent::Poll { window_id } => {
86                // Kept for embedders that send it. The poll itself happens in
87                // `about_to_wait` with every other request, which runs before
88                // the loop sleeps, so this is not deferred past this turn.
89                if let Some(window) = self.windows.get(&window_id) {
90                    window.request_poll();
91                };
92            }
93            BlitzShellEvent::CloseWindow { window_id } => {
94                // Drop window before exiting event loop
95                // See https://github.com/rust-windowing/winit/issues/4135
96                let window = self.windows.remove(&window_id);
97                drop(window);
98                if self.windows.is_empty() {
99                    event_loop.exit();
100                }
101            }
102            BlitzShellEvent::ResumeReady { window_id } => {
103                // The renderer fires `on_ready` after it has sent on the
104                // channel, so `complete_resume` should always succeed here.
105                // If a stale event survives a suspend, dropping it is safe.
106                let _paint_committed = if let Some(window) = self.windows.get_mut(&window_id)
107                    && window.waker.is_none()
108                {
109                    let ok = window.complete_resume();
110                    debug_assert!(ok, "ResumeReady received but renderer not ready");
111                    ok
112                } else {
113                    false
114                };
115                #[cfg(feature = "debug-control")]
116                if _paint_committed && let Some(controller) = self.debug_controller.as_mut() {
117                    controller.note_paint_committed();
118                }
119            }
120            BlitzShellEvent::RequestRedraw { doc_id } => {
121                // TODO: Handle multiple documents per window
122                if let Some(window) = self.window_mut_by_doc_id(doc_id) {
123                    window.request_redraw();
124                }
125            }
126
127            #[cfg(feature = "accessibility")]
128            BlitzShellEvent::Accessibility { window_id, data } => {
129                if let Some(window) = self.windows.get_mut(&window_id) {
130                    match &*data {
131                        accesskit_xplat::WindowEvent::InitialTreeRequested => {
132                            window.build_accessibility_tree();
133                        }
134                        accesskit_xplat::WindowEvent::AccessibilityDeactivated => {
135                            // TODO
136                        }
137                        accesskit_xplat::WindowEvent::ActionRequested(_req) => {
138                            // TODO
139                        }
140                    }
141                }
142            }
143            BlitzShellEvent::Embedder(_) => {
144                // Do nothing. Should be handled by embedders (if required).
145            }
146            BlitzShellEvent::Navigate(_opts) => {
147                // Do nothing. Should be handled by embedders (if required).
148            }
149            BlitzShellEvent::NavigationLoad { .. } => {
150                // Do nothing. Should be handled by embedders (if required).
151            }
152            #[cfg(target_arch = "wasm32")]
153            BlitzShellEvent::ResizeSettleCheck { window_id } => {
154                if let Some(window) = self.windows.get_mut(&window_id) {
155                    window.apply_pending_resize_if_settled();
156                }
157            }
158        }
159    }
160}
161
162impl<Rend: WindowRenderer> ApplicationHandler for BlitzApplication<Rend> {
163    fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
164        #[cfg(feature = "debug-control")]
165        let mut committed_frames = 0usize;
166        // Resume existing windows
167        for view in self.windows.values_mut() {
168            view.resume();
169            #[cfg(not(target_arch = "wasm32"))]
170            {
171                let ok = view.complete_resume();
172                debug_assert!(ok, "native renderer did not resume synchronously");
173                #[cfg(feature = "debug-control")]
174                if ok {
175                    committed_frames += 1;
176                }
177            }
178        }
179
180        // Initialise pending windows. The renderer's resume is non-blocking —
181        // on native it finishes inline, on wasm32 it spawns a future that will
182        // dispatch BlitzShellEvent::ResumeReady when init completes. Either way
183        // we insert the view immediately so the event handler can find it.
184        for window_config in self.pending_windows.drain(..) {
185            let mut view = View::init(window_config, event_loop, &self.proxy);
186            view.resume();
187            #[cfg(not(target_arch = "wasm32"))]
188            {
189                let ok = view.complete_resume();
190                debug_assert!(ok, "native renderer did not resume synchronously");
191                #[cfg(feature = "debug-control")]
192                if ok {
193                    committed_frames += 1;
194                }
195            }
196            self.windows.insert(view.window_id(), view);
197        }
198        #[cfg(feature = "debug-control")]
199        if let Some(controller) = self.debug_controller.as_mut() {
200            for _ in 0..committed_frames {
201                controller.note_paint_committed();
202            }
203        }
204    }
205
206    fn destroy_surfaces(&mut self, _event_loop: &dyn ActiveEventLoop) {
207        for view in self.windows.values_mut() {
208            view.suspend();
209        }
210    }
211
212    fn resumed(&mut self, _event_loop: &dyn ActiveEventLoop) {
213        // TODO
214    }
215
216    fn suspended(&mut self, _event_loop: &dyn ActiveEventLoop) {
217        // TODO
218    }
219
220    fn window_event(
221        &mut self,
222        event_loop: &dyn ActiveEventLoop,
223        window_id: WindowId,
224        event: WindowEvent,
225    ) {
226        // Exit the app when window close is requested.
227        if matches!(event, WindowEvent::CloseRequested) {
228            // Drop window before exiting event loop
229            // See https://github.com/rust-windowing/winit/issues/4135
230            let window = self.windows.remove(&window_id);
231            drop(window);
232            if self.windows.is_empty() {
233                event_loop.exit();
234            }
235            return;
236        }
237
238        let _paint_committed = if let Some(window) = self.windows.get_mut(&window_id) {
239            let committed = window.handle_winit_event(event);
240            // Flag rather than a queued event and a wake: this runs on the
241            // event loop's own thread, `about_to_wait` follows before the loop
242            // sleeps, and a drag delivers hundreds of these a second.
243            window.request_poll();
244            committed
245        } else {
246            false
247        };
248        #[cfg(feature = "debug-control")]
249        if _paint_committed && let Some(controller) = self.debug_controller.as_mut() {
250            controller.note_paint_committed();
251        }
252    }
253
254    fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
255        while let Ok(event) = self.event_queue.try_recv() {
256            self.handle_blitz_shell_event(event_loop, event);
257        }
258        #[cfg(feature = "debug-control")]
259        self.service_debug_controller(event_loop);
260    }
261
262    #[cfg(target_os = "macos")]
263    fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> {
264        Some(self)
265    }
266
267    fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
268        let _ = event_loop;
269        #[cfg(feature = "debug-control")]
270        self.service_debug_controller(event_loop);
271
272        // Every poll asked for since the loop last slept, coalesced to one per
273        // window. Before the animation deadline below, because a poll is what
274        // schedules the next animation frame.
275        for window in self.windows.values_mut() {
276            window.poll_if_requested();
277        }
278
279        #[cfg(target_os = "ios")]
280        for view in self.windows.values_mut() {
281            if view.ios_request_redraw.get() {
282                view.window.request_redraw();
283            }
284        }
285
286        // Animation frames are paced here rather than requested at the end of
287        // the last one, which is what would run them at the display's rate. The
288        // earliest deadline across every window becomes the wait, so a window
289        // that is animating does not stop the others sleeping.
290        //
291        // Restored to `Wait` when nothing is animating, rather than left alone.
292        //
293        // `ControlFlow::Wait` is the default, but it is not what the loop is
294        // still set to once an animation has ended: the last frame's
295        // `WaitUntil` stays in force, its deadline is already in the past, and
296        // the loop then wakes immediately, forever, with nothing to do. It
297        // costs 76% of a core on an idle window, and it is invisible in a
298        // profile of the app because no frame of ours is on the stack: the
299        // whole main thread sits in `__CFRunLoopDoTimers` re-arming a timer.
300        //
301        // web_time, not std: on wasm they are genuinely distinct types, and
302        // both `poll_animation_frame` and winit's own `ControlFlow::WaitUntil`
303        // are in web_time's. On native web_time re-exports std's, which is why
304        // std compiled here and broke only the wasm job.
305        let now = web_time::Instant::now();
306        let next_frame = self
307            .windows
308            .values()
309            .filter_map(|view| view.poll_animation_frame(now))
310            .min();
311        if let Some(deadline) = next_frame {
312            event_loop.set_control_flow(ControlFlow::WaitUntil(deadline));
313        } else {
314            event_loop.set_control_flow(ControlFlow::Wait);
315        }
316    }
317}
318
319#[cfg(target_os = "macos")]
320impl<Rend: WindowRenderer> ApplicationHandlerExtMacOS for BlitzApplication<Rend> {
321    fn standard_key_binding(
322        &mut self,
323        _event_loop: &dyn ActiveEventLoop,
324        window_id: WindowId,
325        action: &str,
326    ) {
327        if let Some(window) = self.windows.get_mut(&window_id) {
328            window.handle_apple_standard_keybinding(action);
329            window.request_poll();
330        }
331    }
332}