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/platform/headless/delegate.rs
1use parking_lot::Mutex;
2 
3use crate::{
4 clipboard::InMemoryClipboard,
5 notification::{NotificationSendError, RequestPermissionsOutcome},
6 platform::{self, Cursor},
7};
8 
9use std::mem::ManuallyDrop;
10use std::sync::mpsc::Sender;
11use std::sync::Arc;
12use std::sync::OnceLock;
13use std::thread;
14 
15use 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.
19static 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.
24pub(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 
30pub struct AppDelegate {
31 clipboard: InMemoryClipboard,
32 cursor_shape: Mutex<Cursor>,
33 event_sender: Sender<AppEvent>,
34}
35 
36impl 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 
52impl 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 
198struct DispatchDelegate {
199 event_sender: Sender<AppEvent>,
200}
201 
202impl 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