vectorcraft/apps/drawcraft/src/native_menu.rs

129 lines
4.9 KiB
Rust

//! macOS: DrawCraft's menu tree as the native system menu bar (like Illustrator on the Mac).
//! Items dispatch through the same command path as the in-window menus; enablement, check marks
//! and dynamic labels ("Undo Move") are refreshed a few times per second.
use std::collections::HashMap;
use std::str::FromStr;
use drawcraft_ui_egui::DrawcraftApp;
use drawcraft_ui_egui::menus::{self, Item};
use muda::accelerator::Accelerator;
use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
use serde_json::Value;
enum Handle {
Plain(MenuItem),
Check(CheckMenuItem),
}
pub struct NativeMenu {
_menu: Menu,
items: HashMap<String, (String, Value, Handle)>,
last_refresh: f64,
}
/// Accelerator for a shortcut like "Cmd+Shift+]" (modifier-less shortcuts stay in the app so they
/// don't steal keys from text fields).
fn accel(sc: &str) -> Option<Accelerator> {
if !(sc.contains("Cmd") || sc.contains("Ctrl") || sc.contains("Alt") || sc.starts_with('F')) {
return None;
}
Accelerator::from_str(&sc.replace("Cmd", "CMD").replace("Alt", "ALT").replace("Shift", "SHIFT").replace("Ctrl", "CTRL")).ok()
}
impl NativeMenu {
pub fn install(app: &mut DrawcraftApp) -> Self {
let menu = Menu::new();
let mut items = HashMap::new();
let mut counter = 0usize;
for (title, entries) in menus::menu_tree() {
let sub = Submenu::new(title, true);
build(app, &sub, &entries, &mut items, &mut counter);
let _ = menu.append(&sub);
}
menu.init_for_nsapp();
app.native_menu = true;
app.native_shortcuts =
items.values().filter(|(c, p, _)| p.is_null() && menus::shortcut_of(c).and_then(accel).is_some()).map(|(c, _, _)| c.clone()).collect();
Self { _menu: menu, items, last_refresh: 0.0 }
}
/// Dispatch clicked items and refresh state.
pub fn poll(&mut self, app: &mut DrawcraftApp) {
while let Ok(ev) = MenuEvent::receiver().try_recv() {
if let Some((cmd, params, _)) = self.items.get(ev.id.as_ref()) {
let p = if params.is_null() { serde_json::json!({}) } else { params.clone() };
menus::invoke(app, cmd, p);
}
}
let now = drawcraft_ui_egui::now_ms();
if now - self.last_refresh < 250.0 {
return;
}
self.last_refresh = now;
for (cmd, params, h) in self.items.values() {
let en = menus::enabled(app, cmd);
match h {
Handle::Plain(i) => {
i.set_enabled(en);
if matches!(
cmd.as_str(),
"edit.undo"
| "edit.redo"
| "view.outline"
| "view.rulers"
| "view.guides"
| "view.grid"
| "view.edges"
| "view.artboards"
| "view.boundingBox"
| "view.transparencyGrid"
) {
i.set_text(menus::dynamic_label(app, cmd, ""));
}
}
Handle::Check(c) => {
c.set_enabled(en);
c.set_checked(menus::checked(app, cmd, params).unwrap_or(false));
}
}
}
}
}
fn build(app: &DrawcraftApp, parent: &Submenu, entries: &[Item], items: &mut HashMap<String, (String, Value, Handle)>, counter: &mut usize) {
for e in entries {
match e {
Item::Sep => {
let _ = parent.append(&PredefinedMenuItem::separator());
}
Item::Header(h) => {
let _ = parent.append(&MenuItem::new(*h, false, None));
}
Item::Todo(label, sc) => {
let _ = parent.append(&MenuItem::new(*label, false, accel(sc)));
}
Item::Sub(label, children) => {
let sub = Submenu::new(*label, true);
build(app, &sub, children, items, counter);
let _ = parent.append(&sub);
}
Item::Cmd(label, cmd, params) => {
*counter += 1;
let id = format!("dc{counter}");
let sc = if params.is_null() { menus::shortcut_of(cmd).and_then(accel) } else { None };
let handle = if menus::checked(app, cmd, params).is_some() {
let c = CheckMenuItem::with_id(id.clone(), *label, true, false, sc);
let _ = parent.append(&c);
Handle::Check(c)
} else {
let i = MenuItem::with_id(id.clone(), *label, true, sc);
let _ = parent.append(&i);
Handle::Plain(i)
};
items.insert(id, (cmd.to_string(), params.clone(), handle));
}
}
}
}