mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-09 03:51:47 +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
493 lines
23 KiB
Rust
493 lines
23 KiB
Rust
//! The app's side: events from the platform menu in, menu bars out.
|
||
|
||
use std::borrow::Cow;
|
||
use std::collections::HashSet;
|
||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||
|
||
use craft_keymap::{Chord, CommandRef, Key, Mods};
|
||
|
||
use crate::layout::{Clash, Layout};
|
||
use crate::model::{Builtin, Item, ItemKind, MenuBar, Node};
|
||
|
||
/// What the native menu reports.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub enum Event {
|
||
/// An item was chosen with the mouse (or the keyboard inside an open menu), or by a key
|
||
/// equivalent the app's key handling doesn't know ([`crate::Item::layout_shortcut`]): the
|
||
/// item's command.
|
||
Click(CommandRef),
|
||
/// A key equivalent was pressed: the shortcut, to be handled as the key press it was.
|
||
Key(Chord),
|
||
/// An item whose shortcut a text field handles itself (Edit › Copy, Undo) was clicked while a
|
||
/// text field has the keys: the shortcut, to be handled as a press and release of it.
|
||
Tap(Chord),
|
||
/// A click the app's [`NativeMenu::on_command`] hook ran; [`NativeMenu::take_clicks`] leaves it
|
||
/// out.
|
||
Handled(CommandRef),
|
||
}
|
||
|
||
/// How ⌘C, ⌘X and ⌘V reach egui when the native menu takes them as key equivalents (an Edit
|
||
/// menu with Copy, Cut and Paste items).
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub enum ClipboardKeys {
|
||
/// As egui-winit makes them without a native menu: ⌘C is [`egui::Event::Copy`], ⌘X
|
||
/// [`egui::Event::Cut`], and ⌘V a [`egui::ViewportCommand::RequestPaste`], which eframe answers
|
||
/// with an [`egui::Event::Paste`] of the clipboard's text in the next frame. Text fields copy,
|
||
/// cut and paste.
|
||
#[default]
|
||
Egui,
|
||
/// Key presses like any other key equivalent, for an app that runs its clipboard commands
|
||
/// from the keys itself.
|
||
Raw,
|
||
}
|
||
|
||
/// What may run from the menus this frame, set by the app.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub struct Gate {
|
||
/// A modal dialog is open.
|
||
pub modal: bool,
|
||
/// The shortcut editor is recording a chord.
|
||
pub recording_shortcut: bool,
|
||
/// A text field has the keys.
|
||
pub text_editing: bool,
|
||
}
|
||
|
||
impl Gate {
|
||
/// Whether `item` may run now: while a modal dialog is open or a shortcut is being recorded,
|
||
/// only the built-in items (Hide, Hide Others, Minimize, Quit) do. The built-in Quit stays
|
||
/// because it asks the window to close, and the app's close guard knows about its dialogs; an
|
||
/// app's own Quit item ([`crate::ItemRole::Quit`]) is greyed with its other commands. A text field having the keys doesn't stop a click: menu commands run while
|
||
/// typing, as in a Mac app, and the keys a field keeps are handled before the menus see them.
|
||
pub fn allows(&self, item: &Item) -> bool {
|
||
self.allows_command(&item.command)
|
||
}
|
||
|
||
/// [`Gate::allows`] for an item that runs `command`.
|
||
pub fn allows_command(&self, command: &CommandRef) -> bool {
|
||
!(self.modal || self.recording_shortcut) || Builtin::of(command).is_some()
|
||
}
|
||
|
||
/// `bar` with every item the gate forbids disabled, so the menus show them greyed, but the
|
||
/// items whose shortcut a text field handles itself (Undo, Copy, Select All, …).
|
||
pub(crate) fn grey<'a>(&self, bar: &'a MenuBar) -> Cow<'a, MenuBar> {
|
||
fn walk(nodes: &mut [Node], gate: &Gate) {
|
||
for n in nodes {
|
||
match n {
|
||
// The field's own keys stay enabled: AppKit doesn't send a disabled item's key
|
||
// equivalent on, so greyed Copy or Undo would leave ⌘C and ⌘Z dead in a
|
||
// dialog's text fields. Their clicks are still dropped (`take_clicks`).
|
||
Node::Item(it) if !gate.allows(it) && !it.shortcut.as_ref().is_some_and(text_editing_key) => it.enabled = false,
|
||
Node::Submenu { children, .. } => walk(children, gate),
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
if !(self.modal || self.recording_shortcut) {
|
||
return Cow::Borrowed(bar);
|
||
}
|
||
let mut bar = bar.clone();
|
||
for m in &mut bar.menus {
|
||
walk(&mut m.children, self);
|
||
}
|
||
Cow::Owned(bar)
|
||
}
|
||
}
|
||
|
||
/// An app's [`NativeMenu::on_command`] hook: `true` when it ran the command.
|
||
pub type CommandHook = Box<dyn Fn(&CommandRef) -> bool + Send + Sync>;
|
||
|
||
/// What a backend's menu event handler needs from the driver when an item is chosen: the
|
||
/// current [`Gate`] and the app's [`NativeMenu::on_command`] hook. The platform calls the handler
|
||
/// between frames, from the event that chose the item, so this is shared with the driver rather
|
||
/// than borrowed from it. [`NativeMenu::new`] hands it to the backend ([`Backend::attach`]).
|
||
#[derive(Clone, Default)]
|
||
pub struct Dispatch(Arc<Mutex<Shared>>);
|
||
|
||
type SharedHook = Arc<dyn Fn(&CommandRef) -> bool + Send + Sync>;
|
||
|
||
#[derive(Default)]
|
||
struct Shared {
|
||
gate: Gate,
|
||
hook: Option<SharedHook>,
|
||
}
|
||
|
||
impl Dispatch {
|
||
/// The event for an item with `command`, its `shortcut` and [`Item::layout_shortcut`],
|
||
/// chosen with the modifiers `held` for a key-down (⌘, ⌃, ⌥, ⇧ as AppKit reports them) or
|
||
/// `None` for a mouse click, under the current gate. A click the gate allows on an app command
|
||
/// (not a [`Builtin`]) also goes to the app's [`NativeMenu::on_command`] hook, before this
|
||
/// returns; when the hook ran it, the event is [`Event::Handled`].
|
||
pub fn chosen(&self, command: &CommandRef, shortcut: Option<&Chord>, layout_shortcut: bool, held: Option<Mods>) -> Event {
|
||
let (gate, hook) = {
|
||
let shared = self.lock();
|
||
(shared.gate, shared.hook.clone())
|
||
};
|
||
let event = chosen_event(command, shortcut, layout_shortcut, held, gate);
|
||
if let (Event::Click(c), Some(hook)) = (&event, hook)
|
||
&& gate.allows_command(c)
|
||
&& Builtin::of(c).is_none()
|
||
&& hook(c)
|
||
{
|
||
return Event::Handled(c.clone());
|
||
}
|
||
event
|
||
}
|
||
|
||
fn lock(&self) -> MutexGuard<'_, Shared> {
|
||
self.0.lock().unwrap_or_else(PoisonError::into_inner)
|
||
}
|
||
}
|
||
|
||
/// A platform's native menu bar.
|
||
pub trait Backend {
|
||
/// Bring the menu in step with `bar`: update labels, enabled and checked in place, or
|
||
/// rebuild when its structure changed ([`crate::structure_key`], [`crate::window_key`]).
|
||
fn sync(&mut self, bar: &MenuBar);
|
||
/// Events since the last call. Platform items (Hide, Services, Zoom, …) are handled by the
|
||
/// backend and not reported.
|
||
fn drain(&mut self) -> Vec<Event>;
|
||
/// Whether one of the native menus is open.
|
||
fn tracking(&self) -> bool {
|
||
false
|
||
}
|
||
/// Close the open native menu, if there is one.
|
||
fn cancel_tracking(&mut self) {}
|
||
/// Keep the driver's [`Dispatch`]: the backend's menu event handler turns every chosen item
|
||
/// into an [`Event`] through [`Dispatch::chosen`]. [`NativeMenu::new`] calls it once.
|
||
fn attach(&mut self, dispatch: Dispatch);
|
||
}
|
||
|
||
/// The native menu bar, while the app uses one (its in-window menus are hidden then).
|
||
pub struct NativeMenu {
|
||
backend: Box<dyn Backend>,
|
||
/// The state the menus were last built from.
|
||
state: Option<u64>,
|
||
clicks: Vec<CommandRef>,
|
||
/// An event arrived (AppKit may have toggled a check item itself): re-sync.
|
||
dirty: bool,
|
||
clashes: Vec<Clash>,
|
||
clipboard: ClipboardKeys,
|
||
/// ⌘V arrived: [`NativeMenu::take_clicks`] asks eframe for the paste.
|
||
paste: bool,
|
||
/// Whether a click may be kept from egui as the one closing an open menu (see
|
||
/// [`NativeMenu::raw_input`]).
|
||
armed: bool,
|
||
gate: Gate,
|
||
dispatch: Dispatch,
|
||
/// The menus last laid out, before the gate greyed anything, and the gate they were last
|
||
/// synced with: a gate change re-syncs them without laying them out again.
|
||
bar: Option<MenuBar>,
|
||
synced_gate: Option<Gate>,
|
||
/// The commands whose items were check items in a layout so far: they stay check items, so
|
||
/// that `checked` going from `Some` to `None` doesn't rebuild the menus.
|
||
checkable: HashSet<String>,
|
||
}
|
||
|
||
impl NativeMenu {
|
||
pub fn new(mut backend: Box<dyn Backend>) -> NativeMenu {
|
||
let dispatch = Dispatch::default();
|
||
backend.attach(dispatch.clone());
|
||
NativeMenu {
|
||
backend,
|
||
state: None,
|
||
clicks: Vec::new(),
|
||
dirty: true,
|
||
clashes: Vec::new(),
|
||
clipboard: ClipboardKeys::default(),
|
||
paste: false,
|
||
armed: true,
|
||
gate: Gate::default(),
|
||
dispatch,
|
||
bar: None,
|
||
synced_gate: None,
|
||
checkable: HashSet::new(),
|
||
}
|
||
}
|
||
|
||
pub fn clipboard_keys(mut self, keys: ClipboardKeys) -> NativeMenu {
|
||
self.clipboard = keys;
|
||
self
|
||
}
|
||
|
||
/// What may run from the menus. Call it every frame, before [`NativeMenu::take_clicks`]; it
|
||
/// also holds for the menu events that arrive until the next call.
|
||
///
|
||
/// - While [`Gate::modal`] or [`Gate::recording_shortcut`] is on, the app's items are greyed
|
||
/// (updated in place at the next [`NativeMenu::sync_with`]) and clicks on them are dropped,
|
||
/// also a click that arrived before the gate closed and is still waiting for
|
||
/// [`NativeMenu::take_clicks`]. The built-in items (Hide, Hide Others, Minimize, Quit) stay.
|
||
/// A greyed item's key equivalent goes to the window, so the app's key handling sees it.
|
||
/// - While [`Gate::recording_shortcut`] is on, every key equivalent reaches egui as the plain
|
||
/// key press that was made (⌘Q, ⌘H and ⌘C included), for the shortcut recorder.
|
||
/// - While [`Gate::text_editing`] is on (in egui, `ctx.wants_keyboard_input()`), a key
|
||
/// equivalent runs its item as an [`Event::Click`], as a Mac app's menu commands run while
|
||
/// typing, except the keys the field handles itself: ⌘C, ⌘X and ⌘V (following
|
||
/// [`ClipboardKeys`]) and [`craft_keymap::text_editing_chords`] (⌘A, ⌘Z, ⇧⌘Z, ⌘←, …) stay
|
||
/// key presses.
|
||
pub fn set_gate(&mut self, gate: Gate) {
|
||
self.gate = gate;
|
||
self.dispatch.lock().gate = gate;
|
||
}
|
||
|
||
/// Run `hook` with each command clicked in the menus, from the menu event handler, as the
|
||
/// click arrives: on macOS on the main thread, between frames, also while egui is idle. Only
|
||
/// app commands the current gate ([`NativeMenu::set_gate`]) allows reach it, and key
|
||
/// equivalents reported as key presses don't. It must return quickly; it is for waking the
|
||
/// app's own loop or a worker.
|
||
///
|
||
/// The hook returns `true` when it ran the command: [`NativeMenu::take_clicks`] then leaves
|
||
/// it out. A command it returned `false` for comes back from `take_clicks`, after the gate as
|
||
/// it is then.
|
||
pub fn on_command(&mut self, hook: CommandHook) {
|
||
self.dispatch.lock().hook = Some(Arc::from(hook));
|
||
}
|
||
|
||
/// Before egui sees this frame's input: key equivalents become the key presses they were,
|
||
/// and clicks wait for [`NativeMenu::take_clicks`].
|
||
///
|
||
/// A click or Escape in the window closes an open native menu. AppKit can deliver it to the
|
||
/// window while the menu is still open (asynchronous menu presentation), so while the backend
|
||
/// reports one open, the click or Escape is taken out of the input: it closes the menu
|
||
/// without starting a canvas gesture or reaching the active tool. Pointer moves stay. Only
|
||
/// one such click is kept from egui until the backend reports no menu open again, so a
|
||
/// backend that wrongly keeps reporting one open costs one click, not every click.
|
||
///
|
||
/// AppKit takes only the key-down: winit still delivers the key-up (it forwards key-ups while
|
||
/// ⌘ is held). So a key equivalent adds just its press, ahead of its key-up when that is
|
||
/// already queued. Adding a made-up release as well sends two key-ups; in PhotoCraft, which
|
||
/// reads a ⌘V release without a press as an image paste, that made ⌘V paste three times.
|
||
/// ⌘C, ⌘X and ⌘V follow [`ClipboardKeys`].
|
||
pub fn raw_input(&mut self, raw: &mut egui::RawInput) {
|
||
// The window's own input, before key equivalents are added to it.
|
||
if let Some(kept) = dismiss_input(raw, || self.armed && self.backend.tracking()) {
|
||
if kept {
|
||
self.armed = false;
|
||
}
|
||
// Also without a menu reported open: an asynchronously presented menu can still be
|
||
// on screen after it reports closing.
|
||
self.backend.cancel_tracking();
|
||
}
|
||
if !self.armed && !self.backend.tracking() {
|
||
self.armed = true;
|
||
}
|
||
for e in self.backend.drain() {
|
||
match e {
|
||
Event::Key(chord) => self.add_key(raw, &chord, false),
|
||
Event::Tap(chord) => self.add_key(raw, &chord, true),
|
||
Event::Click(command) => self.clicks.push(command),
|
||
Event::Handled(_) => {}
|
||
}
|
||
self.dirty = true;
|
||
}
|
||
}
|
||
|
||
/// `tap`: a press and its release, for a click that stands for the key (no key-up follows).
|
||
fn add_key(&mut self, raw: &mut egui::RawInput, chord: &Chord, tap: bool) {
|
||
let pressed_key = chord.key.to_egui();
|
||
let clipboard = self.clipboard == ClipboardKeys::Egui && chord.mods == Mods::CMD && !self.gate.recording_shortcut;
|
||
let event = match chord.key {
|
||
Key::C if clipboard => egui::Event::Copy,
|
||
Key::X if clipboard => egui::Event::Cut,
|
||
Key::V if clipboard => {
|
||
self.paste = true;
|
||
return;
|
||
}
|
||
_ => key_event(chord, true),
|
||
};
|
||
let at = raw
|
||
.events
|
||
.iter()
|
||
.position(|e| matches!(e, egui::Event::Key { key, pressed: false, .. } if *key == pressed_key))
|
||
.unwrap_or(raw.events.len());
|
||
let press = matches!(event, egui::Event::Key { .. });
|
||
raw.events.insert(at, event);
|
||
if tap && press {
|
||
raw.events.insert(at + 1, key_event(chord, false));
|
||
}
|
||
}
|
||
|
||
/// The commands clicked since the last call that the gate ([`NativeMenu::set_gate`]) allows
|
||
/// now, for the app to run like in-window menu clicks; the others are dropped.
|
||
/// [`Builtin::Minimize`] (minimizes the window) and [`Builtin::Quit`] (asks the window to
|
||
/// close, see [`craft_quit::on_menu_event`]) are run here and left out. A ⌘V from the menu
|
||
/// ([`ClipboardKeys::Egui`]) is asked of eframe here too.
|
||
pub fn take_clicks(&mut self, ctx: &egui::Context) -> Vec<CommandRef> {
|
||
if std::mem::take(&mut self.paste) {
|
||
ctx.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||
}
|
||
let mut out = Vec::new();
|
||
for command in std::mem::take(&mut self.clicks) {
|
||
if !self.gate.allows_command(&command) {
|
||
continue;
|
||
}
|
||
match Builtin::of(&command) {
|
||
Some(Builtin::Minimize) => ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true)),
|
||
Some(Builtin::Quit) => {
|
||
craft_quit::on_menu_event(ctx, Builtin::Quit.id());
|
||
}
|
||
_ => out.push(command),
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Bring the native menu up to date. `state` is a hash of what the menus show (see
|
||
/// [`had_input`]); `build` lays them out ([`crate::mac_layout`]) and runs only when `state`
|
||
/// changed or a menu event arrived since the last sync, since building the rows usually walks
|
||
/// every command's enabled state. A menu event re-syncs because AppKit flips a check or radio
|
||
/// item's state itself when it is chosen: the sync sets it back to the app's state. A gate
|
||
/// change ([`NativeMenu::set_gate`]) re-syncs the menus last laid out, greyed or not, without
|
||
/// calling `build`.
|
||
///
|
||
/// `state` must cover everything the menus show. A change that comes without input, such as
|
||
/// a load or save finishing on a worker or a timer, leaves the menus stale until the next
|
||
/// input or menu event unless it is in the hash. An app that can't keep the hash complete
|
||
/// uses [`NativeMenu::sync`] instead.
|
||
///
|
||
/// Returns the layout's clashes when they differ from the last ones returned, for the app to
|
||
/// log; otherwise nothing.
|
||
pub fn sync_with(&mut self, state: u64, build: impl FnOnce() -> Layout) -> Vec<Clash> {
|
||
let relayout = self.state != Some(state) || self.dirty;
|
||
if !relayout && self.synced_gate == Some(self.gate) {
|
||
return Vec::new();
|
||
}
|
||
let mut clashes = Vec::new();
|
||
if relayout {
|
||
let mut layout = build();
|
||
self.keep_kinds(&mut layout.bar);
|
||
clashes = self.keep(layout);
|
||
self.state = Some(state);
|
||
}
|
||
self.push();
|
||
clashes
|
||
}
|
||
|
||
/// [`NativeMenu::sync_with`] without a state hash: the app lays the menus out every frame
|
||
/// and they are synced when the layout differs from the last one (or a menu event or gate
|
||
/// change calls for it), so they never go stale. It costs a layout per frame.
|
||
pub fn sync(&mut self, mut layout: Layout) -> Vec<Clash> {
|
||
self.keep_kinds(&mut layout.bar);
|
||
if !self.dirty && self.bar.as_ref() == Some(&layout.bar) && self.clashes == layout.clashes && self.synced_gate == Some(self.gate) {
|
||
return Vec::new();
|
||
}
|
||
let clashes = self.keep(layout);
|
||
// A later `sync_with` lays the menus out again.
|
||
self.state = None;
|
||
self.push();
|
||
clashes
|
||
}
|
||
|
||
/// An item that was a check item once stays one, unchecked: switching its kind would change
|
||
/// [`crate::structure_key`] and rebuild the menus.
|
||
fn keep_kinds(&mut self, bar: &mut MenuBar) {
|
||
let checkable = &mut self.checkable;
|
||
bar.for_each_item_mut(|it| match it.kind {
|
||
ItemKind::Check(_) => {
|
||
checkable.insert(it.command.key());
|
||
}
|
||
ItemKind::Plain if checkable.contains(&it.command.key()) => it.kind = ItemKind::Check(false),
|
||
_ => {}
|
||
});
|
||
}
|
||
|
||
/// Keep `layout` as the menus to show; its clashes when they differ from the last ones.
|
||
fn keep(&mut self, layout: Layout) -> Vec<Clash> {
|
||
self.dirty = false;
|
||
self.bar = Some(layout.bar);
|
||
if layout.clashes == self.clashes {
|
||
return Vec::new();
|
||
}
|
||
self.clashes = layout.clashes.clone();
|
||
layout.clashes
|
||
}
|
||
|
||
/// Sync the backend with the kept menus, greyed by the gate.
|
||
fn push(&mut self) {
|
||
if let Some(bar) = &self.bar {
|
||
self.backend.sync(&self.gate.grey(bar));
|
||
}
|
||
self.synced_gate = Some(self.gate);
|
||
}
|
||
}
|
||
|
||
/// `Some` when `raw` has a press or Escape that closes an open menu: `Some(true)` when, with
|
||
/// `tracking` (a menu is open), those events were removed, since they belong to the menu.
|
||
pub(crate) fn dismiss_input(raw: &mut egui::RawInput, tracking: impl FnOnce() -> bool) -> Option<bool> {
|
||
if !raw
|
||
.events
|
||
.iter()
|
||
.any(|e| matches!(e, egui::Event::PointerButton { pressed: true, .. } | egui::Event::Key { key: egui::Key::Escape, pressed: true, .. }))
|
||
{
|
||
return None;
|
||
}
|
||
let kept = tracking();
|
||
if kept {
|
||
raw.events.retain(|e| !matches!(e, egui::Event::PointerButton { .. } | egui::Event::Key { key: egui::Key::Escape, .. }));
|
||
}
|
||
Some(kept)
|
||
}
|
||
|
||
/// What a backend reports for an item chosen with `command`, `chord` (its shortcut) and
|
||
/// `layout_shortcut` ([`crate::Item::layout_shortcut`]) under `gate`. `held` holds the modifiers
|
||
/// held when a key-down chose it, `None` for a mouse click.
|
||
///
|
||
/// A key-down is the item's key equivalent, reported as the key press, unless it was ↩ or Space
|
||
/// inside the open menu: the item's shortcut is not a key equivalent at all ([`native_ok`]), or
|
||
/// neither ⌘ nor ⌃ is held. The key is the item's (AppKit matched on it); the modifiers are the
|
||
/// ones held, since AppKit may match ⌘R to a ⇧⌘R item. Minimize, craft-quit's Quit and shortcuts
|
||
/// the layout assigned are run by the driver or the app's click handling, not by its key
|
||
/// handling, so they stay clicks. While text editing, the app's key handling leaves shortcuts to
|
||
/// the field, so a key equivalent is a click too, except the field's own editing keys
|
||
/// ([`text_editing_key`]); a mouse click on one of those items (Edit › Copy, Undo) is a tap of
|
||
/// its shortcut ([`Event::Tap`]), so it acts on the field rather than the document. While recording a shortcut every key equivalent is a key press, for the
|
||
/// recorder.
|
||
pub(crate) fn chosen_event(command: &CommandRef, chord: Option<&Chord>, layout_shortcut: bool, held: Option<Mods>, gate: Gate) -> Event {
|
||
let recording = gate.recording_shortcut;
|
||
let runs_as_click = !recording && (layout_shortcut || matches!(Builtin::of(command), Some(Builtin::Minimize | Builtin::Quit)));
|
||
let Some(held) = held else {
|
||
return match chord {
|
||
Some(c) if !recording && gate.text_editing && text_editing_key(c) => Event::Tap(*c),
|
||
_ => Event::Click(command.clone()),
|
||
};
|
||
};
|
||
let pressed = chord
|
||
.filter(|c| !runs_as_click && native_ok(c) && (held.cmd || held.ctrl))
|
||
.map(|c| Chord::new(held, c.key))
|
||
.filter(|c| recording || !gate.text_editing || text_editing_key(c));
|
||
match pressed {
|
||
Some(chord) => Event::Key(chord),
|
||
None => Event::Click(command.clone()),
|
||
}
|
||
}
|
||
|
||
/// A chord a text field handles itself: the clipboard keys and craft-keymap's text-editing
|
||
/// chords (undo, redo, select all, moving by word and line).
|
||
fn text_editing_key(c: &Chord) -> bool {
|
||
const CLIPBOARD: [Chord; 3] = [Chord::new(Mods::CMD, Key::C), Chord::new(Mods::CMD, Key::X), Chord::new(Mods::CMD, Key::V)];
|
||
CLIPBOARD.contains(c) || craft_keymap::text_editing_chords(craft_keymap::Platform::Mac).contains(c)
|
||
}
|
||
|
||
/// May the native menu make this a key equivalent? Only with ⌘ or ⌃. Other shortcuts (a bare
|
||
/// letter, ⇧ or ⌥ alone, a bare function key) are tool keys: as key equivalents AppKit would take
|
||
/// a `b` typed into a text field, and the app's key handling may bind them per tool or context.
|
||
/// They are shown but not made key equivalents.
|
||
pub(crate) fn native_ok(c: &Chord) -> bool {
|
||
c.has_command()
|
||
}
|
||
|
||
/// The key press egui would have received for `chord` on macOS: `cmd` is ⌘, `ctrl` the Control
|
||
/// key.
|
||
pub(crate) fn key_event(chord: &Chord, pressed: bool) -> egui::Event {
|
||
let m = chord.mods;
|
||
let key = chord.key.to_egui();
|
||
let modifiers = egui::Modifiers { alt: m.alt, ctrl: m.ctrl, shift: m.shift, mac_cmd: m.cmd, command: m.cmd };
|
||
egui::Event::Key { key, physical_key: Some(key), pressed, repeat: false, modifiers }
|
||
}
|
||
|
||
/// A key press or click this frame. It may have changed state an app's menu hash doesn't list,
|
||
/// so apps fold it into the `state` passed to [`NativeMenu::sync_with`], for example as
|
||
/// `had_input(ctx).then_some(ctx.cumulative_frame_nr())`.
|
||
pub fn had_input(ctx: &egui::Context) -> bool {
|
||
ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Key { pressed: true, .. } | egui::Event::PointerButton { pressed: false, .. })))
|
||
}
|