fix(macos): Cmd+Q asks about unsaved changes (#1136) (#1533)

* fix(macos): Cmd+Q asks about unsaved changes (#1136)

winit's default app menu sends Quit (Cmd+Q) straight to
-[NSApplication terminate:], so PdfCraft quit without asking and
unsaved edits were lost; the close button and Dock > Quit already ask.

Install craft-quit's app menu (storytold/craft-libs#21) from the app
creator. Its Quit sends ViewportCommand::Close, which guard_quit
handles like the close button. If the menu can't be built, winit's
menu stays and a warning is logged.

craft-quit is pinned to the head of craft-libs#21 until #5 and #21
are merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(macos): closing the window quits through terminate: (#1136)

On Touch Bar Macs the close button crashed PdfCraft (exit 133,
-[_NSTouchBarFinderObservation invalidate]): eframe drops the window
inside AppKit's run loop. craft_quit::on_close_request, right after
guard_quit in `logic`, cancels an allowed close and quits through
-[NSApplication terminate:] instead. A close that guard_quit holds for
the unsaved-changes prompt is left alone. Elsewhere it does nothing.

craft-quit moves to the workspace dependencies, pinned to craft-libs
main (f58bc1a, with #5 and #21 merged) instead of #21's head.

The on_close_request change and its Touch Bar test are from jmacek.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Mahmud Emre BAŞPEHLİVAN
2026-10-15 03:15:34 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 7461c29791
commit 41809cf765
6 changed files with 65 additions and 0 deletions
Generated
+51
View File
@@ -1154,6 +1154,18 @@ dependencies = [
"winsafe",
]
[[package]]
name = "craft-quit"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=f58bc1a3df490992d633393d0898af0588ce882c#f58bc1a3df490992d633393d0898af0588ce882c"
dependencies = [
"dispatch2",
"egui",
"muda",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
]
[[package]]
name = "craft-ui"
version = "0.1.0"
@@ -1184,6 +1196,15 @@ dependencies = [
"x509-cert",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
@@ -3005,6 +3026,16 @@ dependencies = [
"cpufeatures 0.3.1",
]
[[package]]
name = "keyboard-types"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
dependencies = [
"bitflags 2.13.2",
"serde",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -3248,6 +3279,24 @@ dependencies = [
"pxfm",
]
[[package]]
name = "muda"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
dependencies = [
"crossbeam-channel",
"dpi",
"keyboard-types",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"png 0.18.1",
"thiserror 2.0.21",
"windows-sys 0.60.2",
]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
@@ -3877,6 +3926,7 @@ dependencies = [
name = "pdfcraft"
version = "0.8.0"
dependencies = [
"craft-quit",
"eframe",
"egui",
"fmv-macos-events",
@@ -4291,6 +4341,7 @@ dependencies = [
"arboard",
"base64 0.22.1",
"bytemuck",
"craft-quit",
"craft-ui",
"eframe",
"egui",
+2
View File
@@ -14,6 +14,8 @@ repository = "https://github.com/storytold/pdfcraft"
[workspace.dependencies]
# Shared frontend components; engines remain independent of egui.
craft-ui = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
# Quit the macOS way (#1136): ⌘Q asks about unsaved changes, and closing quits through `terminate:`.
craft-quit = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "f58bc1a3df490992d633393d0898af0588ce882c" }
# Shared system spooler; document composition remains in pdfcraft-print.
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "666c358065261a2816b376fa3bb10538dbf53040" }
# internal
+3
View File
@@ -29,6 +29,9 @@ log = { workspace = true }
# crate registers the handler (Objective-C interop needs unsafe, which our workspace forbids)
# behind a safe API. Same version as PhotoCraft. Pinned exactly: review the diff before bumping.
fmv-macos-events = "=0.1.0"
# ⌘Q asks about unsaved changes like the close button (#1136): an app menu whose Quit asks the
# window to close instead of winit's, which ends the process without asking.
craft-quit = { workspace = true, features = ["menu"] }
[target.'cfg(target_os = "linux")'.dependencies]
# Asks the Wayland compositor which GPU it draws with (linux-dmabuf feedback), so the window is
+5
View File
@@ -379,6 +379,11 @@ fn app_creator<'a>(
#[cfg(target_os = "macos")]
{
app.os_events = Some(apple_events.connect(&cc.egui_ctx));
// winit's Quit (⌘Q) ends the process without asking about unsaved changes (#1136). This
// menu's Quit closes the window instead, so `guard_quit` asks, as for the close button.
if let Err(e) = craft_quit::install_app_menu(&cc.egui_ctx, "PdfCraft") {
log::warn!("couldn't install the app menu, so ⌘Q quits without asking to save: {e}");
}
}
if let Some(server) = instance {
app.os_events = Some(server.connect(&cc.egui_ctx, native_wayland(cc)));
+2
View File
@@ -11,6 +11,8 @@ workspace = true
[dependencies]
craft-ui = { workspace = true }
# macOS: a window close quits through `terminate:` (the Touch Bar crash, #1136); elsewhere a no-op.
craft-quit = { workspace = true, features = ["egui"] }
pdfcraft-fonts = { workspace = true }
pdfcraft-engine = { workspace = true }
pdfcraft-cos = { workspace = true }
+2
View File
@@ -3013,6 +3013,8 @@ impl eframe::App for PdfCraftApp {
self.control = Some(control);
}
self.guard_quit(ctx);
// macOS: quit through `terminate:` instead of dropping the window (Touch Bar crash, #1136).
craft_quit::on_close_request(ctx);
let now = ctx.input(|i| i.time);
self.autosave_tick(now);
// Reload documents whose files a build rewrote (#431).