Files
ElyOffice/crates/ely-web-content/src/mac.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

106 lines
3.1 KiB
Rust

//! Chromium's app protocol on GPUI's own `NSApplication` class.
use std::{cell::Cell, ffi::CStr};
use anyhow::{Result, bail};
use objc2::{
ffi,
runtime::{AnyClass, AnyObject, Bool, Imp, Sel},
sel,
};
thread_local! {
/// NSApp lives on the main thread; one flag serves it.
static SENDING: Cell<bool> = const { Cell::new(false) };
}
extern "C-unwind" fn is_handling(_this: *mut AnyObject, _cmd: Sel) -> Bool {
Bool::new(SENDING.get())
}
extern "C-unwind" fn set_handling(_this: *mut AnyObject, _cmd: Sel, on: Bool) {
SENDING.set(on.as_bool());
}
extern "C-unwind" fn send_event(this: *mut AnyObject, _cmd: Sel, event: *mut AnyObject) {
let was = SENDING.replace(true);
let base = AnyClass::get(c"NSApplication").expect("AppKit has NSApplication");
unsafe {
let _: () = objc2::msg_send![super(&*this, base), sendEvent: event];
}
SENDING.set(was);
}
fn add(class: &AnyClass, name: Sel, imp: Imp, types: &CStr) -> Result<()> {
let added = unsafe {
ffi::class_addMethod(
class as *const AnyClass as *mut AnyClass,
name,
imp,
types.as_ptr(),
)
};
if !added.as_bool() {
bail!("{} already has -{name}", class.name().to_string_lossy());
}
Ok(())
}
/// Gives NSApp's class what CEF checks before it runs.
pub fn adopt_app_protocol() -> Result<()> {
let Some(app) = (unsafe { objc2_app_kit_nsapp() }) else {
bail!("no NSApp yet");
};
let class = unsafe { &*ffi::object_getClass(app.cast()) };
if class.responds_to(sel!(isHandlingSendEvent)) {
return Ok(());
}
unsafe {
add(
class,
sel!(isHandlingSendEvent),
std::mem::transmute(is_handling as *const ()),
c"c@:",
)?;
add(
class,
sel!(setHandlingSendEvent:),
std::mem::transmute(set_handling as *const ()),
c"v@:c",
)?;
add(
class,
sel!(sendEvent:),
std::mem::transmute(send_event as *const ()),
c"v@:@",
)?;
}
// Chromium checks the two `Cr` protocols; CEF's own is header-only.
for (name, needed) in [
(c"CrAppProtocol", true),
(c"CrAppControlProtocol", true),
(c"CefAppProtocol", false),
] {
let protocol = unsafe { ffi::objc_getProtocol(name.as_ptr()) };
match (protocol.is_null(), needed) {
(true, true) => bail!("CEF did not register {}", name.to_string_lossy()),
(true, false) => log::info!("web: {} is not registered", name.to_string_lossy()),
(false, _) => unsafe {
ffi::class_addProtocol(class as *const AnyClass as *mut AnyClass, protocol);
},
}
}
log::info!(
"web: {} takes CefAppProtocol",
class.name().to_string_lossy()
);
Ok(())
}
/// The shared application, if made.
unsafe fn objc2_app_kit_nsapp() -> Option<*mut AnyObject> {
let class = AnyClass::get(c"NSApplication")?;
let app: *mut AnyObject = unsafe { objc2::msg_send![class, sharedApplication] };
(!app.is_null()).then_some(app)
}