Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/app.rs
1use futures_util::future::LocalBoxFuture;
2use std::mem::ManuallyDrop;
3 
4use crate::{
5 clipboard::ClipboardContent,
6 integration::TestDriver,
7 keymap,
8 platform::{self, TerminationMode},
9 AppContext, AssetProvider, WindowId,
10};
11use derivative::Derivative;
12 
13use super::window::{IntegrationTestWindowManager, WindowManager};
14use crate::notification::RequestPermissionsOutcome;
15 
16use crate::platform::NotificationInfo;
17 
18#[cfg(target_os = "linux")]
19use std::sync::OnceLock;
20 
21#[cfg(target_os = "linux")]
22pub static WINDOWING_SYSTEM: OnceLock<WindowingSystem> = OnceLock::new();
23 
24pub type RequestPermissionsCallback =
25 Box<dyn FnOnce(RequestPermissionsOutcome, &mut AppContext) + Send + Sync>;
26 
27#[derive(Derivative)]
28#[derivative(Debug)]
29pub enum CustomEvent {
30 /// Open a window with the given window ID and options.
31 OpenWindow {
32 window_id: crate::WindowId,
33 window_options: platform::WindowOptions,
34 },
35 /// Run the wrapped task on the main thread.
36 RunTask(ManuallyDrop<async_task::Runnable>),
37 /// Exit the event loop, terminating the application.
38 Terminate(TerminationMode),
39 /// Close the specified window.
40 CloseWindow {
41 window_id: crate::WindowId,
42 termination_mode: TerminationMode,
43 },
44 /// A global hotkey was pressed. Global hotkeys are not yet supported on wasm.
45 #[cfg_attr(target_family = "wasm", allow(dead_code))]
46 GlobalShortcutTriggered(keymap::Keystroke),
47 /// The active window changed.
48 ///
49 /// We use this to trigger [`platform::AppCallbacks::on_active_window_changed`] instead of
50 /// winit's [`winit::event::WindowEvent::Focused`]. This is because winit's `Focused` event
51 /// actually fires twice when focus is transferred between 2 of Warp's own windows. But, we
52 /// only want to fire `on_active_window_changed` once for that focus change. So, we coalesce
53 /// multiple `Focused` events into a single `ActiveWindowChanged` event on the next tick of the
54 /// [`winit::event_loop::EventLoop`].
55 ActiveWindowChanged,
56 /// Update the UI App using the given closure.
57 UpdateUIApp(#[derivative(Debug = "ignore")] Box<dyn FnOnce(&mut AppContext) + Send + Sync>),
58 RequestUserAttention {
59 window_id: WindowId,
60 },
61 StopRequestingUserAttention {
62 window_id: WindowId,
63 },
64 #[allow(dead_code)]
65 Clipboard(ClipboardEvent),
66 SetCursorShape(platform::Cursor),
67 ActiveCursorPositionUpdated,
68 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
69 AboutToSleep,
70 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
71 ResumedFromSleep,
72 /// The application is connected to the internet.
73 #[cfg_attr(any(target_os = "macos"), allow(dead_code))]
74 InternetConnected,
75 /// The application is disconnected from the internet.
76 #[cfg_attr(any(target_os = "macos"), allow(dead_code))]
77 InternetDisconnected,
78 /// The system theme (light/dark) changed.
79 /// TODO(CORE-2274): theming on Windows
80 #[cfg_attr(any(target_os = "macos", target_os = "windows"), allow(dead_code))]
81 SystemThemeChanged,
82 /// Send a platform-native notification.
83 SendNotification {
84 window_id: WindowId,
85 notification_info: NotificationInfo,
86 },
87 /// Focus the native window that triggered a notification.
88 #[cfg_attr(target_family = "wasm", allow(dead_code))]
89 FocusWindow {
90 window_id: WindowId,
91 },
92 RequestNotificationPermissions(#[derivative(Debug = "ignore")] RequestPermissionsCallback),
93 /// Fire a debounced drag-and-drop files event.
94 DragAndDropFilesDebounced {
95 window_id: winit::window::WindowId,
96 },
97 /// Input received from the soft keyboard on mobile WASM.
98 #[cfg(target_family = "wasm")]
99 SoftKeyboardInput(crate::platform::wasm::SoftKeyboardInput),
100 /// The visual viewport was resized (typically due to soft keyboard appearing/disappearing).
101 #[cfg(target_family = "wasm")]
102 VisualViewportResized {
103 width: f32,
104 height: f32,
105 },
106 /// Momentum scrolling animation frame.
107 MomentumScroll {
108 window_id: winit::window::WindowId,
109 },
110}
111 
112#[derive(Debug)]
113#[allow(dead_code)]
114pub enum ClipboardEvent {
115 Paste(ClipboardContent),
116}
117 
118#[cfg(target_os = "linux")]
119#[derive(Debug, PartialEq)]
120pub enum WindowingSystem {
121 X11,
122 Wayland,
123}
124 
125pub struct App {
126 callbacks: platform::app::AppCallbacks,
127 assets: Box<dyn AssetProvider>,
128 is_integration_test: bool,
129 window_class: Option<String>,
130 #[cfg(target_os = "linux")]
131 force_x11: bool,
132}
133 
134impl App {
135 pub(crate) fn new(
136 callbacks: platform::app::AppCallbacks,
137 assets: Box<dyn AssetProvider>,
138 test_driver: Option<&TestDriver>,
139 ) -> Self {
140 Self {
141 callbacks,
142 assets,
143 is_integration_test: test_driver.is_some(),
144 window_class: None,
145 #[cfg(target_os = "linux")]
146 force_x11: false,
147 }
148 }
149 
150 // Dead code is allowed on wasm and Windows as the window class is only set for Linux
151 // platforms.
152 #[cfg_attr(any(target_family = "wasm", target_os = "windows"), allow(dead_code))]
153 pub(crate) fn set_window_class(&mut self, window_class: String) {
154 self.window_class = Some(window_class);
155 }
156 
157 #[cfg(target_os = "linux")]
158 pub(crate) fn force_x11(&mut self, force_x11: bool) {
159 self.force_x11 = force_x11;
160 }
161 
162 pub(crate) fn run(
163 self,
164 init_fn: impl FnOnce(&mut AppContext, LocalBoxFuture<'static, crate::App>) + 'static,
165 ) {
166 let App {
167 callbacks,
168 assets,
169 is_integration_test,
170 window_class,
171 #[cfg(target_os = "linux")]
172 force_x11,
173 } = self;
174 
175 let mut event_loop_builder = winit::event_loop::EventLoop::with_user_event();
176 
177 #[cfg(target_os = "linux")]
178 if force_x11 {
179 winit::platform::x11::EventLoopBuilderExtX11::with_x11(&mut event_loop_builder);
180 }
181 
182 let event_loop = event_loop_builder
183 .build()
184 .expect("should be able to create event loop");
185 
186 // Initialize the wgpu instance with the event loop's display handle.
187 crate::rendering::wgpu::init_wgpu_instance(Box::new(event_loop.owned_display_handle()));
188 
189 // Perform some platform-specific initialization.
190 cfg_if::cfg_if! {
191 if #[cfg(target_os = "linux")] {
192 super::linux::maybe_register_xlib_error_hook(&event_loop);
193 super::linux::ensure_cursor_theme();
194 } else if #[cfg(target_family = "wasm")] {
195 crate::platform::wasm::add_paste_listener(event_loop.create_proxy());
196 if callbacks.on_internet_reachability_changed.is_some() {
197 crate::platform::wasm::add_network_connection_listener(event_loop.create_proxy());
198 }
199 crate::platform::wasm::add_system_theme_listener(event_loop.create_proxy());
200 crate::platform::wasm::setup_visual_viewport_resize_listener(event_loop.create_proxy());
201 }
202 }
203 
204 // Set the current thread as the main thread (the one that hosts the
205 // application event loop).
206 super::delegate::mark_current_thread_as_main();
207 
208 let ui_app = Self::construct_ui_app(assets, is_integration_test, &event_loop);
209 let inner_event_loop = super::EventLoop::new(
210 ui_app,
211 callbacks,
212 init_fn,
213 window_class,
214 event_loop.create_proxy(),
215 );
216 
217 // Prevent dropping of our internal event loop state structure during
218 // panic unwinds.
219 //
220 // We've seen crashes where a panic unwind leads to the dropping of the
221 // event loop, which ultimately causes a segfault in graphics driver
222 // code. Given the fact that we terminate the app via `exit(0)` and
223 // not by returning from the event loop, we don't ever need to drop the
224 // event loop, even during a panic unwind.
225 let mut inner_event_loop = std::mem::ManuallyDrop::new(inner_event_loop);
226 
227 // Temporarily allow use of the deprecated run() method until winit
228 // 0.30 is here for good, at which point we'll migrate to the new
229 // trait-based APIs.
230 #[allow(deprecated)]
231 event_loop
232 .run(move |evt, window_target| {
233 inner_event_loop.handle_event(evt, window_target);
234 })
235 .expect("Unable to run winit event loop");
236 }
237 
238 fn construct_ui_app(
239 assets: Box<dyn AssetProvider>,
240 is_integration_test: bool,
241 event_loop: &winit::event_loop::EventLoop<CustomEvent>,
242 ) -> crate::App {
243 let platform_delegate: Box<dyn platform::Delegate> = if is_integration_test {
244 let delegate = super::delegate::IntegrationTestDelegate::new(event_loop.create_proxy())
245 .expect("should not fail to create platform delegate");
246 Box::new(delegate)
247 } else {
248 let mut delegate = super::delegate::AppDelegate::new(event_loop.create_proxy())
249 .expect("should not fail to create platform delegate");
250 delegate.use_platform_clipboard();
251 Box::new(delegate)
252 };
253 
254 let display_handle = event_loop.owned_display_handle();
255 let window_manager: Box<dyn platform::WindowManager> = if is_integration_test {
256 Box::new(IntegrationTestWindowManager::new(
257 event_loop.create_proxy(),
258 display_handle,
259 ))
260 } else {
261 Box::new(WindowManager::new(
262 event_loop.create_proxy(),
263 display_handle,
264 ))
265 };
266 
267 crate::App::new(
268 platform_delegate,
269 window_manager,
270 Box::new(super::fonts::FontDB::new()),
271 assets,
272 )
273 .expect("should not fail to construct application")
274 }
275}
276