mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 18:13:14 +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
562 lines
18 KiB
Rust
562 lines
18 KiB
Rust
//! The menu tree, the flat rows it can be built from, and the keys that tell a backend what
|
|
//! changed.
|
|
|
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
use std::ops::BitOr;
|
|
|
|
use craft_keymap::{Chord, CommandRef};
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct MenuBar {
|
|
pub menus: Vec<Menu>,
|
|
/// The app's open documents, for the Window menu.
|
|
pub documents: Vec<Document>,
|
|
}
|
|
|
|
/// An open document, listed in the Window menu.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Document {
|
|
/// What choosing it runs (bring the document to the front).
|
|
pub command: CommandRef,
|
|
/// The document's name.
|
|
pub label: String,
|
|
/// The document in front.
|
|
pub active: bool,
|
|
/// Open but not listed: a document the app keeps open in the background.
|
|
pub hidden: bool,
|
|
}
|
|
|
|
impl Document {
|
|
/// A listed document, not the one in front.
|
|
pub fn new(command: impl Into<CommandRef>, label: impl Into<String>) -> Document {
|
|
Document { command: command.into(), label: label.into(), active: false, hidden: false }
|
|
}
|
|
|
|
pub fn active(mut self, active: bool) -> Document {
|
|
self.active = active;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Menu {
|
|
pub title: String,
|
|
pub role: MenuRole,
|
|
pub children: Vec<Node>,
|
|
}
|
|
|
|
/// What a top-level menu is for, set by the app. macOS treats the app, Window and Help menus
|
|
/// specially; File is where the system Close Window item goes.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
|
pub enum MenuRole {
|
|
/// Any menu without a fixed place.
|
|
#[default]
|
|
Other,
|
|
App,
|
|
File,
|
|
Edit,
|
|
View,
|
|
Window,
|
|
Help,
|
|
}
|
|
|
|
/// The platforms an item or submenu is shown on. All of them by default.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct Platforms(u8);
|
|
|
|
impl Platforms {
|
|
pub const NONE: Platforms = Platforms(0);
|
|
pub const MAC: Platforms = Platforms(1);
|
|
pub const WINDOWS: Platforms = Platforms(1 << 1);
|
|
pub const LINUX: Platforms = Platforms(1 << 2);
|
|
pub const WEB: Platforms = Platforms(1 << 3);
|
|
pub const ALL: Platforms = Platforms(0b1111);
|
|
|
|
/// Whether every platform in `other` is in `self`.
|
|
pub const fn contains(self, other: Platforms) -> bool {
|
|
self.0 & other.0 == other.0
|
|
}
|
|
|
|
/// `self` without the platforms in `other`: `Platforms::ALL.without(Platforms::WEB)`.
|
|
pub const fn without(self, other: Platforms) -> Platforms {
|
|
Platforms(self.0 & !other.0)
|
|
}
|
|
|
|
pub const fn is_empty(self) -> bool {
|
|
self.0 == 0
|
|
}
|
|
}
|
|
|
|
impl Default for Platforms {
|
|
fn default() -> Platforms {
|
|
Platforms::ALL
|
|
}
|
|
}
|
|
|
|
impl BitOr for Platforms {
|
|
type Output = Platforms;
|
|
fn bitor(self, other: Platforms) -> Platforms {
|
|
Platforms(self.0 | other.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum Node {
|
|
Item(Item),
|
|
Separator,
|
|
Submenu {
|
|
label: String,
|
|
children: Vec<Node>,
|
|
role: Option<ItemRole>,
|
|
platforms: Platforms,
|
|
},
|
|
/// An item AppKit provides and runs itself (Services, Zoom, …), with the label to show.
|
|
Standard {
|
|
kind: Standard,
|
|
label: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Item {
|
|
/// What the item runs: the app's command (`file.open`), with its parameters
|
|
/// (`CommandRef::with_params("file.open_recent", json!({"index": 3}))`), or a [`Builtin`]. A
|
|
/// click reports it.
|
|
pub command: CommandRef,
|
|
pub label: String,
|
|
/// `cmd` is ⌘ and `ctrl` the Control key: ⌃⌘M and ⌘M are different shortcuts.
|
|
pub shortcut: Option<Chord>,
|
|
pub enabled: bool,
|
|
pub kind: ItemKind,
|
|
pub role: Option<ItemRole>,
|
|
/// The platforms the item is shown on.
|
|
pub platforms: Platforms,
|
|
/// The layout gave this item its shortcut (⌘Q on the app's Quit, ⌘, on Settings): the app's
|
|
/// key handling doesn't know it, so a backend reports the key equivalent as a click.
|
|
pub layout_shortcut: bool,
|
|
}
|
|
|
|
/// A plain item, or one with a checked state.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
|
pub enum ItemKind {
|
|
#[default]
|
|
Plain,
|
|
/// A check item, and whether it is checked.
|
|
Check(bool),
|
|
/// One choice of a group: the radio items next to each other between separators. Whether it
|
|
/// is the selected one. macOS shows it as a check item.
|
|
Radio(bool),
|
|
}
|
|
|
|
impl ItemKind {
|
|
/// The checked or selected state, `None` for a plain item.
|
|
pub fn checked(self) -> Option<bool> {
|
|
match self {
|
|
ItemKind::Plain => None,
|
|
ItemKind::Check(on) | ItemKind::Radio(on) => Some(on),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Items the layouts put in fixed places. The app tags its own About, Settings, Quit, Appearance
|
|
/// and Language items; the Mac layout adds Hide and Minimize.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum ItemRole {
|
|
About,
|
|
/// The app's settings: an item, or a submenu of settings pages.
|
|
Settings,
|
|
/// The app's own quit command, which asks about unsaved changes. Without one the layout adds
|
|
/// [`Builtin::Quit`].
|
|
Quit,
|
|
Hide,
|
|
Minimize,
|
|
/// A submenu of themes or appearance modes, and items that go with it (Next Theme).
|
|
Appearance,
|
|
/// The app's language chooser, a submenu.
|
|
Language,
|
|
}
|
|
|
|
/// The items AppKit provides.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum Standard {
|
|
Services,
|
|
ShowAll,
|
|
Minimize,
|
|
Zoom,
|
|
BringAllToFront,
|
|
CloseWindow,
|
|
}
|
|
|
|
impl Standard {
|
|
/// The English label, as passed to [`crate::LayoutOptions::label`].
|
|
pub fn english(self) -> &'static str {
|
|
match self {
|
|
Standard::Services => "Services",
|
|
Standard::ShowAll => "Show All",
|
|
Standard::Minimize => "Minimize",
|
|
Standard::Zoom => "Zoom",
|
|
Standard::BringAllToFront => "Bring All to Front",
|
|
Standard::CloseWindow => "Close Window",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The commands of the items the crate adds itself. The crate runs them; an app sees them only
|
|
/// in a layout's result.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum Builtin {
|
|
/// Hide the app: a custom item, because AppKit's own always takes ⌘H, which apps often bind.
|
|
Hide,
|
|
/// Hide the other apps: a custom item, because AppKit's own always takes ⌥⌘H.
|
|
HideOthers,
|
|
/// Minimize: a custom item, used when the app binds ⌘M (AppKit's own always takes ⌘M).
|
|
/// [`crate::NativeMenu::take_clicks`] runs it.
|
|
Minimize,
|
|
/// Quit for an app without its own quit item: asks the window to close, through
|
|
/// [`craft_quit::on_menu_event`], so the app's unsaved-changes check runs.
|
|
Quit,
|
|
}
|
|
|
|
impl Builtin {
|
|
/// The command id.
|
|
pub const fn id(self) -> &'static str {
|
|
match self {
|
|
Builtin::Hide => "craft-menu.hide",
|
|
Builtin::HideOthers => "craft-menu.hide-others",
|
|
Builtin::Minimize => "craft-menu.minimize",
|
|
Builtin::Quit => craft_quit::QUIT_MENU_ID,
|
|
}
|
|
}
|
|
|
|
pub fn command(self) -> CommandRef {
|
|
CommandRef::new(self.id())
|
|
}
|
|
|
|
/// The built-in command `command` is, if any.
|
|
pub fn of(command: &CommandRef) -> Option<Builtin> {
|
|
if !command.params.is_null() {
|
|
return None;
|
|
}
|
|
[Builtin::Hide, Builtin::HideOthers, Builtin::Minimize, Builtin::Quit].into_iter().find(|b| b.id() == command.id)
|
|
}
|
|
}
|
|
|
|
impl Item {
|
|
/// An enabled plain item with no shortcut, shown on every platform.
|
|
pub fn new(command: impl Into<CommandRef>, label: impl Into<String>) -> Item {
|
|
Item {
|
|
command: command.into(),
|
|
label: label.into(),
|
|
shortcut: None,
|
|
enabled: true,
|
|
kind: ItemKind::Plain,
|
|
role: None,
|
|
platforms: Platforms::ALL,
|
|
layout_shortcut: false,
|
|
}
|
|
}
|
|
|
|
pub fn shortcut(mut self, shortcut: Chord) -> Item {
|
|
self.shortcut = Some(shortcut);
|
|
self
|
|
}
|
|
|
|
pub fn enabled(mut self, enabled: bool) -> Item {
|
|
self.enabled = enabled;
|
|
self
|
|
}
|
|
|
|
/// Make this a check item.
|
|
pub fn check(mut self, checked: bool) -> Item {
|
|
self.kind = ItemKind::Check(checked);
|
|
self
|
|
}
|
|
|
|
/// Make this a radio item, `selected` or not.
|
|
pub fn radio(mut self, selected: bool) -> Item {
|
|
self.kind = ItemKind::Radio(selected);
|
|
self
|
|
}
|
|
|
|
pub fn role(mut self, role: ItemRole) -> Item {
|
|
self.role = Some(role);
|
|
self
|
|
}
|
|
|
|
pub fn platforms(mut self, platforms: Platforms) -> Item {
|
|
self.platforms = platforms;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl From<Item> for Node {
|
|
fn from(item: Item) -> Node {
|
|
Node::Item(item)
|
|
}
|
|
}
|
|
|
|
impl Node {
|
|
/// A submenu shown on every platform.
|
|
pub fn submenu(label: impl Into<String>, children: Vec<Node>) -> Node {
|
|
Node::Submenu { label: label.into(), children, role: None, platforms: Platforms::ALL }
|
|
}
|
|
|
|
/// Give an item or a submenu a role: `Node::submenu("Theme", pages).role(ItemRole::Appearance)`.
|
|
pub fn role(mut self, role: ItemRole) -> Node {
|
|
match &mut self {
|
|
Node::Item(it) => it.role = Some(role),
|
|
Node::Submenu { role: r, .. } => *r = Some(role),
|
|
Node::Separator | Node::Standard { .. } => {}
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Show an item or a submenu only on `platforms`.
|
|
pub fn platforms(mut self, platforms: Platforms) -> Node {
|
|
match &mut self {
|
|
Node::Item(it) => it.platforms = platforms,
|
|
Node::Submenu { platforms: p, .. } => *p = platforms,
|
|
Node::Separator | Node::Standard { .. } => {}
|
|
}
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Menu {
|
|
pub fn new(title: impl Into<String>, children: Vec<Node>) -> Menu {
|
|
Menu { title: title.into(), role: MenuRole::Other, children }
|
|
}
|
|
|
|
pub fn role(mut self, role: MenuRole) -> Menu {
|
|
self.role = role;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl MenuBar {
|
|
/// Every item, depth first.
|
|
pub fn items(&self) -> Vec<&Item> {
|
|
fn walk<'a>(nodes: &'a [Node], out: &mut Vec<&'a Item>) {
|
|
for n in nodes {
|
|
match n {
|
|
Node::Item(it) => out.push(it),
|
|
Node::Submenu { children, .. } => walk(children, out),
|
|
Node::Separator | Node::Standard { .. } => {}
|
|
}
|
|
}
|
|
}
|
|
let mut out = Vec::new();
|
|
for m in &self.menus {
|
|
walk(&m.children, &mut out);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Every item, depth first, changed in place.
|
|
pub(crate) fn for_each_item_mut(&mut self, mut f: impl FnMut(&mut Item)) {
|
|
fn walk(nodes: &mut [Node], f: &mut dyn FnMut(&mut Item)) {
|
|
for n in nodes {
|
|
match n {
|
|
Node::Item(it) => f(it),
|
|
Node::Submenu { children, .. } => walk(children, f),
|
|
Node::Separator | Node::Standard { .. } => {}
|
|
}
|
|
}
|
|
}
|
|
for m in &mut self.menus {
|
|
walk(&mut m.children, &mut f);
|
|
}
|
|
}
|
|
|
|
/// Whether `command` opens one of [`MenuBar::documents`].
|
|
pub(crate) fn is_document(&self, command: &CommandRef) -> bool {
|
|
self.documents.iter().any(|d| d.command == *command)
|
|
}
|
|
|
|
pub fn find(&self, command: &CommandRef) -> Option<&Item> {
|
|
self.items().into_iter().find(|it| it.command == *command)
|
|
}
|
|
|
|
pub fn menu(&self, role: MenuRole) -> Option<&Menu> {
|
|
self.menus.iter().find(|m| m.role == role)
|
|
}
|
|
|
|
/// Give every item with this command the role, at any depth.
|
|
pub fn tag_item(&mut self, command: &CommandRef, role: ItemRole) {
|
|
fn walk(nodes: &mut [Node], command: &CommandRef, role: ItemRole) {
|
|
for n in nodes {
|
|
match n {
|
|
Node::Item(it) if it.command == *command => it.role = Some(role),
|
|
Node::Submenu { children, .. } => walk(children, command, role),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
for m in &mut self.menus {
|
|
walk(&mut m.children, command, role);
|
|
}
|
|
}
|
|
|
|
/// Give the submenu `name`, directly in the menu with the role `menu`, the role.
|
|
pub fn tag_submenu(&mut self, menu: MenuRole, name: &str, role: ItemRole) {
|
|
let Some(menu) = self.menus.iter_mut().find(|m| m.role == menu) else { return };
|
|
for n in &mut menu.children {
|
|
if let Node::Submenu { label, role: r, .. } = n
|
|
&& label == name
|
|
{
|
|
*r = Some(role);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Give the menu titled `title` the role: a shortcut for apps that build the bar from rows.
|
|
pub fn tag_menu(&mut self, title: &str, role: MenuRole) {
|
|
if let Some(m) = self.menus.iter_mut().find(|m| m.title == title) {
|
|
m.role = role;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One row of a flat menu list: the menu path (`["File", "Open Recent"]`) and the item in it.
|
|
/// A row labelled `---` is a separator.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Row {
|
|
pub path: Vec<String>,
|
|
pub command: CommandRef,
|
|
pub label: String,
|
|
pub shortcut: Option<Chord>,
|
|
pub enabled: bool,
|
|
pub kind: ItemKind,
|
|
}
|
|
|
|
/// The menus as a tree, from flat rows, in the order an in-window bar draws them: one menu per
|
|
/// entry of `tops`, and at each depth leaves and separators in order, with a submenu where its
|
|
/// first row is. The result is [`tidy`].
|
|
pub fn from_items(tops: &[&str], rows: &[Row]) -> MenuBar {
|
|
fn level(rows: &[&Row], depth: usize) -> Vec<Node> {
|
|
let mut out = Vec::new();
|
|
let mut shown: Vec<&str> = Vec::new();
|
|
for it in rows {
|
|
if it.path.len() == depth {
|
|
if it.label == "---" {
|
|
out.push(Node::Separator);
|
|
} else {
|
|
out.push(Node::Item(Item {
|
|
shortcut: it.shortcut,
|
|
enabled: it.enabled,
|
|
kind: it.kind,
|
|
..Item::new(it.command.clone(), it.label.clone())
|
|
}));
|
|
}
|
|
} else if let Some(name) = it.path.get(depth) {
|
|
let name = name.as_str();
|
|
if shown.contains(&name) {
|
|
continue;
|
|
}
|
|
shown.push(name);
|
|
let child: Vec<&Row> = rows.iter().copied().filter(|c| c.path.get(depth).map(String::as_str) == Some(name)).collect();
|
|
out.push(Node::submenu(name, level(&child, depth + 1)));
|
|
}
|
|
}
|
|
tidy(out)
|
|
}
|
|
let menus = tops
|
|
.iter()
|
|
.map(|top| {
|
|
let rows: Vec<&Row> = rows.iter().filter(|r| r.path.first().map(String::as_str) == Some(*top)).collect();
|
|
Menu::new(*top, level(&rows, 1))
|
|
})
|
|
.collect();
|
|
MenuBar { menus, documents: Vec::new() }
|
|
}
|
|
|
|
/// Drop separators at the start or end of a level and collapse runs of them, and drop empty
|
|
/// submenus, at every depth. A submenu holding only a disabled placeholder (`No Recent Files`)
|
|
/// stays.
|
|
pub fn tidy(nodes: Vec<Node>) -> Vec<Node> {
|
|
let mut out: Vec<Node> = Vec::with_capacity(nodes.len());
|
|
for n in nodes {
|
|
let n = match n {
|
|
Node::Submenu { label, children, role, platforms } => {
|
|
let children = tidy(children);
|
|
if children.is_empty() {
|
|
continue;
|
|
}
|
|
Node::Submenu { label, children, role, platforms }
|
|
}
|
|
other => other,
|
|
};
|
|
if matches!(n, Node::Separator) && matches!(out.last(), None | Some(Node::Separator)) {
|
|
continue;
|
|
}
|
|
out.push(n);
|
|
}
|
|
while matches!(out.last(), Some(Node::Separator)) {
|
|
out.pop();
|
|
}
|
|
out
|
|
}
|
|
|
|
/// A hash of everything a native menu can only change by rebuilding: menus, roles, item
|
|
/// commands, kinds (plain, check or radio), shortcuts, submenu labels and the labels of AppKit's items. Item
|
|
/// labels, enabled and checked are updated in place. Window's open documents (the items running a
|
|
/// [`MenuBar::documents`] command) are left out: opening or closing a document rebuilds only the
|
|
/// Window menu ([`window_key`]).
|
|
pub fn structure_key(bar: &MenuBar) -> u64 {
|
|
let mut h = DefaultHasher::new();
|
|
for m in &bar.menus {
|
|
(&m.title, m.role).hash(&mut h);
|
|
if m.role == MenuRole::Window {
|
|
let document = |n: &Node| matches!(n, Node::Item(it) if bar.is_document(&it.command));
|
|
let mut rest: Vec<Node> = m.children.iter().filter(|n| !document(n)).cloned().collect();
|
|
while matches!(rest.last(), Some(Node::Separator)) {
|
|
rest.pop();
|
|
}
|
|
hash_nodes(&rest, &mut h);
|
|
} else {
|
|
hash_nodes(&m.children, &mut h);
|
|
}
|
|
}
|
|
h.finish()
|
|
}
|
|
|
|
/// The Window menu's structure, its open documents included: when it changes and nothing else
|
|
/// did, a native menu rebuilds just the Window menu.
|
|
pub fn window_key(bar: &MenuBar) -> u64 {
|
|
let mut h = DefaultHasher::new();
|
|
if let Some(m) = bar.menu(MenuRole::Window) {
|
|
hash_nodes(&m.children, &mut h);
|
|
}
|
|
h.finish()
|
|
}
|
|
|
|
fn hash_nodes(nodes: &[Node], h: &mut DefaultHasher) {
|
|
for n in nodes {
|
|
match n {
|
|
Node::Item(it) => ('i', it.command.key(), std::mem::discriminant(&it.kind), &it.shortcut, it.role, it.layout_shortcut).hash(h),
|
|
Node::Separator => '-'.hash(h),
|
|
Node::Submenu { label, children, role, .. } => {
|
|
('[', label, role).hash(h);
|
|
hash_nodes(children, h);
|
|
']'.hash(h);
|
|
}
|
|
Node::Standard { kind, label } => ('s', kind, label).hash(h),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The native id of an item with `command`: its [`CommandRef::key`], or `key#2`, `key#3`, …
|
|
/// while `taken`, since native ids must be unique and a command can be shown twice (Keyboard
|
|
/// Shortcuts in Edit and Window).
|
|
#[cfg(any(target_os = "macos", test))]
|
|
pub(crate) fn native_id(command: &CommandRef, taken: impl Fn(&str) -> bool) -> String {
|
|
let key = command.key();
|
|
let mut unique = key.clone();
|
|
let mut n = 2;
|
|
while taken(&unique) {
|
|
unique = format!("{key}#{n}");
|
|
n += 1;
|
|
}
|
|
unique
|
|
}
|