Merge pull request #164 from LloydNicholson/macos-open-documents

macOS: open documents from Finder, the Dock and `open -a` (#69)

# Conflicts:
#	apps/gridcraft/src/main.rs
This commit is contained in:
Brandon Thomas 2026-10-10 15:50:57 -04:00
commit 502c588690
4 changed files with 78 additions and 1 deletions

12
Cargo.lock generated
View File

@ -1139,6 +1139,17 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "fmv-macos-events"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90642cf66dde7f205dc8b42dfe57b47d0f3a29f571b832742883ef512e23fe55"
dependencies = [
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
]
[[package]]
name = "foldhash"
version = "0.1.5"
@ -1410,6 +1421,7 @@ dependencies = [
"arboard",
"eframe",
"egui",
"fmv-macos-events",
"gridcraft-engine",
"gridcraft-ui-egui",
"image",

View File

@ -32,3 +32,8 @@ arboard = { workspace = true, features = ["wayland-data-control"] }
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21", default-features = false }
# Finder double-click, Open With, Dock drops and `open -a GridCraft file.xlsx` arrive as a
# kAEOpenDocuments Apple event, which winit 0.30 doesn't handle (#69). This audited crate (also used
# by PdfCraft and PhotoCraft) registers the handler (Objective-C interop needs unsafe, which our
# workspace forbids) behind a safe API. Pinned exactly: review the diff before bumping.
fmv-macos-events = "=0.1.0"

View File

@ -0,0 +1,49 @@
//! macOS open-documents and quit Apple events (#69).
//!
//! Finder double-clicks, Open With, drops on the Dock icon and `open -a GridCraft book.xlsx` don't
//! pass paths on the command line: LaunchServices sends the running (or just-launched) app a
//! `kAEOpenDocuments` ('odoc') Apple event. winit 0.30 doesn't handle it and owns the
//! `NSApplicationDelegate`, so AppKit answered "GridCraft cannot open files in the Excel Workbook
//! format". Handling it ourselves needs Objective-C class declarations, i.e. `unsafe`, which this
//! workspace forbids. The audited `fmv-macos-events` crate (also used by PdfCraft and PhotoCraft)
//! wraps exactly that, an `NSAppleEventManager` handler registered before Finder's launch event
//! that leaves winit's delegate alone, behind a safe main-thread API.
use fmv_macos_events::{Event, Inbox, Registration};
use gridcraft_ui_egui::SheetApp;
/// Keeps the Apple-event handlers registered; hold it until the event loop returns.
pub struct AppleEvents {
_registration: Registration,
inbox: Inbox,
}
impl AppleEvents {
/// Register the handlers. Call on the main thread before the event loop starts, so the event
/// that launched the app (a Finder double-click) is caught too.
pub fn install() -> Self {
let (registration, inbox) = Registration::install();
Self { _registration: registration, inbox }
}
/// The queue the app drains every frame ([`poll`]); events arriving later wake `ctx`.
pub fn connect(&self, ctx: &egui::Context) -> Inbox {
let ctx = ctx.clone();
self.inbox.set_wake(move || ctx.request_repaint());
self.inbox.clone()
}
}
/// Open the documents and act on the quit requests that arrived since the last frame.
pub fn poll(inbox: &Inbox, app: &mut SheetApp, ctx: &egui::Context) {
for e in inbox.drain() {
match e {
Event::Open(paths) => {
for p in paths.iter().filter_map(|p| p.to_str()) {
app.open_path(p);
}
}
Event::Quit => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
}
}
}

View File

@ -8,6 +8,8 @@
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(target_os = "macos")]
mod apple_events;
mod control_server;
#[cfg(any(target_os = "windows", test))]
mod graphics;
@ -19,7 +21,7 @@ use gridcraft_engine::Session;
use gridcraft_ui_egui::{Services, SheetApp};
use serde_json::json;
struct App(SheetApp, #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>);
struct App(SheetApp, #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>, #[cfg(target_os = "macos")] fmv_macos_events::Inbox);
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
@ -31,6 +33,7 @@ impl eframe::App for App {
if let Some(m) = &self.1 {
m.poll(&mut self.0, ctx);
}
apple_events::poll(&self.2, &mut self.0, ctx);
}
self.0.logic(ctx);
}
@ -264,6 +267,12 @@ fn main() -> eframe::Result<()> {
graphics::configure(&mut options, eframe::wgpu::Backends::from_env());
options
};
// Registered before the event loop starts, so it catches the Finder event that launched us as
// well as later ones. Lives until the event loop returns; the app creator only borrows it.
#[cfg(target_os = "macos")]
let apple_events = apple_events::AppleEvents::install();
#[cfg(target_os = "macos")]
let apple_events = &apple_events;
eframe::run_native(
"GridCraft",
options,
@ -279,6 +288,8 @@ fn main() -> eframe::Result<()> {
app,
#[cfg(target_os = "macos")]
None,
#[cfg(target_os = "macos")]
apple_events.connect(&cc.egui_ctx),
)))
}),
)