Files
craft-libs/crates/menu/src/lib.rs
T
aiongg 9f89fe8f6d Add craft-menu: one menu definition, native on macOS and an egui bar on Windows, Linux and web (#66)
* 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
2026-10-25 21:59:22 -04:00

101 lines
6.7 KiB
Rust

#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
//! A Craft app's menus, laid out for macOS, Windows, Linux and the web.
//!
//! The app supplies its menus as a tree ([`MenuBar`]), built directly or from the flat rows of
//! its in-window bar ([`from_items`]), gives each top-level menu its role ([`MenuRole`]), and
//! tags the items the layouts put in fixed places ([`MenuBar::tag_item`], [`MenuBar::tag_submenu`]).
//! Each item runs a [`craft_keymap::CommandRef`], its shortcut is a [`craft_keymap::Chord`], and
//! its [`ItemKind`] makes it plain, a check item or a radio item. [`layout`]
//! arranges them the way the platform's apps do, and [`NativeMenu`] hands the result to a platform
//! [`Backend`] and brings its events back. This part is pure and platform-free, so it is tested
//! everywhere and builds for the web; only the backend, `mac` (macOS only, built on muda), touches
//! AppKit. Its `mac::install` builds the menu bar and returns the [`NativeMenu`].
//!
//! **Layouts.** [`layout`] takes a [`Target`] ([`Target::current`] for this build) and calls
//! [`mac_layout`], [`desktop_layout`] (Windows and Linux) or [`web_layout`]. Each first drops the
//! items and submenus whose [`Platforms`] leave the target out, and every one lists
//! [`MenuBar::documents`] (but the hidden ones) last in the Window menu, as radio items with the
//! active document selected. On Windows and Linux About goes last in Help, Settings last in Edit
//! as Preferences, and Quit last in File, as Exit on Windows; the web layout has no Quit and takes
//! the browser's own shortcuts (Ctrl+W, Ctrl+T, …) off the items, reporting each as a [`Clash`].
//! A layout adds the menus it needs when the app has none. A submenu left empty goes; a dynamic
//! list with nothing in it shows the disabled placeholder item the app supplies.
//!
//! **Keymap.** [`apply_keymap`] sets the items' shortcuts from the app's
//! [`craft_keymap::Keymap`], user overrides included, and makes toggle commands check items;
//! [`apply_state`] sets enabled and checked per frame from an [`ItemState`] the app returns for
//! each command. Both run on the app's bar, before the layout.
//!
//! What the Mac layout does:
//! - an **app menu** named after the app: About (the item tagged [`ItemRole::About`]), Settings
//! ([`ItemRole::Settings`], an item or a submenu of pages), Language ([`ItemRole::Language`]),
//! Appearance ([`ItemRole::Appearance`]), Services, Hide ([`Builtin::Hide`]), Hide Others
//! ([`Builtin::HideOthers`]), Show All and Quit. Quit is the app's own item tagged
//! [`ItemRole::Quit`], so its unsaved-changes prompt runs; without one the layout adds
//! [`Builtin::Quit`], which asks the window to close. These items leave the menus they came from. A menu the app
//! tagged [`MenuRole::App`] is merged into this one;
//! - **Window** ([`MenuRole::Window`], added when the app has none) gets Minimize, Zoom and Bring
//! All to Front, with the open documents ([`MenuBar::documents`]) after them, where a Mac app
//! lists its windows. **Help** ([`MenuRole::Help`]) gets the system search field from the
//! backend. **File** ([`MenuRole::File`]) gets Close Window unless the app binds ⌘W;
//! - **clashes** with the system's keys (⌘H, ⌥⌘H, ⌘M, ⌘W, ⌘,, ⌘Q) are resolved in the app's
//! favour and reported ([`Clash`]); Hide and Minimize move to ⌃⌘H and ⌃⌘M, as Photoshop does,
//! the others go without a shortcut. Shortcuts the app binds outside its menus count too
//! ([`LayoutOptions::extra_shortcuts`]).
//!
//! The labels the layout writes itself are English unless the app passes its translations in
//! [`LayoutOptions::label`].
//!
//! **Keys stay with egui.** AppKit runs a menu's key equivalents before the window sees the key,
//! which would skip the app's own key rules (a focused text field keeps ⌘C, ⌘V and ⌘Z, dialogs
//! are modal, a tool may have its own ⌘Z). So the backend reports a key equivalent as
//! [`Event::Key`], and [`NativeMenu::raw_input`] feeds it to egui as the input egui-winit would
//! have made of it: a key press, or for ⌘C, ⌘X and ⌘V egui's copy, cut and paste events
//! ([`ClipboardKeys`]). The shortcut runs through the app's key handling as without a native
//! menu. A click is [`Event::Click`], and [`NativeMenu::take_clicks`] returns its command for the app
//! to run like an in-window menu click; so is a key equivalent the layout assigned (⌘Q on the
//! app's Quit item, ⌘, on Settings), which the app's key handling doesn't know, and, while a text
//! field has the keys ([`Gate::text_editing`]), every key equivalent but the field's own
//! (⌘C, ⌘X, ⌘V and [`craft_keymap::text_editing_chords`]): an app's key handling skips shortcuts
//! while text has the keys, and a Mac app's menu commands (⌘S, ⌘N) run while typing. Only
//! shortcuts with ⌘ or ⌃ become key equivalents; tool keys (`B`, `⇧B`, `F5`) stay with the app.
//!
//! What may run from the menus is the app's [`Gate`], set every frame ([`NativeMenu::set_gate`]):
//! while a modal dialog is open or a shortcut is being recorded, the app's items are greyed (but
//! the text fields' own Undo, Copy, …) and their clicks dropped, also clicks that arrived before
//! the gate closed. While a shortcut is
//! recorded, key equivalents reach egui as plain key presses.
//!
//! craft-menu owns muda's one menu event handler: `mac::install` (a full menu bar) and
//! `mac::install_app_menu` (a minimal app menu, for apps without a [`MenuBar`]) both use it.
//! [`NativeMenu::on_command`] sees each allowed click as it arrives, also while egui is idle.
//!
//! Per frame: [`NativeMenu::raw_input`] before egui reads the input (`eframe::App::raw_input_hook`),
//! then [`NativeMenu::set_gate`], [`NativeMenu::take_clicks`] and run the commands, then
//! [`NativeMenu::sync_with`], which lays the menus out again only when the app's state changed
//! (or [`NativeMenu::sync`], which compares a layout built every frame).
//! [`NativeMenu::raw_input`] also keeps a click or Escape that closes an open native menu away
//! from egui.
pub mod bar;
mod driver;
mod keymap;
mod layout;
mod model;
#[cfg(target_os = "macos")]
pub mod mac;
pub use bar::{BarStyle, Labeled, MenuBarUi};
pub use craft_keymap::{Chord, CommandRef};
pub use driver::{Backend, ClipboardKeys, CommandHook, Dispatch, Event, Gate, NativeMenu, had_input};
pub use keymap::{ItemState, apply_keymap, apply_state};
pub use layout::{Clash, Layout, LayoutOptions, Target, desktop_layout, layout, mac_layout, web_layout};
pub use model::{
Builtin, Document, Item, ItemKind, ItemRole, Menu, MenuBar, MenuRole, Node, Platforms, Row, Standard, from_items, structure_key, tidy, window_key,
};
#[cfg(test)]
mod tests;