mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-07 12:13:20 +00:00
* Add craft-menu: Mac menu-bar layout and native-menu driver A platform-free menu model (built directly or from flat rows), the macOS layout (app menu, Window, Help, system-key clash resolution, translatable labels), and NativeMenu, which feeds a backend's key equivalents to egui as key presses, runs Minimize and craft-quit's Quit, and re-syncs only when the app's state changes. Generalised from PhotoCraft's native_menu.rs. * craft-menu: macOS menu bar through muda * craft-menu: clipboard keys, layout-assigned shortcuts, Hide Others and atomic rebuilds * craft-menu: menu key equivalents run while a text field has the keys * craft-menu: commands as CommandRef, item kinds, craft-keymap chords Items and rows run a craft_keymap::CommandRef and take a craft_keymap::Chord shortcut; crates/menu/src/chord.rs is gone. Native ids are the command's key, made unique with #2, #3 when a command is shown twice. Item.checked becomes ItemKind { Plain, Check, Radio } (radio shows as a check item on macOS). The built-in items' commands are Builtin::{Hide, HideOthers, Minimize, Quit}. MenuRole gains Edit, View and Other; tag_submenu finds its menu by role. While text editing, the keys left to the field are the clipboard keys and craft_keymap::text_editing_chords. Adds the Platforms, Document and Gate types (data only) and MenuBar::documents. * craft-menu: Gate::allows, the one rule for what may run from the menus * craft-menu: Windows, Linux and web layouts, documents in Window, keymap adapter layout(bar, Target, options) dispatches to mac_layout, desktop_layout or web_layout. Every layout drops what the target's Platforms leave out and lists MenuBar::documents as radio items last in the Window menu (ItemRole::Document is gone). Windows and Linux put About in Help, Settings in Edit as Preferences and Quit in File (Exit on Windows); the web layout has no Quit and takes the browser's own shortcuts off the items, reporting clashes. tidy drops empty submenus. apply_keymap sets shortcuts and toggle check items from a craft-keymap Keymap; apply_state sets enabled and checked per frame. * craft-menu: the egui-drawn menu bar for Windows, Linux and the web * craft-menu: gate, command hook and one muda handler for the native menu - NativeMenu::set_gate: app items greyed in place while modal or recording a shortcut; take_clicks drops clicks the gate forbids when they are taken. While recording, key equivalents reach egui as plain key presses. - NativeMenu::on_command: a hook called from the menu event handler for each allowed app click; take_clicks still returns every command. - Dispatch, shared between the driver and the backend's handler (Backend::attach replaces Backend::set_text_editing). - Only Cmd or Ctrl chords become key equivalents; other shortcuts are not shown natively (building an attributed title needs unsafe objc2 calls). - mac: one muda event handler per process, routed to the latest install; mac::install_app_menu ports craft-quit's minimal app menu onto it. - craft_quit::install_app_menu is deprecated in favor of craft_menu::mac::install_app_menu. * craft-menu: the bar takes Target, README section and NOTICE credit * craft-menu: bar tests beside the layout and native ones; only a click outside closes the menus * craft-menu: clippy (complex types, Copy clones, elided lifetimes) * craft-menu: keyboard access to the egui bar A tap on Alt alone or F10 focuses the bar, Alt+letter opens a menu and a letter in an open menu runs or opens the row with that mnemonic, as in a Windows menu bar. Mnemonics are assigned per level from the shown labels, underlined only while the keyboard drives the bar, and a label without an ASCII letter shows its English letter after it ("文件(F)") through the optional english hook. Preferred letters come from MenuBarUi::mnemonics. A Mac target has none, and a dialog or the shortcut recorder keeps the keys. Ported from PhotoCraft's menu_keys.rs and menu_keys_tests.rs. * craft-menu: one installer or the other; sort mnemonics before checking uniqueness * craft-menu: egui 0.37 * craft-menu: text fields keep their Edit items; web names by the browser's system; sync per frame; stable check kinds; handled hook clicks * craft-menu: sync reports a clash change on an unchanged bar
106 lines
5.2 KiB
Rust
106 lines
5.2 KiB
Rust
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
|
|
//! Quit a Craft app the platform's own way.
|
|
//!
|
|
//! On macOS an eframe app must not leave by closing its window. eframe drops the window while
|
|
//! AppKit's run loop is still running, and in the next display cycle AppKit's Touch Bar finder
|
|
//! tries to remove a KVO observer from the view that is gone
|
|
//! (`-[_NSTouchBarFinderObservation invalidate]`). That is an uncaught Objective-C exception and
|
|
//! the app crashes (seen on macOS 27 on a Touch Bar Mac).
|
|
//! `-[NSApplication terminate:]`, what ⌘Q does in winit's default menu, ends the process before
|
|
//! another display cycle runs.
|
|
//!
|
|
//! [`quit`] is that quit. With the `egui` feature, [`on_close_request`] routes a window close
|
|
//! through it. Put both the app's close decision and this hook in `eframe::App::logic`, with the
|
|
//! hook last. eframe calls `logic` even while the window is minimized or hidden, when `ui` does
|
|
//! not run. Move any unsaved-changes check out of `ui` too: `logic` runs before `ui`, so a check
|
|
//! left in `ui` would happen after the quit. Keep prompt state in the app and render it in `ui`:
|
|
//!
|
|
//! ```ignore
|
|
//! fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
//! if ctx.input(|i| i.viewport().close_requested())
|
|
//! && self.has_unsaved_changes()
|
|
//! && !self.discard_confirmed
|
|
//! {
|
|
//! ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
|
//! self.show_save_prompt = true;
|
|
//! }
|
|
//! craft_quit::on_close_request(ctx); // last, so it sees the app's `CancelClose`
|
|
//! }
|
|
//!
|
|
//! fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
|
//! // ... the app's UI, including the prompt when show_save_prompt is true ...
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! The prompt sends `ViewportCommand::Close` only after Save has succeeded (and cleared the
|
|
//! dirty state), or Don't Save has set `discard_confirmed`. Cancel, a cancelled file picker and
|
|
//! a failed save keep the app open; reset prompt/confirmation state when the decision is cancelled
|
|
//! or a new document is opened. An asynchronous save must finish before requesting Close. If the
|
|
//! window is hidden, bring it back before showing the prompt so the user can answer it.
|
|
//!
|
|
//! The app still saves what it keeps in `eframe::App::on_exit`: winit's
|
|
//! `applicationWillTerminate:` runs eframe's exit handling before the process ends. The process
|
|
//! ends there, so code after `eframe::run_native` in `main` does not run; move it to `on_exit`.
|
|
|
|
#[cfg(feature = "egui")]
|
|
mod close;
|
|
#[cfg(target_os = "macos")]
|
|
mod macos;
|
|
|
|
#[cfg(all(feature = "menu", target_os = "macos"))]
|
|
mod menu;
|
|
|
|
#[cfg(feature = "egui")]
|
|
pub use close::{QUIT_MENU_ID, on_close_request, on_menu_event};
|
|
|
|
/// Replace winit's default macOS app menu with one whose Quit (⌘Q) asks the app first.
|
|
///
|
|
/// winit's Quit ends the process without asking, so unsaved work is lost. This menu's Quit sends
|
|
/// the root window a `ViewportCommand::Close` instead: the app's unsaved-changes check runs as it
|
|
/// does for the close button, and [`on_close_request`] then quits. Call it once, from the eframe
|
|
/// app creator: winit builds its default menu before that and would replace one installed
|
|
/// earlier. `app_name` is shown in the menu ("Quit WordCraft").
|
|
///
|
|
/// For apps without an app menu of their own. It replaces the whole menu and sets muda's
|
|
/// process-wide menu-event handler, so an app that builds its own muda menu should not call it:
|
|
/// give that menu's Quit item [`QUIT_MENU_ID`] and pass its events to [`on_menu_event`] instead.
|
|
/// muda keeps the first handler set, so this and `craft_menu::mac::install` are alternatives: an
|
|
/// app calls one of them. `craft_menu::mac::install_app_menu` builds the same menu on craft-menu's
|
|
/// menu event handler, which its full menu bar shares.
|
|
///
|
|
/// On error winit's menu stays: the app still runs, but ⌘Q quits without asking. The app decides
|
|
/// whether that is fatal; `?` passes it out of the eframe app creator.
|
|
///
|
|
/// Other platforms: does nothing and returns `Ok`.
|
|
#[cfg(feature = "menu")]
|
|
#[deprecated(note = "use craft_menu::mac::install_app_menu")]
|
|
pub fn install_app_menu(ctx: &egui::Context, app_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
#[cfg(target_os = "macos")]
|
|
menu::install(ctx, app_name)?;
|
|
#[cfg(not(target_os = "macos"))]
|
|
let _ = (ctx, app_name);
|
|
Ok(())
|
|
}
|
|
|
|
/// Quit through the platform's own path, where leaving by closing the window is not safe.
|
|
///
|
|
/// macOS: schedules `-[NSApplication terminate:]` and returns `true`; the caller keeps its window
|
|
/// open and the process ends shortly after, once the current event has been handled. Must be
|
|
/// called while the app's event loop is running (any eframe callback is). Return to the event loop
|
|
/// before blocking: a blocking native dialog opened in the same callback would run the quit while
|
|
/// winit is still handling the event, which panics.
|
|
///
|
|
/// Other platforms: does nothing and returns `false`; the caller closes its window as usual.
|
|
pub fn quit() -> bool {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
macos::terminate_later();
|
|
true
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
false
|
|
}
|
|
}
|