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::window::WindowId;
10
11#[cfg(target_os = "macos")]
12use winit::platform::macos::ApplicationHandlerExtMacOS;
13
14use crate::{View, WindowConfig};
15
16pub struct BlitzApplication<Rend: WindowRenderer> {
17    pub windows: HashMap<WindowId, View<Rend>>,
18    pub pending_windows: Vec<WindowConfig<Rend>>,
19    pub proxy: BlitzShellProxy,
20    pub event_queue: Receiver<BlitzShellEvent>,
21}
22
23impl<Rend: WindowRenderer> BlitzApplication<Rend> {
24    pub fn new(proxy: BlitzShellProxy, event_queue: Receiver<BlitzShellEvent>) -> Self {
25        BlitzApplication {
26            windows: HashMap::new(),
27            pending_windows: Vec::new(),
28            proxy,
29            event_queue,
30        }
31    }
32
33    pub fn add_window(&mut self, window_config: WindowConfig<Rend>) {
34        self.pending_windows.push(window_config);
35    }
36
37    fn window_mut_by_doc_id(&mut self, doc_id: usize) -> Option<&mut View<Rend>> {
38        self.windows.values_mut().find(|w| w.doc.id() == doc_id)
39    }
40
41    pub fn handle_blitz_shell_event(
42        &mut self,
43        event_loop: &dyn ActiveEventLoop,
44        event: BlitzShellEvent,
45    ) {
46        match event {
47            BlitzShellEvent::Poll { window_id } => {
48                if let Some(window) = self.windows.get_mut(&window_id) {
49                    window.poll();
50                };
51            }
52            BlitzShellEvent::CloseWindow { window_id } => {
53                // Drop window before exiting event loop
54                // See https://github.com/rust-windowing/winit/issues/4135
55                let window = self.windows.remove(&window_id);
56                drop(window);
57                if self.windows.is_empty() {
58                    event_loop.exit();
59                }
60            }
61            BlitzShellEvent::ResumeReady { window_id } => {
62                // The renderer fires `on_ready` after it has sent on the
63                // channel, so `complete_resume` should always succeed here.
64                // If a stale event survives a suspend, dropping it is safe.
65                if let Some(window) = self.windows.get_mut(&window_id) {
66                    let ok = window.complete_resume();
67                    debug_assert!(ok, "ResumeReady received but renderer not ready");
68                }
69            }
70            BlitzShellEvent::RequestRedraw { doc_id } => {
71                // TODO: Handle multiple documents per window
72                if let Some(window) = self.window_mut_by_doc_id(doc_id) {
73                    window.request_redraw();
74                }
75            }
76
77            #[cfg(feature = "accessibility")]
78            BlitzShellEvent::Accessibility { window_id, data } => {
79                if let Some(window) = self.windows.get_mut(&window_id) {
80                    match &*data {
81                        accesskit_xplat::WindowEvent::InitialTreeRequested => {
82                            window.build_accessibility_tree();
83                        }
84                        accesskit_xplat::WindowEvent::AccessibilityDeactivated => {
85                            // TODO
86                        }
87                        accesskit_xplat::WindowEvent::ActionRequested(_req) => {
88                            // TODO
89                        }
90                    }
91                }
92            }
93            BlitzShellEvent::Embedder(_) => {
94                // Do nothing. Should be handled by embedders (if required).
95            }
96            BlitzShellEvent::Navigate(_opts) => {
97                // Do nothing. Should be handled by embedders (if required).
98            }
99            BlitzShellEvent::NavigationLoad { .. } => {
100                // Do nothing. Should be handled by embedders (if required).
101            }
102            #[cfg(target_arch = "wasm32")]
103            BlitzShellEvent::ResizeSettleCheck { window_id } => {
104                if let Some(window) = self.windows.get_mut(&window_id) {
105                    window.apply_pending_resize_if_settled();
106                }
107            }
108        }
109    }
110}
111
112impl<Rend: WindowRenderer> ApplicationHandler for BlitzApplication<Rend> {
113    fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
114        // Resume existing windows
115        for (_, view) in self.windows.iter_mut() {
116            view.resume();
117        }
118
119        // Initialise pending windows. The renderer's resume is non-blocking —
120        // on native it finishes inline, on wasm32 it spawns a future that will
121        // dispatch BlitzShellEvent::ResumeReady when init completes. Either way
122        // we insert the view immediately so the event handler can find it.
123        for window_config in self.pending_windows.drain(..) {
124            let mut view = View::init(window_config, event_loop, &self.proxy);
125            view.resume();
126            self.windows.insert(view.window_id(), view);
127        }
128    }
129
130    fn destroy_surfaces(&mut self, _event_loop: &dyn ActiveEventLoop) {
131        for (_, view) in self.windows.iter_mut() {
132            view.suspend();
133        }
134    }
135
136    fn resumed(&mut self, _event_loop: &dyn ActiveEventLoop) {
137        // TODO
138    }
139
140    fn suspended(&mut self, _event_loop: &dyn ActiveEventLoop) {
141        // TODO
142    }
143
144    fn window_event(
145        &mut self,
146        event_loop: &dyn ActiveEventLoop,
147        window_id: WindowId,
148        event: WindowEvent,
149    ) {
150        // Exit the app when window close is requested.
151        if matches!(event, WindowEvent::CloseRequested) {
152            // Drop window before exiting event loop
153            // See https://github.com/rust-windowing/winit/issues/4135
154            let window = self.windows.remove(&window_id);
155            drop(window);
156            if self.windows.is_empty() {
157                event_loop.exit();
158            }
159            return;
160        }
161
162        if let Some(window) = self.windows.get_mut(&window_id) {
163            window.handle_winit_event(event);
164        }
165        self.proxy.send_event(BlitzShellEvent::Poll { window_id });
166    }
167
168    fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
169        while let Ok(event) = self.event_queue.try_recv() {
170            self.handle_blitz_shell_event(event_loop, event);
171        }
172    }
173
174    #[cfg(target_os = "macos")]
175    fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> {
176        Some(self)
177    }
178
179    #[cfg(target_os = "ios")]
180    fn about_to_wait(&mut self, _event_loop: &dyn ActiveEventLoop) {
181        for view in self.windows.values_mut() {
182            if view.ios_request_redraw.get() {
183                view.window.request_redraw();
184            }
185        }
186    }
187}
188
189#[cfg(target_os = "macos")]
190impl<Rend: WindowRenderer> ApplicationHandlerExtMacOS for BlitzApplication<Rend> {
191    fn standard_key_binding(
192        &mut self,
193        _event_loop: &dyn ActiveEventLoop,
194        window_id: WindowId,
195        action: &str,
196    ) {
197        if let Some(window) = self.windows.get_mut(&window_id) {
198            window.handle_apple_standard_keybinding(action);
199            self.proxy.send_event(BlitzShellEvent::Poll { window_id });
200        }
201    }
202}