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