native/desktop-macos/src/macos/application_api.rs (548 lines of code) (raw):
use super::{
appearance::Appearance,
drag_and_drop::DragAndDropHandlerState,
events::EventHandler,
string::{copy_to_c_string, copy_to_ns_string},
text_direction::TextDirection,
};
use crate::macos::application_menu::{handle_app_menu_callback, set_initial_app_menu};
use crate::macos::application_menu_api::ItemId;
use crate::macos::events::{
handle_application_appearance_change, handle_application_did_finish_launching, handle_application_open_urls,
handle_display_configuration_change,
};
use crate::macos::image::Image;
use anyhow::{Context, anyhow};
use desktop_common::ffi_utils::AutoDropArray;
use desktop_common::{
ffi_utils::{BorrowedStrPtr, RustAllocatedStrPtr},
logger::{catch_panic, ffi_boundary},
};
use log::info;
use objc2::{
ClassType, DeclaredClass, MainThreadOnly, define_class, msg_send,
rc::Retained,
runtime::{AnyObject, ProtocolObject},
};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSApplicationTerminateReply, NSEvent, NSEventModifierFlags,
NSEventType, NSMenuItem, NSRequestUserAttentionType, NSRunningApplication, NSWorkspace,
};
use objc2_foundation::{
MainThreadMarker, NSArray, NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions, NSNotification, NSObject,
NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint, NSString, NSURL, NSUserDefaults, ns_string,
};
use std::{cell::OnceCell, ffi::c_void};
thread_local! {
pub static APP_STATE: OnceCell<AppState> = const { OnceCell::new() };
}
#[derive(Debug)]
pub(crate) struct AppState {
#[allow(dead_code)]
pub(crate) app: Retained<MyNSApplication>,
#[allow(dead_code)]
app_delegate: Retained<AppDelegate>,
pub(crate) event_handler: EventHandler,
pub(crate) drag_and_drop_handler_state: DragAndDropHandlerState,
pub(crate) mtm: MainThreadMarker,
}
impl AppState {
pub(crate) fn with<T, F>(f: F) -> T
where
F: FnOnce(&Self) -> T,
{
APP_STATE.with(|app_state| {
let app_state = app_state.get().expect("Can't access the app state before initialization!");
f(app_state)
})
}
}
#[repr(C)]
#[derive(Debug)]
pub struct ApplicationCallbacks {
// returns true if the application should terminate,
// otherwise termination will be canceled
pub on_should_terminate: extern "C" fn() -> bool,
pub on_will_terminate: extern "C" fn(),
pub event_handler: EventHandler,
}
#[repr(C)]
#[derive(Debug)]
pub struct ApplicationConfig {
pub disable_dictation_menu_item: bool,
pub disable_character_palette_menu_item: bool,
}
#[unsafe(no_mangle)]
pub extern "C" fn application_init(config: &ApplicationConfig, callbacks: ApplicationCallbacks) {
ffi_boundary("application_init", || {
info!("Application Init");
let mtm: MainThreadMarker = MainThreadMarker::new().unwrap();
// NSUserDefaults::resetStandardUserDefaults();
let user_defaults = NSUserDefaults::standardUserDefaults();
user_defaults.setBool_forKey(config.disable_dictation_menu_item, ns_string!("NSDisabledDictationMenuItem"));
user_defaults.setBool_forKey(
config.disable_character_palette_menu_item,
ns_string!("NSDisabledCharacterPaletteMenuItem"),
);
let app = MyNSApplication::sharedApplication(mtm);
// let default_presentation_options = app.presentationOptions();
// app.setPresentationOptions(default_presentation_options | NSApplicationPresentationOptions::NSApplicationPresentationFullScreen);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
let event_handler = callbacks.event_handler;
let app_delegate = AppDelegate::new(mtm, callbacks);
app.setDelegate(Some(ProtocolObject::from_ref(&*app_delegate)));
app.set_appearance_observer(&app_delegate);
APP_STATE.with(|app_state| {
app_state
.set(AppState {
app,
app_delegate,
event_handler,
drag_and_drop_handler_state: DragAndDropHandlerState::default(),
mtm,
})
.map_err(|_| anyhow!("Can't initialize second time!"))?;
Ok(())
})
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_get_appearance() -> Appearance {
ffi_boundary("application_get_appearance", || -> Result<Appearance, anyhow::Error> {
let mtm: MainThreadMarker = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
let appearance = app.effectiveAppearance();
Ok(Appearance::from_ns_appearance(&appearance))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_get_text_direction() -> TextDirection {
ffi_boundary("application_get_text_direction", || -> Result<TextDirection, anyhow::Error> {
let mtm: MainThreadMarker = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
let layout_direction = app.userInterfaceLayoutDirection();
Ok(TextDirection::from_ns_layout_direction(layout_direction))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_shutdown() {
ffi_boundary("application_shutdown", || {
// todo
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_run_event_loop() {
ffi_boundary("application_run_event_loop", || {
info!("Start event loop");
let mtm: MainThreadMarker = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.run();
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_stop_event_loop() {
ffi_boundary("application_stop_event_loop", || {
info!("Stop event loop");
let mtm: MainThreadMarker = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.stop(None);
// In case application_stop_event_loop is not called in response to a UI event, we need to trigger
// a dummy event so the UI processing loop picks up the stop request.
let dummy_event = NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
NSEventType::ApplicationDefined,
NSPoint::ZERO,
NSEventModifierFlags::empty(),
0f64,
0,
None,
0,
0,
0,
)
.unwrap();
app.postEvent_atStart(&dummy_event, true);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_request_termination() {
ffi_boundary("application_request_termination", || {
let mtm: MainThreadMarker = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.terminate(None);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_get_name() -> RustAllocatedStrPtr {
ffi_boundary("application_name", || {
match NSRunningApplication::currentApplication().localizedName() {
Some(name) => copy_to_c_string(&name),
None => Ok(RustAllocatedStrPtr::null()),
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_hide() {
ffi_boundary("application_hide", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.hide(None);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_hide_other_applications() {
ffi_boundary("application_hide_other_applications", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.hideOtherApplications(None);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_unhide_all_applications() {
ffi_boundary("application_unhide_all_applications", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.unhideAllApplications(None);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_is_active() -> bool {
ffi_boundary("application_is_active", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
let is_active = app.isActive();
Ok(is_active)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_activate_ignoring_other_apps() {
ffi_boundary("application_activate_ignoring_other_apps", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
#[allow(deprecated)]
app.activateIgnoringOtherApps(true);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_set_dock_icon(image: Image) {
ffi_boundary("application_set_dock_icon", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
let image = image.to_ns_image(mtm)?;
unsafe {
app.setApplicationIconImage(Some(&image));
}
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_set_dock_icon_badge(label: BorrowedStrPtr) {
ffi_boundary("application_set_dock_icon_badge", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
let label = copy_to_ns_string(&label)?;
app.dockTile().setBadgeLabel(Some(&label));
Ok(())
});
}
type AttentionRequestId = isize;
#[unsafe(no_mangle)]
pub extern "C" fn application_request_user_attention(is_critical: bool) -> AttentionRequestId {
ffi_boundary("application_set_dock_icon_badge", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
let attention_type = if is_critical {
NSRequestUserAttentionType::CriticalRequest
} else {
NSRequestUserAttentionType::InformationalRequest
};
Ok(app.requestUserAttention(attention_type))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_cancel_request_user_attention(request_id: AttentionRequestId) {
ffi_boundary("application_cancel_request_user_attention", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.cancelUserAttentionRequest(request_id);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_order_front_character_palete() {
ffi_boundary("application_order_front_character_palete", || {
let mtm = MainThreadMarker::new().unwrap();
let app = MyNSApplication::sharedApplication(mtm);
app.orderFrontCharacterPalette(None);
Ok(())
});
}
#[unsafe(no_mangle)]
pub extern "C" fn application_open_url(url: BorrowedStrPtr) -> bool {
ffi_boundary("application_open_url", || {
let url_string = copy_to_ns_string(&url)?;
let url = NSURL::URLWithString(&url_string).context("Can't create NSURL from string")?;
let was_opened = NSWorkspace::sharedWorkspace().openURL(&url);
Ok(was_opened)
})
}
#[allow(unused_doc_comments)]
/// cbindgen:ignore
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *const c_void;
// Note: TISGetInputSourceProperty returns a borrowed reference, NOT an owned one
pub(super) fn TISGetInputSourceProperty(inputSource: *const c_void, propertyKey: *const c_void) -> *const c_void;
pub(super) fn TISCreateInputSourceList(properties: *const c_void, include_all_installed: bool) -> *const c_void;
pub(super) fn TISSelectInputSource(inputSource: *const c_void) -> i32;
#[allow(dead_code)]
pub(super) static kTISPropertyUnicodeKeyLayoutData: *const c_void;
pub(super) static kTISPropertyInputSourceID: *const c_void;
#[allow(dead_code)]
pub(super) static kTISPropertyLocalizedName: *const c_void;
}
#[allow(unused_doc_comments)]
/// cbindgen:ignore
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFRelease(cf: *const c_void);
fn CFArrayGetCount(array: *const c_void) -> isize;
fn CFArrayGetValueAtIndex(array: *const c_void, index: isize) -> *const c_void;
}
#[unsafe(no_mangle)]
pub extern "C" fn application_current_input_source() -> RustAllocatedStrPtr {
ffi_boundary("application_current_input_source", || {
let _mtm = MainThreadMarker::new().unwrap();
unsafe {
let input_source = TISCopyCurrentKeyboardLayoutInputSource();
if input_source.is_null() {
log::warn!("Can't get current keyboard layout");
return Ok(RustAllocatedStrPtr::null());
}
let source_id_ptr = TISGetInputSourceProperty(input_source, kTISPropertyInputSourceID);
let result = if source_id_ptr.is_null() {
Ok(RustAllocatedStrPtr::null())
} else {
// source_id is a CFStringRef (borrowed), toll-free bridged to NSString
let ns_string: &NSString = &*source_id_ptr.cast::<NSString>();
copy_to_c_string(ns_string)
};
// Release the input source we got from TISCopyCurrentKeyboardLayoutInputSource
CFRelease(input_source);
result
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_list_input_sources() -> AutoDropArray<RustAllocatedStrPtr> {
ffi_boundary("application_list_input_sources", || {
unsafe {
let source_list = TISCreateInputSourceList(std::ptr::null(), false);
if source_list.is_null() {
return Ok(AutoDropArray::new(Box::new([])));
}
#[allow(clippy::cast_sign_loss)]
let count = CFArrayGetCount(source_list) as usize;
if count == 0 {
CFRelease(source_list);
return Ok(AutoDropArray::new(Box::new([])));
}
let mut source_ids: Vec<RustAllocatedStrPtr> = Vec::with_capacity(count);
for i in 0..count {
let input_source = CFArrayGetValueAtIndex(source_list, i as isize);
let source_id_ptr = TISGetInputSourceProperty(input_source, kTISPropertyInputSourceID);
if !source_id_ptr.is_null() {
// source_id is a CFStringRef (borrowed), toll-free bridged to NSString
let ns_string: &NSString = &*source_id_ptr.cast::<NSString>();
source_ids.push(copy_to_c_string(ns_string)?);
}
}
CFRelease(source_list);
Ok(AutoDropArray::new(source_ids.into_boxed_slice()))
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn application_choose_input_source(source_id: BorrowedStrPtr) -> bool {
ffi_boundary("application_choose_input_source", || {
let source_id_str = source_id.as_str()?;
unsafe {
let source_list = TISCreateInputSourceList(std::ptr::null(), true);
if source_list.is_null() {
return Ok(false);
}
#[allow(clippy::cast_sign_loss)]
let count = CFArrayGetCount(source_list) as usize;
let mut result = false;
for i in 0..count {
let input_source = CFArrayGetValueAtIndex(source_list, i as isize);
let prop_ptr = TISGetInputSourceProperty(input_source, kTISPropertyInputSourceID);
if !prop_ptr.is_null() {
let ns_string: &NSString = &*prop_ptr.cast::<NSString>();
if ns_string.to_string() == source_id_str {
let status = TISSelectInputSource(input_source);
result = status == 0; // noErr
break;
}
}
}
CFRelease(source_list);
Ok(result)
}
})
}
define_class!(
#[unsafe(super(NSApplication))]
#[name = "MyNSApplication"]
#[ivars = ()]
#[derive(Debug)]
pub(crate) struct MyNSApplication;
unsafe impl NSObjectProtocol for MyNSApplication {}
impl MyNSApplication {
#[unsafe(method(sendEvent:))]
fn send_event(&self, event: &NSEvent) {
catch_panic(|| {
self.send_event_impl(event);
Ok(())
});
}
}
);
impl MyNSApplication {
#[allow(non_snake_case)]
pub(crate) fn sharedApplication(_mtm: MainThreadMarker) -> Retained<Self> {
unsafe { msg_send!(Self::class(), sharedApplication) }
}
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
// The default sendEvent turns key downs into performKeyEquivalent when
// modifiers are down, but swallows the key up if the modifiers include
// command. This one makes all modifiers consistent by always sending key ups.
fn send_event_impl(&self, event: &NSEvent) {
if event.r#type() == NSEventType::KeyUp {
let mtm: MainThreadMarker = self.mtm();
if let Some(window) = event.window(mtm) {
window.sendEvent(event);
return;
}
}
let _: () = unsafe { msg_send![super(self), sendEvent: event] };
}
fn set_appearance_observer(&self, delegate: &NSObject) {
unsafe {
self.addObserver_forKeyPath_options_context(
delegate,
ns_string!("effectiveAppearance"),
NSKeyValueObservingOptions::New,
std::ptr::null_mut(),
);
}
}
}
#[derive(Debug)]
struct AppDelegateIvars {
callbacks: ApplicationCallbacks,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "AppDelegate"]
#[ivars = AppDelegateIvars]
#[derive(Debug)]
struct AppDelegate;
impl AppDelegate {
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn observe_value(
&self,
key_path: Option<&NSString>,
object: Option<&AnyObject>,
change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
context: *mut c_void,
) {
catch_panic(|| {
match (object, key_path) {
(Some(object), Some(key_path))
if object.class().superclass() == Some(MyNSApplication::class())
&& key_path == ns_string!("effectiveAppearance") =>
{
handle_application_appearance_change();
}
_ => unsafe {
let _: () = msg_send![super(self), observeValueForKeyPath: key_path,
ofObject: object,
change: change,
context: context];
},
}
Ok(())
});
}
#[unsafe(method(itemCallback:))]
fn item_callback(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
// We need this strange layer of delegation to give MacOS a chance to handle
// application menu events before we handle them ourselves.
// For example, copy, paste and others wouldn't work in open dialogs without this.
#[unsafe(method(undo:))]
fn undo(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
#[unsafe(method(redo:))]
fn redo(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
#[unsafe(method(cut:))]
fn cut(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
#[unsafe(method(copy:))]
fn copy(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
#[unsafe(method(paste:))]
fn paste(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
#[unsafe(method(selectAll:))]
fn select_all(&self, sender: &NSMenuItem) {
handle_app_menu_callback(sender.tag() as ItemId);
}
}
unsafe impl NSObjectProtocol for AppDelegate {}
unsafe impl NSApplicationDelegate for AppDelegate {
#[unsafe(method(applicationDidChangeScreenParameters:))]
fn did_change_screen_parameters(&self, _notification: &NSNotification) {
catch_panic(|| {
handle_display_configuration_change();
Ok(())
});
}
#[unsafe(method(application:openURLs:))]
unsafe fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
catch_panic(|| {
handle_application_open_urls(urls);
Ok(())
});
}
#[unsafe(method(applicationWillFinishLaunching:))]
fn application_will_finish_launching(&self, _notification: &NSNotification) {
catch_panic(|| {
set_initial_app_menu();
Ok(())
});
}
#[unsafe(method(applicationDidFinishLaunching:))]
fn did_finish_launching(&self, _notification: &NSNotification) {
catch_panic(|| {
handle_application_did_finish_launching();
Ok(())
});
}
#[unsafe(method(applicationShouldTerminate:))]
fn should_terminate(&self, _sender: &NSApplication) -> NSApplicationTerminateReply {
let result = (self.ivars().callbacks.on_should_terminate)();
return if result {
NSApplicationTerminateReply::TerminateNow
} else {
NSApplicationTerminateReply::TerminateCancel
};
}
#[unsafe(method(applicationWillTerminate:))]
fn will_terminate(&self, _notification: &NSNotification) {
(self.ivars().callbacks.on_will_terminate)();
}
}
);
impl AppDelegate {
fn new(mtm: MainThreadMarker, callbacks: ApplicationCallbacks) -> Retained<Self> {
let this = mtm.alloc();
let this = this.set_ivars(AppDelegateIvars { callbacks });
unsafe { msg_send![super(this), init] }
}
}