StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use parking_lot::Mutex; |
| 2 | |
| 3 | use crate::{ |
| 4 | clipboard::InMemoryClipboard, |
| 5 | notification::{NotificationSendError, RequestPermissionsOutcome}, |
| 6 | platform::{self, Cursor}, |
| 7 | }; |
| 8 | |
| 9 | use std::mem::ManuallyDrop; |
| 10 | use std::sync::mpsc::Sender; |
| 11 | use std::sync::Arc; |
| 12 | use std::sync::OnceLock; |
| 13 | use std::thread; |
| 14 | |
| 15 | use super::event_loop::AppEvent; |
| 16 | |
| 17 | /// Stores the ID of the application's main thread, which we can reference |
| 18 | /// to determine if a given thread is the main thread or not. |
| 19 | static MAIN_THREAD_ID: OnceLock<thread::ThreadId> = OnceLock::new(); |
| 20 | |
| 21 | /// Marks the current thread as the application's main thread. |
| 22 | /// |
| 23 | /// Panics if called more than once. |
| 24 | pub(super) fn mark_current_thread_as_main() { |
| 25 | MAIN_THREAD_ID |
| 26 | .set(thread::current().id()) |
| 27 | .expect("should only call mark_current_thread_as_main once!"); |
| 28 | } |
| 29 | |
| 30 | pub struct AppDelegate { |
| 31 | clipboard: InMemoryClipboard, |
| 32 | cursor_shape: Mutex<Cursor>, |
| 33 | event_sender: Sender<AppEvent>, |
| 34 | } |
| 35 | |
| 36 | impl AppDelegate { |
| 37 | pub(super) fn new(event_sender: Sender<AppEvent>) -> Self { |
| 38 | Self { |
| 39 | clipboard: InMemoryClipboard::default(), |
| 40 | cursor_shape: Mutex::new(Cursor::Arrow), |
| 41 | event_sender, |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | fn send_event(&self, event: AppEvent) { |
| 46 | if self.event_sender.send(event).is_err() { |
| 47 | log::warn!("Tried to send event, but event loop is no longer running"); |
| 48 | } |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | impl platform::Delegate for AppDelegate { |
| 53 | fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> { |
| 54 | Arc::new(DispatchDelegate { |
| 55 | event_sender: self.event_sender.clone(), |
| 56 | }) |
| 57 | } |
| 58 | |
| 59 | fn request_user_attention(&self, _window_id: crate::WindowId) { |
| 60 | // Unsupported. |
| 61 | } |
| 62 | |
| 63 | fn clipboard(&mut self) -> &mut dyn crate::Clipboard { |
| 64 | &mut self.clipboard |
| 65 | } |
| 66 | |
| 67 | fn system_theme(&self) -> platform::SystemTheme { |
| 68 | platform::SystemTheme::Light |
| 69 | } |
| 70 | |
| 71 | fn open_url(&self, url: &str) { |
| 72 | #[cfg(target_os = "macos")] |
| 73 | { |
| 74 | // Use macOS platform implementation |
| 75 | crate::platform::mac::Window::open_url(url); |
| 76 | } |
| 77 | #[cfg(not(target_os = "macos"))] |
| 78 | { |
| 79 | // Reuse the winit implementation for non-mac platforms |
| 80 | crate::windowing::winit::delegate::open_url_in_system(url); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | fn open_file_path(&self, _path: &std::path::Path) { |
| 85 | // Unsupported. |
| 86 | } |
| 87 | |
| 88 | fn open_file_path_in_explorer(&self, _path: &std::path::Path) { |
| 89 | // Unsupported. |
| 90 | } |
| 91 | |
| 92 | fn open_file_picker( |
| 93 | &self, |
| 94 | callback: platform::FilePickerCallback, |
| 95 | _file_picker_config: platform::FilePickerConfiguration, |
| 96 | ) { |
| 97 | self.send_event(AppEvent::RunCallback(Box::new(move |ctx| { |
| 98 | callback(Ok(vec![]), ctx); |
| 99 | }))); |
| 100 | } |
| 101 | |
| 102 | fn open_save_file_picker( |
| 103 | &self, |
| 104 | callback: platform::SaveFilePickerCallback, |
| 105 | _config: platform::SaveFilePickerConfiguration, |
| 106 | ) { |
| 107 | self.send_event(AppEvent::RunCallback(Box::new(move |ctx| { |
| 108 | callback(None, ctx); |
| 109 | }))); |
| 110 | } |
| 111 | |
| 112 | fn application_bundle_info( |
| 113 | &self, |
| 114 | _bundle_identifier: &str, |
| 115 | ) -> Option<crate::ApplicationBundleInfo<'_>> { |
| 116 | // This is unsupported, though we could delegate to the macOS implementation. |
| 117 | None |
| 118 | } |
| 119 | |
| 120 | fn show_native_platform_modal( |
| 121 | &self, |
| 122 | _id: crate::modals::ModalId, |
| 123 | _modal: crate::modals::AlertDialog, |
| 124 | ) { |
| 125 | // Unsupported. |
| 126 | } |
| 127 | |
| 128 | fn request_desktop_notification_permissions( |
| 129 | &self, |
| 130 | on_completion: platform::RequestNotificationPermissionsCallback, |
| 131 | ) { |
| 132 | self.send_event(AppEvent::RunCallback(Box::new(move |ctx| { |
| 133 | on_completion(RequestPermissionsOutcome::PermissionsDenied, ctx); |
| 134 | }))); |
| 135 | } |
| 136 | |
| 137 | fn send_desktop_notification( |
| 138 | &self, |
| 139 | _notification_content: crate::notification::UserNotification, |
| 140 | _window_id: crate::WindowId, |
| 141 | on_error: platform::SendNotificationErrorCallback, |
| 142 | ) { |
| 143 | self.send_event(AppEvent::RunCallback(Box::new(move |ctx| { |
| 144 | on_error(NotificationSendError::PermissionsDenied, ctx); |
| 145 | }))); |
| 146 | } |
| 147 | |
| 148 | fn set_cursor_shape(&self, cursor: Cursor) { |
| 149 | *self.cursor_shape.lock() = cursor; |
| 150 | } |
| 151 | |
| 152 | #[cfg(feature = "test-util")] |
| 153 | fn get_cursor_shape(&self) -> Cursor { |
| 154 | *self.cursor_shape.lock() |
| 155 | } |
| 156 | |
| 157 | fn close_ime_async(&self, _window_id: crate::WindowId) { |
| 158 | // Unsupported. |
| 159 | } |
| 160 | |
| 161 | fn is_ime_open(&self) -> bool { |
| 162 | false |
| 163 | } |
| 164 | |
| 165 | fn open_character_palette(&self) { |
| 166 | // Unsupported. |
| 167 | } |
| 168 | |
| 169 | fn set_accessibility_contents(&self, _content: crate::accessibility::AccessibilityContent) { |
| 170 | // Unsupported. |
| 171 | } |
| 172 | |
| 173 | fn register_global_shortcut(&self, _shortcut: crate::keymap::Keystroke) { |
| 174 | // Unsupported. |
| 175 | } |
| 176 | |
| 177 | fn unregister_global_shortcut(&self, _shortcut: &crate::keymap::Keystroke) { |
| 178 | // Unsupported. |
| 179 | } |
| 180 | |
| 181 | fn terminate_app(&self, termination_mode: platform::TerminationMode) { |
| 182 | self.send_event(AppEvent::Terminate(termination_mode)); |
| 183 | } |
| 184 | |
| 185 | fn is_screen_reader_enabled(&self) -> Option<bool> { |
| 186 | None |
| 187 | } |
| 188 | |
| 189 | fn microphone_access_state(&self) -> platform::MicrophoneAccessState { |
| 190 | platform::MicrophoneAccessState::Denied |
| 191 | } |
| 192 | |
| 193 | fn is_headless(&self) -> bool { |
| 194 | true |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | struct DispatchDelegate { |
| 199 | event_sender: Sender<AppEvent>, |
| 200 | } |
| 201 | |
| 202 | impl platform::DispatchDelegate for DispatchDelegate { |
| 203 | fn is_main_thread(&self) -> bool { |
| 204 | thread::current().id() |
| 205 | == *MAIN_THREAD_ID |
| 206 | .get() |
| 207 | .expect("should have marked a thread as the main thread") |
| 208 | } |
| 209 | |
| 210 | fn run_on_main_thread(&self, task: async_task::Runnable) { |
| 211 | // See crate::windowing::winit::delegate::DispatchDelegate for why we use ManuallyDrop. |
| 212 | if self |
| 213 | .event_sender |
| 214 | .send(AppEvent::RunTask(ManuallyDrop::new(task))) |
| 215 | .is_err() |
| 216 | { |
| 217 | log::warn!("Tried to send event, but event loop is no longer running"); |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 |