Skip to content

Commit

Permalink
feat: add support to deeplink and file association on macOS (#422)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Co-authored-by: amrbashir <amr.bashir2015@gmail.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
  • Loading branch information
4 people committed Jul 12, 2023
1 parent ea14c6b commit 093d8fb
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changes/support-open-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tao": minor
---

Add `Event::OpenURLs` on macOS.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
19 changes: 5 additions & 14 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<url::Url> },
}

impl<T: Clone> Clone for Event<'static, T> {
Expand Down Expand Up @@ -217,9 +210,7 @@ impl<T: Clone> 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() },
}
}
}
Expand Down Expand Up @@ -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 }),
}
}

Expand Down Expand Up @@ -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 }),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> EventLoopExtMacOS for EventLoop<T> {
#[inline]
fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) {
Expand Down
6 changes: 2 additions & 4 deletions src/platform_impl/ios/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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
}
Expand Down
31 changes: 29 additions & 2 deletions src/platform_impl/macos/app_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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 {
Expand All @@ -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));
Expand All @@ -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())
Expand Down Expand Up @@ -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<AuxDelegateState>);
drop(Box::from_raw(state_ptr as *mut RefCell<AuxDelegateState>));
}
}

Expand All @@ -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::<Vec<_>>()
};
trace!("Get `application:openURLs:` URLs: {:?}", urls);
AppState::open_urls(urls);
trace!("Completed `application:openURLs:`");
}
4 changes: 4 additions & 0 deletions src/platform_impl/macos/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ impl AppState {
HANDLER.set_in_callback(false);
}

pub fn open_urls(urls: Vec<url::Url>) {
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls }));
}

pub fn wakeup(panic_info: Weak<PanicInfo>) {
let panic_info = panic_info
.upgrade()
Expand Down
43 changes: 24 additions & 19 deletions src/platform_impl/macos/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,28 +130,10 @@ pub struct EventLoop<T: 'static> {

impl<T> EventLoop<T> {
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<PanicInfo> = 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,
Expand All @@ -177,6 +159,29 @@ impl<T> EventLoop<T> {
where
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &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
Expand Down
4 changes: 2 additions & 2 deletions src/platform_impl/macos/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/platform_impl/macos/window_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ fn with_state<F: FnOnce(&mut WindowDelegateState) -> 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));
});
}

Expand Down

0 comments on commit 093d8fb

Please sign in to comment.