blitz_shell/
application.rs1use 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 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 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 if let Some(window) = self.windows.get(&window_id) {
90 window.request_poll();
91 };
92 }
93 BlitzShellEvent::CloseWindow { window_id } => {
94 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 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 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 }
137 accesskit_xplat::WindowEvent::ActionRequested(_req) => {
138 }
140 }
141 }
142 }
143 BlitzShellEvent::Embedder(_) => {
144 }
146 BlitzShellEvent::Navigate(_opts) => {
147 }
149 BlitzShellEvent::NavigationLoad { .. } => {
150 }
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 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 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 }
215
216 fn suspended(&mut self, _event_loop: &dyn ActiveEventLoop) {
217 }
219
220 fn window_event(
221 &mut self,
222 event_loop: &dyn ActiveEventLoop,
223 window_id: WindowId,
224 event: WindowEvent,
225 ) {
226 if matches!(event, WindowEvent::CloseRequested) {
228 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 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 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 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}