mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-02 08:43:57 +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
101 lines
6.7 KiB
Rust
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;
|