From 093d8fbc20954d51c96c94618479ff465ca55888 Mon Sep 17 00:00:00 2001 From: Bo Date: Wed, 12 Jul 2023 19:46:26 +0800 Subject: [PATCH] feat: add support to deeplink and file association on macOS (#422) Co-authored-by: Lucas Nogueira Co-authored-by: amrbashir Co-authored-by: Lucas Nogueira --- .changes/support-open-file.md | 5 +++ Cargo.toml | 4 +- src/event.rs | 19 +++------- src/platform/macos.rs | 1 + src/platform_impl/ios/view.rs | 6 +-- src/platform_impl/macos/app_delegate.rs | 31 +++++++++++++++- src/platform_impl/macos/app_state.rs | 4 ++ src/platform_impl/macos/event_loop.rs | 43 ++++++++++++---------- src/platform_impl/macos/view.rs | 4 +- src/platform_impl/macos/window_delegate.rs | 2 +- 10 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 .changes/support-open-file.md diff --git a/.changes/support-open-file.md b/.changes/support-open-file.md new file mode 100644 index 000000000..b4de7ef6b --- /dev/null +++ b/.changes/support-open-file.md @@ -0,0 +1,5 @@ +--- +"tao": minor +--- + +Add `Event::OpenURLs` on macOS. diff --git a/Cargo.toml b/Cargo.toml index 87533c3a8..3267f441c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,8 @@ members = [ "tao-macros" ] [features] default = [] -dox = ["gtk/dox"] -tray = ["libappindicator", "dirs-next"] +dox = [ "gtk/dox"] +tray = [ "libappindicator", "dirs-next" ] [build-dependencies] cc = "1" diff --git a/src/event.rs b/src/event.rs index 788ccd987..ecc3ec657 100644 --- a/src/event.rs +++ b/src/event.rs @@ -165,15 +165,8 @@ pub enum Event<'a, T: 'static> { /// gets emitted. You generally want to treat this as an "do on quit" event. LoopDestroyed, - /// Emitted when the app is open by external resources, like opening a Url. - Opened { event: OpenEvent }, -} - -/// What the app is opening. -#[derive(Debug, PartialEq, Clone)] -pub enum OpenEvent { - /// App is opening an URL. - Url(url::Url), + /// Emitted when the app is open by external resources, like opening a file or deeplink. + Opened { urls: Vec }, } impl Clone for Event<'static, T> { @@ -217,9 +210,7 @@ impl Clone for Event<'static, T> { position: *position, }, GlobalShortcutEvent(accelerator_id) => GlobalShortcutEvent(*accelerator_id), - Opened { event } => Opened { - event: event.clone(), - }, + Opened { urls } => Opened { urls: urls.clone() }, } } } @@ -259,7 +250,7 @@ impl<'a, T> Event<'a, T> { position, }), GlobalShortcutEvent(accelerator_id) => Ok(GlobalShortcutEvent(accelerator_id)), - Opened { event } => Ok(Opened { event }), + Opened { urls } => Ok(Opened { urls }), } } @@ -301,7 +292,7 @@ impl<'a, T> Event<'a, T> { position, }), GlobalShortcutEvent(accelerator_id) => Some(GlobalShortcutEvent(accelerator_id)), - Opened { event } => Some(Opened { event }), + Opened { urls } => Some(Opened { urls }), } } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 04c52da94..7a92659d9 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -512,6 +512,7 @@ pub trait EventLoopExtMacOS { /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return) fn set_activate_ignoring_other_apps(&mut self, ignore: bool); } + impl EventLoopExtMacOS for EventLoop { #[inline] fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index 24ebaa10d..db06fa30a 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -11,7 +11,7 @@ use objc::{ use crate::{ dpi::PhysicalPosition, - event::{DeviceId as RootDeviceId, Event, Force, OpenEvent, Touch, TouchPhase, WindowEvent}, + event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, platform::ios::MonitorHandleExtIOS, platform_impl::platform::{ app_state::{self, OSCapabilities}, @@ -580,9 +580,7 @@ pub fn create_delegate_class() { let url = url::Url::parse(std::str::from_utf8(bytes).unwrap()).unwrap(); - app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { - event: OpenEvent::Url(url), - })); + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls: vec![url] })); YES } diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index eaddcb1d2..dcc697211 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -5,6 +5,7 @@ use crate::{platform::macos::ActivationPolicy, platform_impl::platform::app_state::AppState}; use cocoa::base::id; +use cocoa::foundation::NSString; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, @@ -14,6 +15,10 @@ use std::{ os::raw::c_void, }; +use cocoa::foundation::NSArray; +use cocoa::foundation::NSURL; +use std::ffi::CStr; + static AUX_DELEGATE_STATE_NAME: &str = "auxState"; pub struct AuxDelegateState { @@ -34,7 +39,7 @@ unsafe impl Sync for AppDelegateClass {} lazy_static! { pub static ref APP_DELEGATE_CLASS: AppDelegateClass = unsafe { let superclass = class!(NSResponder); - let mut decl = ClassDecl::new("TaoAppDelegate", superclass).unwrap(); + let mut decl = ClassDecl::new("TaoAppDelegateParent", superclass).unwrap(); decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id); decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); @@ -47,6 +52,10 @@ lazy_static! { sel!(applicationWillTerminate:), application_will_terminate as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(application:openURLs:), + application_open_urls as extern "C" fn(&Object, Sel, id, id), + ); decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME); AppDelegateClass(decl.register()) @@ -81,7 +90,7 @@ extern "C" fn dealloc(this: &Object, _: Sel) { let state_ptr: *mut c_void = *(this.get_ivar(AUX_DELEGATE_STATE_NAME)); // As soon as the box is constructed it is immediately dropped, releasing the underlying // memory - Box::from_raw(state_ptr as *mut RefCell); + drop(Box::from_raw(state_ptr as *mut RefCell)); } } @@ -96,3 +105,21 @@ extern "C" fn application_will_terminate(_: &Object, _: Sel, _: id) { AppState::exit(); trace!("Completed `applicationWillTerminate`"); } + +extern "C" fn application_open_urls(_: &Object, _: Sel, _: id, urls: id) -> () { + trace!("Trigger `application:openURLs:`"); + + let urls = unsafe { + (0..urls.count()) + .map(|i| { + url::Url::parse( + &CStr::from_ptr(urls.objectAtIndex(i).absoluteString().UTF8String()).to_string_lossy(), + ) + }) + .flatten() + .collect::>() + }; + trace!("Get `application:openURLs:` URLs: {:?}", urls); + AppState::open_urls(urls); + trace!("Completed `application:openURLs:`"); +} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 091c58adf..bae2cf7e2 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -302,6 +302,10 @@ impl AppState { HANDLER.set_in_callback(false); } + pub fn open_urls(urls: Vec) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls })); + } + pub fn wakeup(panic_info: Weak) { let panic_info = panic_info .upgrade() diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 78ebd13a8..93f6aed71 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -130,28 +130,10 @@ pub struct EventLoop { impl EventLoop { pub fn new() -> Self { - let delegate = unsafe { - let is_main_thread: BOOL = msg_send!(class!(NSThread), isMainThread); - if is_main_thread == NO { - panic!("On macOS, `EventLoop` must be created on the main thread!"); - } - - // This must be done before `NSApp()` (equivalent to sending - // `sharedApplication`) is called anywhere else, or we'll end up - // with the wrong `NSApplication` class and the wrong thread could - // be marked as main. - let app: id = msg_send![APP_CLASS.0, sharedApplication]; - - let delegate = IdRef::new(msg_send![APP_DELEGATE_CLASS.0, new]); - let pool = NSAutoreleasePool::new(nil); - let _: () = msg_send![app, setDelegate:*delegate]; - let _: () = msg_send![pool, drain]; - delegate - }; let panic_info: Rc = Default::default(); setup_control_flow_observers(Rc::downgrade(&panic_info)); EventLoop { - delegate, + delegate: IdRef::new(nil), window_target: Rc::new(RootWindowTarget { p: Default::default(), _marker: PhantomData, @@ -177,6 +159,29 @@ impl EventLoop { where F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { + // initialize app delegate if needed + if self.delegate.is_null() { + let delegate = unsafe { + let is_main_thread: BOOL = msg_send!(class!(NSThread), isMainThread); + if is_main_thread == NO { + panic!("On macOS, `EventLoop` must be created on the main thread!"); + } + + // This must be done before `NSApp()` (equivalent to sending + // `sharedApplication`) is called anywhere else, or we'll end up + // with the wrong `NSApplication` class and the wrong thread could + // be marked as main. + let app: id = msg_send![APP_CLASS.0, sharedApplication]; + + let delegate = IdRef::new(msg_send![APP_DELEGATE_CLASS.0, new]); + let pool = NSAutoreleasePool::new(nil); + let _: () = msg_send![app, setDelegate:*delegate]; + let _: () = msg_send![pool, drain]; + delegate + }; + self.delegate = delegate; + } + // This transmute is always safe, in case it was reached through `run`, since our // lifetime will be already 'static. In other cases caller should ensure that all data // they passed to callback will actually outlive it, some apps just can't move diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 5366cff87..9ae2d1d30 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -308,7 +308,7 @@ extern "C" fn dealloc(this: &Object, _sel: Sel) { let state: *mut c_void = *this.get_ivar("taoState"); let marked_text: id = *this.get_ivar("markedText"); let _: () = msg_send![marked_text, release]; - Box::from_raw(state as *mut ViewState); + drop(Box::from_raw(state as *mut ViewState)); } } @@ -573,7 +573,7 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran trace!("Completed `insertText`"); } -extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { +extern "C" fn do_command_by_selector(_this: &Object, _sel: Sel, _command: Sel) { trace!("Triggered `doCommandBySelector`"); // TODO: (Artur) all these inputs seem to trigger a key event with the correct text // content so this is not needed anymore, it seems. diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index faa716ac3..83ad6e035 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -276,7 +276,7 @@ fn with_state T, T>(this: &Object, callba extern "C" fn dealloc(this: &Object, _sel: Sel) { with_state(this, |state| unsafe { - Box::from_raw(state as *mut WindowDelegateState); + drop(Box::from_raw(state as *mut WindowDelegateState)); }); }