StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use cocoa::appkit::NSApp; |
| 2 | use cocoa::foundation::{NSUInteger, NSURL}; |
| 3 | use cocoa::{ |
| 4 | base::{id, nil}, |
| 5 | foundation::{NSArray, NSAutoreleasePool, NSData, NSString}, |
| 6 | }; |
| 7 | use futures_util::future::LocalBoxFuture; |
| 8 | use objc::{ |
| 9 | class, msg_send, |
| 10 | runtime::{Object, Sel, BOOL, NO, YES}, |
| 11 | sel, sel_impl, |
| 12 | }; |
| 13 | |
| 14 | use std::{ |
| 15 | borrow::Cow, |
| 16 | ffi::CStr, |
| 17 | os::raw::{c_char, c_void}, |
| 18 | path::PathBuf, |
| 19 | }; |
| 20 | |
| 21 | use crate::platform::{ |
| 22 | app::{AppBackend, AppBuilder}, |
| 23 | AsInnerMut, |
| 24 | }; |
| 25 | use strato_ui_core::{ |
| 26 | assets::AssetProvider, |
| 27 | integration::TestDriver, |
| 28 | keymap::{Keystroke, Trigger}, |
| 29 | modals::{AlertDialog, ModalId}, |
| 30 | platform::app::{AppCallbackDispatcher, ApproveTerminateResult}, |
| 31 | platform::menu::{Menu, MenuBar}, |
| 32 | platform::SaveFilePickerCallback, |
| 33 | platform::{self, FilePickerCallback}, |
| 34 | AppContext, Event, |
| 35 | }; |
| 36 | |
| 37 | use super::{ |
| 38 | keycode::{Keycode, CMD_KEY, CONTROL_KEY, OPTION_KEY, SHIFT_KEY}, |
| 39 | make_nsstring, |
| 40 | menus::{make_dock_menu, make_main_menu}, |
| 41 | window::{get_window_state, IntegrationTestWindowManager, Window, WindowManager}, |
| 42 | }; |
| 43 | |
| 44 | pub trait NSAlert: Sized { |
| 45 | unsafe fn alloc(_: Self) -> id { |
| 46 | msg_send![class!(NSAlert), alloc] |
| 47 | } |
| 48 | |
| 49 | unsafe fn init(self) -> id; |
| 50 | unsafe fn autorelease(self) -> id; |
| 51 | unsafe fn set_message_text(self, message_text: id); |
| 52 | unsafe fn set_informative_text(self, informative_text: id); |
| 53 | unsafe fn add_button_with_title(self, title: id); |
| 54 | } |
| 55 | |
| 56 | impl NSAlert for id { |
| 57 | unsafe fn init(self) -> id { |
| 58 | msg_send![self, init] |
| 59 | } |
| 60 | |
| 61 | unsafe fn autorelease(self) -> id { |
| 62 | msg_send![self, autorelease] |
| 63 | } |
| 64 | |
| 65 | unsafe fn set_message_text(self, message_text: id) { |
| 66 | msg_send![self, setMessageText: message_text] |
| 67 | } |
| 68 | |
| 69 | unsafe fn set_informative_text(self, informative_text: id) { |
| 70 | msg_send![self, setInformativeText: informative_text] |
| 71 | } |
| 72 | |
| 73 | unsafe fn add_button_with_title(self, title: id) { |
| 74 | msg_send![self, addButtonWithTitle: title] |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | pub fn create_native_platform_modal(dialog: AlertDialog) -> id { |
| 79 | unsafe { |
| 80 | let alert = NSAlert::autorelease(NSAlert::init(NSAlert::alloc(nil))); |
| 81 | alert.set_informative_text(make_nsstring(&dialog.info_text)); |
| 82 | alert.set_message_text(make_nsstring(&dialog.message_text)); |
| 83 | for title in dialog.buttons { |
| 84 | alert.add_button_with_title(make_nsstring(&title)); |
| 85 | } |
| 86 | alert |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | const RUST_WRAPPER_IVAR_NAME: &str = "rustWrapper"; |
| 91 | |
| 92 | extern "C" { |
| 93 | // Implemented in ObjC to get the warp NSApplication subclass. |
| 94 | pub(super) fn get_warp_app() -> id; |
| 95 | } |
| 96 | |
| 97 | /// An extension trait defining additional configurability for |
| 98 | /// applications when running on macOS. |
| 99 | pub trait AppExt { |
| 100 | /// Sets whether or not the application should be activated |
| 101 | /// when it is launched. |
| 102 | fn set_activate_on_launch(&mut self, value: bool); |
| 103 | |
| 104 | /// Sets the application icon which should be used when running |
| 105 | /// without an application bundle. |
| 106 | fn set_dev_icon(&mut self, value: Cow<'static, [u8]>); |
| 107 | |
| 108 | /// Sets the main menu bar constructor function. |
| 109 | fn set_menu_bar_builder(&mut self, value: impl FnOnce(&mut AppContext) -> MenuBar + 'static); |
| 110 | |
| 111 | /// Sets the macOS dock menu constructor function. |
| 112 | fn set_dock_menu_builder(&mut self, value: impl FnOnce(&mut AppContext) -> Menu + 'static); |
| 113 | } |
| 114 | |
| 115 | type MenuBarBuilderFn = Box<dyn FnOnce(&mut AppContext) -> MenuBar>; |
| 116 | type DockMenuBuilderFn = Box<dyn FnOnce(&mut AppContext) -> Menu>; |
| 117 | |
| 118 | /// The actual application, from the perspective of the platform and the |
| 119 | /// main event loop. This is the true owner of all application state. |
| 120 | pub struct App { |
| 121 | callbacks: AppCallbackDispatcher, |
| 122 | activate_on_launch: bool, |
| 123 | dev_icon: Option<Cow<'static, [u8]>>, |
| 124 | menu_bar_builder: Option<MenuBarBuilderFn>, |
| 125 | dock_menu_builder: Option<DockMenuBuilderFn>, |
| 126 | init_fn: Option<platform::app::AppInitCallbackFn>, |
| 127 | } |
| 128 | |
| 129 | impl App { |
| 130 | pub(in crate::platform) fn new( |
| 131 | callbacks: platform::app::AppCallbacks, |
| 132 | assets: Box<dyn AssetProvider>, |
| 133 | test_driver: Option<&TestDriver>, |
| 134 | ) -> Self { |
| 135 | let platform_delegate: Box<dyn platform::Delegate> = if test_driver.is_some() { |
| 136 | Box::new( |
| 137 | super::delegate::IntegrationTestDelegate::new() |
| 138 | .expect("should not fail to create platform delegate"), |
| 139 | ) |
| 140 | } else { |
| 141 | Box::new( |
| 142 | super::delegate::AppDelegate::new() |
| 143 | .expect("should not fail to create platform delegate"), |
| 144 | ) |
| 145 | }; |
| 146 | |
| 147 | let window_manager: Box<dyn platform::WindowManager> = if test_driver.is_some() { |
| 148 | Box::new(IntegrationTestWindowManager::new()) |
| 149 | } else { |
| 150 | Box::new(WindowManager::new()) |
| 151 | }; |
| 152 | |
| 153 | let ui_app = crate::App::new( |
| 154 | platform_delegate, |
| 155 | window_manager, |
| 156 | Box::new(super::fonts::FontDB::new()), |
| 157 | assets, |
| 158 | ) |
| 159 | .expect("should not fail to construct application"); |
| 160 | |
| 161 | Self { |
| 162 | callbacks: AppCallbackDispatcher::new(callbacks, ui_app), |
| 163 | activate_on_launch: true, |
| 164 | dev_icon: None, |
| 165 | menu_bar_builder: None, |
| 166 | dock_menu_builder: None, |
| 167 | init_fn: None, |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | pub(in crate::platform) fn run( |
| 172 | mut self, |
| 173 | init_fn: impl FnOnce(&mut AppContext, LocalBoxFuture<'static, crate::App>) + 'static, |
| 174 | ) { |
| 175 | self.init_fn = Some(Box::new(init_fn)); |
| 176 | |
| 177 | unsafe { |
| 178 | let pool = NSAutoreleasePool::new(nil); |
| 179 | |
| 180 | // Get (and create, if necessary) the underlying NSApplication. |
| 181 | let app: id = get_warp_app(); |
| 182 | |
| 183 | let running_app: id = msg_send![class!(NSRunningApplication), currentApplication]; |
| 184 | let bundle_id: id = msg_send![running_app, bundleIdentifier]; |
| 185 | let dev_icon = if bundle_id.is_null() { |
| 186 | self.dev_icon.as_ref().map(|dev_icon| { |
| 187 | let data: id = msg_send![class!(NSData), alloc]; |
| 188 | let data: id = data.initWithBytes_length_( |
| 189 | dev_icon.as_ptr() as *const c_void, |
| 190 | dev_icon.len() as u64, |
| 191 | ); |
| 192 | let image: id = msg_send![class!(NSImage), alloc]; |
| 193 | image.initWithData_(data) |
| 194 | }) |
| 195 | } else { |
| 196 | None |
| 197 | }; |
| 198 | |
| 199 | let app_delegate: id = msg_send![app, delegate]; |
| 200 | |
| 201 | let self_ptr = Box::into_raw(Box::new(self)); |
| 202 | (*app).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void); |
| 203 | (*app_delegate).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void); |
| 204 | |
| 205 | if let Some(dev_icon) = dev_icon { |
| 206 | let _: () = msg_send![app, setApplicationIconImage: dev_icon]; |
| 207 | } |
| 208 | |
| 209 | let _: () = msg_send![app, run]; |
| 210 | let _: () = msg_send![pool, drain]; |
| 211 | |
| 212 | // App is done running when we get here, so we can reinstantiate the Box and drop it. |
| 213 | drop(Box::from_raw(self_ptr)); |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | impl AppExt for AppBuilder { |
| 219 | fn set_activate_on_launch(&mut self, value: bool) { |
| 220 | match self.as_inner_mut() { |
| 221 | AppBackend::CurrentPlatform(app) => app.activate_on_launch = value, |
| 222 | AppBackend::Headless(_) => (), |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | fn set_dev_icon(&mut self, value: Cow<'static, [u8]>) { |
| 227 | match self.as_inner_mut() { |
| 228 | AppBackend::CurrentPlatform(app) => app.dev_icon = Some(value), |
| 229 | AppBackend::Headless(_) => (), |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | fn set_menu_bar_builder(&mut self, value: impl FnOnce(&mut AppContext) -> MenuBar + 'static) { |
| 234 | match self.as_inner_mut() { |
| 235 | AppBackend::CurrentPlatform(app) => app.menu_bar_builder = Some(Box::new(value)), |
| 236 | AppBackend::Headless(_) => (), |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | fn set_dock_menu_builder(&mut self, value: impl FnOnce(&mut AppContext) -> Menu + 'static) { |
| 241 | match self.as_inner_mut() { |
| 242 | AppBackend::CurrentPlatform(app) => app.dock_menu_builder = Some(Box::new(value)), |
| 243 | AppBackend::Headless(_) => (), |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | unsafe fn get_app(object: &mut Object) -> &mut App { |
| 249 | let wrapper_ptr: *mut c_void = *object.get_ivar(RUST_WRAPPER_IVAR_NAME); |
| 250 | &mut *(wrapper_ptr as *mut App) |
| 251 | } |
| 252 | |
| 253 | pub(super) fn callback_dispatcher() -> &'static mut AppCallbackDispatcher { |
| 254 | unsafe { |
| 255 | let app = get_warp_app(); |
| 256 | let app = get_app(&mut *app); |
| 257 | &mut app.callbacks |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | #[no_mangle] |
| 262 | pub(crate) extern "C-unwind" fn warp_app_send_global_keybinding( |
| 263 | this: &mut Object, |
| 264 | modifiers: NSUInteger, |
| 265 | key_code: NSUInteger, |
| 266 | ) { |
| 267 | let keystroke = { |
| 268 | let modifiers = modifiers as u16; |
| 269 | let shift_key_pressed = (modifiers & SHIFT_KEY) > 0; |
| 270 | Keycode(key_code as u16) |
| 271 | .try_to_key_name(shift_key_pressed) |
| 272 | .map(|key| Keystroke { |
| 273 | ctrl: (modifiers & CONTROL_KEY) > 0, |
| 274 | alt: (modifiers & OPTION_KEY) > 0, |
| 275 | shift: shift_key_pressed, |
| 276 | cmd: (modifiers & CMD_KEY) > 0, |
| 277 | meta: false, |
| 278 | key, |
| 279 | }) |
| 280 | }; |
| 281 | |
| 282 | if let Some(keystroke) = keystroke { |
| 283 | let app = unsafe { get_app(this) }; |
| 284 | app.callbacks.global_shortcut_triggered(keystroke); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | #[no_mangle] |
| 289 | pub unsafe extern "C-unwind" fn warp_app_will_finish_launching(this: &mut Object) { |
| 290 | log::info!("application will finish launching"); |
| 291 | |
| 292 | let app = get_app(this); |
| 293 | |
| 294 | if app.activate_on_launch { |
| 295 | let _: () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; |
| 296 | } |
| 297 | |
| 298 | if let Some(init_fn) = app.init_fn.take() { |
| 299 | app.callbacks.initialize_app(init_fn); |
| 300 | } |
| 301 | |
| 302 | let app_delegate: id = msg_send![NSApp(), delegate]; |
| 303 | |
| 304 | if app.callbacks.has_internet_reachability_changed_callback() { |
| 305 | let _: () = msg_send![app_delegate, setReachabilityListener]; |
| 306 | } |
| 307 | |
| 308 | if let Some(menu_bar_builder) = app.menu_bar_builder.take() { |
| 309 | let menu_bar = app.callbacks.with_mutable_app_context(menu_bar_builder); |
| 310 | let nsmenu = make_main_menu(menu_bar); |
| 311 | let () = msg_send![NSApp(), setMainMenu: nsmenu]; |
| 312 | } |
| 313 | |
| 314 | if let Some(dock_menu_builder) = app.dock_menu_builder.take() { |
| 315 | let dock_menu = app.callbacks.with_mutable_app_context(dock_menu_builder); |
| 316 | let nsmenu = make_dock_menu(dock_menu); |
| 317 | let _: () = msg_send![app_delegate, setDockMenu: nsmenu]; |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | #[no_mangle] |
| 322 | pub(crate) extern "C-unwind" fn warp_app_did_become_active(this: &mut Object, _: Sel, _: id) { |
| 323 | let app = unsafe { get_app(this) }; |
| 324 | app.callbacks.app_became_active(); |
| 325 | } |
| 326 | |
| 327 | #[no_mangle] |
| 328 | pub(crate) extern "C-unwind" fn warp_app_internet_reachability_changed( |
| 329 | this: &mut Object, |
| 330 | can_reach: u8, |
| 331 | ) { |
| 332 | let is_reachable = can_reach != 0; |
| 333 | |
| 334 | let app = unsafe { get_app(this) }; |
| 335 | app.callbacks.internet_reachability_changed(is_reachable); |
| 336 | } |
| 337 | |
| 338 | /// Returns whether or not we can proceed with termination. |
| 339 | #[no_mangle] |
| 340 | pub(crate) extern "C-unwind" fn warp_app_should_terminate_app(this: &mut Object) -> BOOL { |
| 341 | let app = unsafe { get_app(this) }; |
| 342 | |
| 343 | match app.callbacks.should_terminate_app() { |
| 344 | ApproveTerminateResult::Terminate => YES, |
| 345 | ApproveTerminateResult::Cancel => NO, |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | /// Returns a NSAlert object if we want to show a dialog for users to confirm or |
| 350 | /// nil for closing the window immediately. |
| 351 | #[no_mangle] |
| 352 | pub(crate) extern "C-unwind" fn warp_app_should_close_window( |
| 353 | this: &mut Object, |
| 354 | window_id: &mut Object, |
| 355 | ) -> BOOL { |
| 356 | let app = unsafe { get_app(this) }; |
| 357 | let window = unsafe { get_window_state(window_id) }; |
| 358 | |
| 359 | match app.callbacks.should_close_window(window.id()) { |
| 360 | ApproveTerminateResult::Terminate => YES, |
| 361 | ApproveTerminateResult::Cancel => NO, |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | #[no_mangle] |
| 366 | pub(crate) extern "C-unwind" fn warp_app_are_key_bindings_disabled_for_window( |
| 367 | this: &mut Object, |
| 368 | window_id: &mut Object, |
| 369 | ) -> BOOL { |
| 370 | let app = unsafe { get_app(this) }; |
| 371 | let window = unsafe { get_window_state(window_id) }; |
| 372 | |
| 373 | let disabled = app |
| 374 | .callbacks |
| 375 | .with_mutable_app_context(|ctx| !ctx.key_bindings_enabled(window.id())); |
| 376 | |
| 377 | if disabled { |
| 378 | YES |
| 379 | } else { |
| 380 | NO |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | #[no_mangle] |
| 385 | pub(crate) extern "C-unwind" fn warp_app_has_binding_for_keystroke( |
| 386 | this: &mut Object, |
| 387 | event: id, |
| 388 | ) -> BOOL { |
| 389 | let app = unsafe { get_app(this) }; |
| 390 | let warp_event = unsafe { super::event::from_native(event, None, false) }; |
| 391 | |
| 392 | let Some(Event::KeyDown { keystroke, .. }) = warp_event else { |
| 393 | return NO; |
| 394 | }; |
| 395 | let has_binding = app.callbacks.with_mutable_app_context(|ctx| { |
| 396 | ctx.get_key_bindings().any(|binding| { |
| 397 | if let Trigger::Keystrokes(keystrokes) = binding.trigger { |
| 398 | keystrokes.len() == 1 && keystrokes[0] == keystroke |
| 399 | } else { |
| 400 | false |
| 401 | } |
| 402 | }) |
| 403 | }); |
| 404 | |
| 405 | if has_binding { |
| 406 | YES |
| 407 | } else { |
| 408 | NO |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | #[no_mangle] |
| 413 | pub(crate) extern "C-unwind" fn warp_app_has_custom_action_for_keystroke( |
| 414 | this: &mut Object, |
| 415 | event: id, |
| 416 | ) -> BOOL { |
| 417 | let app = unsafe { get_app(this) }; |
| 418 | let warp_event = unsafe { super::event::from_native(event, None, false) }; |
| 419 | |
| 420 | let Some(Event::KeyDown { keystroke, .. }) = warp_event else { |
| 421 | return NO; |
| 422 | }; |
| 423 | let has_binding = app.callbacks.with_mutable_app_context(|ctx| { |
| 424 | ctx.custom_action_bindings() |
| 425 | .any(|binding| match binding.trigger { |
| 426 | Trigger::Keystrokes(keystrokes) => { |
| 427 | keystrokes.len() == 1 && keystrokes[0] == keystroke |
| 428 | } |
| 429 | Trigger::Custom(tag) => ctx |
| 430 | .default_keystroke_trigger_for_custom_action(*tag) |
| 431 | .is_some_and(|k| k == keystroke), |
| 432 | _ => false, |
| 433 | }) |
| 434 | }); |
| 435 | |
| 436 | if has_binding { |
| 437 | YES |
| 438 | } else { |
| 439 | NO |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | #[no_mangle] |
| 444 | pub(crate) extern "C-unwind" fn warp_app_disable_warning_modal(this: &mut Object) { |
| 445 | let app = unsafe { get_app(this) }; |
| 446 | app.callbacks.warning_modal_disabled(); |
| 447 | } |
| 448 | |
| 449 | #[no_mangle] |
| 450 | pub(crate) extern "C-unwind" fn warp_app_process_modal_response( |
| 451 | this: &mut Object, |
| 452 | modal_id: ModalId, |
| 453 | response: usize, |
| 454 | disable_modal: bool, |
| 455 | ) { |
| 456 | let app = unsafe { get_app(this) }; |
| 457 | app.callbacks |
| 458 | .process_platform_modal_response(modal_id, response, disable_modal); |
| 459 | } |
| 460 | |
| 461 | #[no_mangle] |
| 462 | pub(crate) extern "C-unwind" fn warp_app_notification_clicked( |
| 463 | this: &mut Object, |
| 464 | date: f64, |
| 465 | data: id, |
| 466 | ) { |
| 467 | let app = unsafe { get_app(this) }; |
| 468 | if let Ok(notification_response) = |
| 469 | unsafe { super::notification::response_from_native(date as i32, data) } |
| 470 | { |
| 471 | app.callbacks.notification_clicked(notification_response); |
| 472 | } |
| 473 | } |
| 474 | |
| 475 | #[no_mangle] |
| 476 | extern "C-unwind" fn warp_app_did_resign_active(this: &mut Object, _: Sel, _: id) { |
| 477 | let app = unsafe { get_app(this) }; |
| 478 | app.callbacks.app_resigned_active(); |
| 479 | } |
| 480 | |
| 481 | #[no_mangle] |
| 482 | extern "C-unwind" fn warp_app_will_terminate(this: &mut Object, _: Sel, _: id) { |
| 483 | let app = unsafe { get_app(this) }; |
| 484 | app.callbacks.app_will_terminate(); |
| 485 | } |
| 486 | |
| 487 | #[no_mangle] |
| 488 | extern "C-unwind" fn warp_app_new_window(this: &mut Object) { |
| 489 | let app = unsafe { get_app(this) }; |
| 490 | app.callbacks.open_new_window(); |
| 491 | } |
| 492 | |
| 493 | #[no_mangle] |
| 494 | extern "C-unwind" fn warp_app_active_window_changed(this: &mut Object) { |
| 495 | let app = unsafe { get_app(this) }; |
| 496 | Window::close_ime_on_active_window(); |
| 497 | app.callbacks |
| 498 | .active_window_changed(Window::active_window_id()); |
| 499 | } |
| 500 | |
| 501 | #[no_mangle] |
| 502 | extern "C-unwind" fn warp_app_window_did_resize(this: &mut Object) { |
| 503 | let app = unsafe { get_app(this) }; |
| 504 | app.callbacks.window_resized(); |
| 505 | } |
| 506 | |
| 507 | #[no_mangle] |
| 508 | extern "C-unwind" fn warp_app_window_did_move(this: &mut Object) { |
| 509 | let app = unsafe { get_app(this) }; |
| 510 | app.callbacks.window_moved(); |
| 511 | } |
| 512 | |
| 513 | #[no_mangle] |
| 514 | extern "C-unwind" fn warp_app_window_will_close(this: &mut Object, window: &mut Object) { |
| 515 | let app = unsafe { get_app(this) }; |
| 516 | let window_state = unsafe { get_window_state(window) }; |
| 517 | app.callbacks.window_will_close(window_state.id()); |
| 518 | } |
| 519 | |
| 520 | #[no_mangle] |
| 521 | extern "C-unwind" fn warp_app_screen_did_change(this: &mut Object) { |
| 522 | log::info!("received NSApplicationDidChangeScreenParametersNotification"); |
| 523 | let app = unsafe { get_app(this) }; |
| 524 | app.callbacks.screen_changed(); |
| 525 | } |
| 526 | |
| 527 | #[no_mangle] |
| 528 | extern "C-unwind" fn cpu_awakened(this: &mut Object) { |
| 529 | let app = unsafe { get_app(this) }; |
| 530 | app.callbacks.cpu_awakened(); |
| 531 | } |
| 532 | |
| 533 | #[no_mangle] |
| 534 | extern "C-unwind" fn cpu_will_sleep(this: &mut Object) { |
| 535 | let app = unsafe { get_app(this) }; |
| 536 | app.callbacks.cpu_will_sleep(); |
| 537 | } |
| 538 | |
| 539 | #[no_mangle] |
| 540 | extern "C-unwind" fn warp_app_open_files(this: &mut Object, paths: id) { |
| 541 | let paths = unsafe { |
| 542 | (0..paths.count()) |
| 543 | .filter_map(|i| { |
| 544 | let path = paths.objectAtIndex(i); |
| 545 | match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() { |
| 546 | Ok(string) => Some(PathBuf::from(string)), |
| 547 | Err(err) => { |
| 548 | log::error!("error converting path to string: {err}"); |
| 549 | None |
| 550 | } |
| 551 | } |
| 552 | }) |
| 553 | .collect::<Vec<_>>() |
| 554 | }; |
| 555 | let app = unsafe { get_app(this) }; |
| 556 | app.callbacks.open_files(paths); |
| 557 | } |
| 558 | |
| 559 | #[no_mangle] |
| 560 | extern "C-unwind" fn warp_app_open_urls(this: &mut Object, urls: id) { |
| 561 | let urls = unsafe { |
| 562 | (0..urls.count()) |
| 563 | .filter_map(|i| { |
| 564 | let url = urls.objectAtIndex(i).absoluteString(); |
| 565 | match CStr::from_ptr(url.UTF8String() as *mut c_char).to_str() { |
| 566 | Ok(string) => Some(string.to_string()), |
| 567 | Err(err) => { |
| 568 | log::error!("error converting url to string: {err}"); |
| 569 | None |
| 570 | } |
| 571 | } |
| 572 | }) |
| 573 | .collect::<Vec<_>>() |
| 574 | }; |
| 575 | |
| 576 | let app = unsafe { get_app(this) }; |
| 577 | app.callbacks.open_urls(urls); |
| 578 | } |
| 579 | |
| 580 | #[no_mangle] |
| 581 | extern "C-unwind" fn warp_app_os_appearance_changed(this: &mut Object) { |
| 582 | let app = unsafe { get_app(this) }; |
| 583 | app.callbacks.os_appearance_changed(); |
| 584 | } |
| 585 | |
| 586 | // Calls the callback with None if no file was selected |
| 587 | #[no_mangle] |
| 588 | pub(crate) extern "C-unwind" fn warp_open_panel_file_selected(urls: id, callback: *mut c_void) { |
| 589 | // Start by converting the callback from a raw pointer back into a Box, to |
| 590 | // avoid the memory leak that would occur if we left it in raw pointer form. |
| 591 | let callback = unsafe { Box::from_raw(callback as *mut FilePickerCallback) }; |
| 592 | |
| 593 | let paths = unsafe { |
| 594 | (0..urls.count()) |
| 595 | .map(|i| { |
| 596 | let file_url = urls.objectAtIndex(i); |
| 597 | let file_path: id = msg_send![file_url, path]; |
| 598 | let slice = std::slice::from_raw_parts( |
| 599 | file_path.UTF8String() as *const std::ffi::c_uchar, |
| 600 | file_path.len(), |
| 601 | ); |
| 602 | std::str::from_utf8_unchecked(slice).to_string() |
| 603 | }) |
| 604 | .collect::<Vec<_>>() |
| 605 | }; |
| 606 | |
| 607 | if paths.is_empty() { |
| 608 | log::info!("No file was selected. Dialog was cancelled.") |
| 609 | } |
| 610 | |
| 611 | let app = unsafe { get_app(&mut *get_warp_app()) }; |
| 612 | app.callbacks.with_mutable_app_context(move |ctx| { |
| 613 | callback(Ok(paths), ctx); |
| 614 | }); |
| 615 | } |
| 616 | |
| 617 | // Calls the save callback with the selected path or None if cancelled |
| 618 | #[no_mangle] |
| 619 | pub(crate) extern "C-unwind" fn warp_save_panel_file_selected(url: id, callback: *mut c_void) { |
| 620 | let callback = unsafe { Box::from_raw(callback as *mut SaveFilePickerCallback) }; |
| 621 | |
| 622 | let path = if url.is_null() { |
| 623 | None |
| 624 | } else { |
| 625 | unsafe { |
| 626 | let file_path: id = msg_send![url, path]; |
| 627 | let slice = std::slice::from_raw_parts( |
| 628 | file_path.UTF8String() as *const std::ffi::c_uchar, |
| 629 | file_path.len(), |
| 630 | ); |
| 631 | Some(std::str::from_utf8_unchecked(slice).to_string()) |
| 632 | } |
| 633 | }; |
| 634 | |
| 635 | if path.is_none() { |
| 636 | log::info!("Save dialog was cancelled."); |
| 637 | } |
| 638 | |
| 639 | let app = unsafe { get_app(&mut *get_warp_app()) }; |
| 640 | app.callbacks.with_mutable_app_context(move |ctx| { |
| 641 | callback(path, ctx); |
| 642 | }); |
| 643 | } |
| 644 |