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/mac/delegate.rs
1use super::app::create_native_platform_modal;
2use super::keycode::{modifier_code, Keycode};
3use super::utils::nsstring_as_str;
4use super::{app, make_nsstring, Clipboard, Window};
5use anyhow::Result;
6use cocoa::base::{BOOL, NO, YES};
7use cocoa::foundation::NSUInteger;
8use cocoa::{
9 appkit::{NSApp, NSRequestUserAttentionType},
10 base::{id, nil},
11};
12use objc::{class, msg_send, sel, sel_impl};
13use std::ffi::c_void;
14use std::path::Path;
15use std::sync::Arc;
16use strato_ui_core::clipboard::InMemoryClipboard;
17use strato_ui_core::keymap::Keystroke;
18use strato_ui_core::modals::{AlertDialog, ModalId};
19use strato_ui_core::notification::{NotificationSendError, RequestPermissionsOutcome};
20use strato_ui_core::platform::{
21 Cursor, FilePickerCallback, FilePickerConfiguration, MicrophoneAccessState,
22 SendNotificationErrorCallback, TerminationMode,
23};
24use strato_ui_core::ApplicationBundleInfo;
25use strato_ui_core::{
26 accessibility::AccessibilityContent, notification::UserNotification, platform, WindowId,
27};
28 
29// Functions implemented in objC files.
30extern "C" {
31 // Requests permissions to send desktop notifications.
32 fn requestNotificationPermissions(on_completion_callback: *const c_void);
33 // Sends a desktop notification.
34 fn sendNotification(
35 title: id,
36 body: id,
37 data: id,
38 on_error_callback: *const c_void,
39 play_sound: BOOL,
40 );
41 fn isDarkMode() -> BOOL;
42 fn registerGlobalHotkey(key_code: NSUInteger, modifiers_key: NSUInteger);
43 fn unregisterGlobalHotkey(key_code: NSUInteger, modifiers_key: NSUInteger);
44 fn executableInApplicationBundleWithIdentifier(bundle_path: id) -> id;
45 fn absolutePathForApplicationBundleWithIdentifier(bundle_identifier: id) -> id;
46 fn isVoiceOverEnabled() -> BOOL;
47}
48 
49type RequestNotificationPermissionsCallback = Box<dyn FnOnce(RequestPermissionsOutcome) + Send>;
50type NotificationSendErrorCallback = Box<dyn FnOnce(NotificationSendError) + Send>;
51 
52/// Delegator that wraps platform-specific calls in a common API.
53pub struct AppDelegate {
54 clipboard: Clipboard,
55 dispatch_delegate: Arc<DispatchDelegate>,
56}
57 
58pub struct IntegrationTestDelegate {
59 app_delegate: AppDelegate,
60 clipboard: InMemoryClipboard,
61}
62 
63impl IntegrationTestDelegate {
64 pub fn new() -> Result<Self> {
65 Ok(IntegrationTestDelegate {
66 app_delegate: AppDelegate::new()?,
67 clipboard: InMemoryClipboard::default(),
68 })
69 }
70}
71 
72impl platform::Delegate for IntegrationTestDelegate {
73 #[cfg(feature = "test-util")]
74 fn get_cursor_shape(&self) -> Cursor {
75 self.app_delegate.get_cursor_shape()
76 }
77 
78 fn set_cursor_shape(&self, cursor: Cursor) {
79 self.app_delegate.set_cursor_shape(cursor)
80 }
81 
82 fn open_url(&self, _: &str) {
83 // no-op
84 }
85 
86 fn open_file_path(&self, _: &Path) {
87 // no-op
88 }
89 
90 fn open_file_path_in_explorer(&self, _: &Path) {
91 // no-op
92 }
93 
94 fn open_file_picker(
95 &self,
96 _callback: FilePickerCallback,
97 _file_picker_config: FilePickerConfiguration,
98 ) {
99 // no-op
100 }
101 
102 fn open_save_file_picker(
103 &self,
104 _callback: platform::SaveFilePickerCallback,
105 _config: platform::SaveFilePickerConfiguration,
106 ) {
107 // no-op
108 }
109 
110 fn application_bundle_info(&self, _: &str) -> Option<ApplicationBundleInfo<'_>> {
111 None
112 }
113 
114 fn close_ime_async(&self, _window_id: WindowId) {
115 // no-op
116 }
117 
118 fn is_ime_open(&self) -> bool {
119 false
120 }
121 
122 fn open_character_palette(&self) {
123 // no-op
124 }
125 
126 fn set_accessibility_contents(&self, _: AccessibilityContent) {
127 // no-op
128 }
129 
130 fn request_user_attention(&self, _window_id: WindowId) {
131 // no-op
132 }
133 
134 fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
135 &mut self.clipboard
136 }
137 
138 fn request_desktop_notification_permissions(
139 &self,
140 _on_completion: platform::RequestNotificationPermissionsCallback,
141 ) {
142 // no-op
143 }
144 
145 fn send_desktop_notification(
146 &self,
147 _notification_content: UserNotification,
148 _window_id: WindowId,
149 _on_error: SendNotificationErrorCallback,
150 ) {
151 }
152 
153 fn system_theme(&self) -> platform::SystemTheme {
154 self.app_delegate.system_theme()
155 }
156 
157 fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
158 self.app_delegate.dispatch_delegate()
159 }
160 
161 fn register_global_shortcut(&self, shortcut: Keystroke) {
162 self.app_delegate.register_global_shortcut(shortcut)
163 }
164 
165 fn unregister_global_shortcut(&self, shortcut: &Keystroke) {
166 self.app_delegate.unregister_global_shortcut(shortcut)
167 }
168 
169 fn terminate_app(&self, termination_mode: TerminationMode) {
170 self.app_delegate.terminate_app(termination_mode);
171 }
172 
173 fn is_screen_reader_enabled(&self) -> Option<bool> {
174 self.app_delegate.is_screen_reader_enabled()
175 }
176 
177 fn microphone_access_state(&self) -> MicrophoneAccessState {
178 self.app_delegate.microphone_access_state()
179 }
180 
181 fn show_native_platform_modal(&self, _id: ModalId, _modal: AlertDialog) {
182 // no-op
183 }
184}
185 
186pub struct DispatchDelegate;
187 
188impl AppDelegate {
189 pub fn new() -> Result<Self> {
190 Ok(AppDelegate {
191 clipboard: Clipboard::new()?,
192 dispatch_delegate: Arc::new(DispatchDelegate),
193 })
194 }
195}
196 
197impl platform::Delegate for AppDelegate {
198 /// Sets the cursor shape
199 /// See https://developer.apple.com/documentation/appkit/nscursor?language=objc
200 fn set_cursor_shape(&self, cursor: Cursor) {
201 unsafe {
202 let cursor: id = match cursor {
203 Cursor::Arrow => msg_send![class!(NSCursor), arrowCursor],
204 Cursor::IBeam => msg_send![class!(NSCursor), IBeamCursor],
205 Cursor::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
206 Cursor::OpenHand => msg_send![class!(NSCursor), openHandCursor],
207 Cursor::NotAllowed => msg_send![class!(NSCursor), operationNotAllowedCursor],
208 Cursor::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
209 Cursor::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
210 Cursor::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
211 Cursor::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
212 Cursor::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
213 };
214 let () = msg_send![cursor, set];
215 }
216 }
217 
218 #[cfg(feature = "test-util")]
219 fn get_cursor_shape(&self) -> Cursor {
220 unimplemented!("only implemented in tests")
221 }
222 fn open_url(&self, url: &str) {
223 Window::open_url(url);
224 }
225 
226 fn open_file_path(&self, path: &Path) {
227 Window::open_file_path(path);
228 }
229 
230 fn open_file_path_in_explorer(&self, path: &Path) {
231 Window::open_file_path_in_explorer(path);
232 }
233 
234 fn open_file_picker(
235 &self,
236 callback: FilePickerCallback,
237 file_picker_config: FilePickerConfiguration,
238 ) {
239 Window::open_file_picker(callback, file_picker_config);
240 }
241 
242 fn open_save_file_picker(
243 &self,
244 callback: platform::SaveFilePickerCallback,
245 config: platform::SaveFilePickerConfiguration,
246 ) {
247 Window::open_save_file_picker(callback, config);
248 }
249 
250 fn application_bundle_info(
251 &self,
252 bundle_identifier: &str,
253 ) -> Option<ApplicationBundleInfo<'_>> {
254 let bundle_path = unsafe {
255 let nsstring =
256 absolutePathForApplicationBundleWithIdentifier(make_nsstring(bundle_identifier));
257 
258 if nsstring == nil {
259 return None;
260 }
261 
262 nsstring_as_str(nsstring).ok()?
263 };
264 
265 let executable_path = unsafe {
266 let nsstring = executableInApplicationBundleWithIdentifier(make_nsstring(bundle_path));
267 
268 if nsstring == nil {
269 None
270 } else {
271 nsstring_as_str(nsstring).map(Path::new).ok()
272 }
273 };
274 
275 Some(ApplicationBundleInfo {
276 path: Path::new(bundle_path),
277 executable: executable_path,
278 })
279 }
280 
281 /// Open the macOS character palette.
282 fn open_character_palette(&self) {
283 // Open the character palette in a async task on the main thread to
284 // ensure we don't double-borrow the app.
285 dispatch::Queue::main().exec_async(move || unsafe {
286 // See https://developer.apple.com/documentation/appkit/nsapplication/1428455-orderfrontcharacterpalette.
287 // If the `sender` argument is nil, the palette is shown relative to the
288 // first responder's cursor location. In our case, that will be the Warp
289 // host view, with a location set via the `active_cursor_position` API.
290 let () = msg_send![NSApp(), orderFrontCharacterPalette: nil];
291 });
292 }
293 
294 fn close_ime_async(&self, window_id: WindowId) {
295 Window::close_ime_async(window_id);
296 }
297 
298 fn is_ime_open(&self) -> bool {
299 Window::is_ime_open()
300 }
301 
302 fn set_accessibility_contents(&self, content: AccessibilityContent) {
303 Window::set_accessibility_contents(content);
304 }
305 
306 fn request_user_attention(&self, _window_id: WindowId) {
307 unsafe {
308 let () = msg_send![
309 NSApp(),
310 requestUserAttention: NSRequestUserAttentionType::NSInformationalRequest
311 ];
312 }
313 }
314 
315 fn request_desktop_notification_permissions(
316 &self,
317 on_completion_callback: platform::RequestNotificationPermissionsCallback,
318 ) {
319 unsafe {
320 let callback: RequestNotificationPermissionsCallback = Box::new(|outcome| {
321 app::callback_dispatcher().with_mutable_app_context(|ctx| {
322 on_completion_callback(outcome, ctx);
323 })
324 });
325 requestNotificationPermissions(Box::into_raw(Box::new(callback)) as *const c_void);
326 };
327 }
328 
329 fn send_desktop_notification(
330 &self,
331 notification_content: UserNotification,
332 _window_id: WindowId,
333 on_error_callback: SendNotificationErrorCallback,
334 ) {
335 unsafe {
336 let callback: NotificationSendErrorCallback = Box::new(|error| {
337 app::callback_dispatcher().with_mutable_app_context(|ctx| {
338 on_error_callback(error, ctx);
339 })
340 });
341 sendNotification(
342 make_nsstring(notification_content.title()),
343 make_nsstring(notification_content.body()),
344 make_nsstring(notification_content.data().unwrap_or_default()),
345 Box::into_raw(Box::new(callback)) as *const c_void,
346 if notification_content.play_sound() {
347 YES
348 } else {
349 NO
350 },
351 );
352 };
353 }
354 
355 fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
356 &mut self.clipboard
357 }
358 
359 fn system_theme(&self) -> platform::SystemTheme {
360 unsafe {
361 let dark_mode = isDarkMode();
362 if dark_mode == YES {
363 platform::SystemTheme::Dark
364 } else {
365 platform::SystemTheme::Light
366 }
367 }
368 }
369 
370 fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
371 self.dispatch_delegate.clone()
372 }
373 
374 fn show_native_platform_modal(&self, id: ModalId, modal: AlertDialog) {
375 let alert = create_native_platform_modal(modal);
376 unsafe {
377 let _: () = msg_send![app::get_warp_app(), showModal: alert modalId: id];
378 }
379 }
380 
381 fn register_global_shortcut(&self, shortcut: Keystroke) {
382 unsafe {
383 for shortcut_key in Keycode::keycodes_from_key_name(&shortcut.key) {
384 registerGlobalHotkey(shortcut_key.0.into(), modifier_code(&shortcut).into());
385 }
386 }
387 }
388 
389 fn unregister_global_shortcut(&self, shortcut: &Keystroke) {
390 unsafe {
391 for shortcut_key in Keycode::keycodes_from_key_name(&shortcut.key) {
392 unregisterGlobalHotkey(shortcut_key.0.into(), modifier_code(shortcut).into());
393 }
394 }
395 }
396 
397 fn terminate_app(&self, termination_mode: TerminationMode) {
398 // Execute `[NSApp terminate]` asynchronously on the main thread to
399 // ensure we don't accidentally run into any double-borrow errors.
400 dispatch::Queue::main().exec_async(move || unsafe {
401 match termination_mode {
402 // ContentTransferred windows have already moved their content to another
403 // window (e.g. during tab drag), so they can close immediately without
404 // prompting the user for confirmation.
405 TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => {
406 let _: () = msg_send![NSApp(), setForceTermination];
407 }
408 TerminationMode::Cancellable => {}
409 }
410 let _: () = msg_send![NSApp(), terminate: nil];
411 });
412 }
413 
414 fn is_screen_reader_enabled(&self) -> Option<bool> {
415 unsafe { Some(isVoiceOverEnabled() == YES) }
416 }
417 
418 fn microphone_access_state(&self) -> MicrophoneAccessState {
419 unsafe {
420 let cls = class!(AVCaptureDevice);
421 // "soun" is not a typo, it's the correct constant name.
422 let media_type_audio = make_nsstring("soun");
423 
424 // AVAuthorizationStatus constants:
425 // 0 = AVAuthorizationStatusNotDetermined - User has not yet made a choice
426 // 1 = AVAuthorizationStatusRestricted - Restricted by system settings/parental controls
427 // 2 = AVAuthorizationStatusDenied - User explicitly denied access
428 // 3 = AVAuthorizationStatusAuthorized - User granted access
429 let status: i32 = msg_send![cls, authorizationStatusForMediaType: media_type_audio];
430 match status {
431 0 => MicrophoneAccessState::NotDetermined,
432 1 => MicrophoneAccessState::Restricted,
433 2 => MicrophoneAccessState::Denied,
434 3 => MicrophoneAccessState::Authorized,
435 _ => MicrophoneAccessState::NotDetermined, // fallback
436 }
437 }
438 }
439}
440 
441#[no_mangle]
442/// # Safety
443/// This function is marked unsafe because it retrieves the pointer to the callback
444/// function that we sent down to the Objective-C code.
445pub unsafe extern "C-unwind" fn warp_on_request_notification_permissions_completed(
446 result_type: NSUInteger,
447 result_msg: id,
448 callback: *mut c_void,
449) {
450 let outcome =
451 super::notification::request_permissions_outcome_from_native(result_type, result_msg);
452 if let Ok(outcome) = outcome {
453 let callback = Box::from_raw(callback as *mut RequestNotificationPermissionsCallback);
454 callback(outcome);
455 }
456}
457 
458#[no_mangle]
459/// # Safety
460/// This function is marked unsafe because it retrieves the pointer to the callback
461/// function that we sent down to the Objective-C code.
462pub unsafe extern "C-unwind" fn warp_on_notification_send_error(
463 error_type: NSUInteger,
464 error_msg: id,
465 callback: *mut c_void,
466) {
467 let notification_error = super::notification::send_error_from_native(error_type, error_msg);
468 if let Ok(notification_error) = notification_error {
469 let callback = Box::from_raw(callback as *mut NotificationSendErrorCallback);
470 callback(notification_error);
471 }
472}
473 
474impl platform::DispatchDelegate for DispatchDelegate {
475 fn is_main_thread(&self) -> bool {
476 let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
477 is_main_thread == YES
478 }
479 
480 fn run_on_main_thread(&self, task: async_task::Runnable) {
481 dispatch::Queue::main().exec_async(move || {
482 task.run();
483 });
484 }
485}
486