diff --git a/Cargo.lock b/Cargo.lock index 6ff01cc..5dce477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/apps/gridcraft/Cargo.toml b/apps/gridcraft/Cargo.toml index 66711fa..5cf5e07 100644 --- a/apps/gridcraft/Cargo.toml +++ b/apps/gridcraft/Cargo.toml @@ -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" diff --git a/apps/gridcraft/src/apple_events.rs b/apps/gridcraft/src/apple_events.rs new file mode 100644 index 0000000..405ee89 --- /dev/null +++ b/apps/gridcraft/src/apple_events.rs @@ -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), + } + } +} diff --git a/apps/gridcraft/src/main.rs b/apps/gridcraft/src/main.rs index 3feb62c..3c73a95 100644 --- a/apps/gridcraft/src/main.rs +++ b/apps/gridcraft/src/main.rs @@ -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); +struct App(SheetApp, #[cfg(target_os = "macos")] Option, #[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), ))) }), )