158 lines
5.0 KiB
Rust
158 lines
5.0 KiB
Rust
use muda::Menu as MudaMenu;
|
|
use muda::accelerator::Accelerator;
|
|
use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu};
|
|
|
|
use crate::event::{AppEvent, AppEventScheduler};
|
|
use crate::wrapper::messages::MenuItem as WrapperMenuItem;
|
|
|
|
pub(super) struct Menu {
|
|
inner: MudaMenu,
|
|
}
|
|
|
|
impl Menu {
|
|
pub(super) fn new(event_scheduler: AppEventScheduler) -> Self {
|
|
// TODO: Remove as much app submenu special handling as possible
|
|
let app_submenu = Submenu::with_items("", true, &[]).unwrap();
|
|
|
|
let menu = MudaMenu::new();
|
|
menu.prepend(&app_submenu).unwrap();
|
|
|
|
menu.init_for_nsapp();
|
|
|
|
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
|
|
let mtm = objc2::MainThreadMarker::new().expect("only ever called from main thread");
|
|
let is_shortcut_triggered = objc2_app_kit::NSApplication::sharedApplication(mtm)
|
|
.mainMenu()
|
|
.map(|m| m.highlightedItem().is_some())
|
|
.unwrap_or_default();
|
|
if is_shortcut_triggered {
|
|
tracing::error!("A keyboard input triggered a menu event. This is most likely a bug. Please report!");
|
|
return;
|
|
}
|
|
|
|
if let Some(id) = menu_id_to_u64(event.id()) {
|
|
event_scheduler.schedule(AppEvent::MenuEvent { id });
|
|
}
|
|
}));
|
|
|
|
Menu { inner: menu }
|
|
}
|
|
|
|
pub(super) fn update(&self, entries: Vec<WrapperMenuItem>) {
|
|
let new_entries = menu_items_from_wrapper(entries);
|
|
let existing_entries = self.inner.items();
|
|
|
|
let mut new_entries_iter = new_entries.iter();
|
|
let mut existing_entries_iter = existing_entries.iter().skip(1); // Skip first menu (app menu)
|
|
|
|
let incremental_update_ok = std::iter::from_fn(move || match (existing_entries_iter.next(), new_entries_iter.next()) {
|
|
(Some(MenuItemKind::Submenu(old)), Some(MenuItemKind::Submenu(new))) if old.text() == new.text() => {
|
|
replace_children(old, 0, new.items());
|
|
Some(true)
|
|
}
|
|
(None, None) => None,
|
|
_ => Some(false),
|
|
})
|
|
.all(|b| b);
|
|
|
|
if !incremental_update_ok {
|
|
// Fallback to full replace
|
|
replace_children(&self.inner, 1, new_entries); // Skip first menu (app menu)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn menu_items_from_wrapper(entries: Vec<WrapperMenuItem>) -> Vec<MenuItemKind> {
|
|
let mut menu_items: Vec<MenuItemKind> = Vec::new();
|
|
for entry in entries {
|
|
match entry {
|
|
WrapperMenuItem::Action { id, text, enabled, shortcut } => {
|
|
let id = u64_to_menu_id(id);
|
|
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
|
|
let item = MenuItem::with_id(id, text, enabled, accelerator);
|
|
menu_items.push(MenuItemKind::MenuItem(item));
|
|
}
|
|
WrapperMenuItem::Checkbox { id, text, enabled, shortcut, checked } => {
|
|
let id = u64_to_menu_id(id);
|
|
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
|
|
let check = CheckMenuItem::with_id(id, text, enabled, checked, accelerator);
|
|
menu_items.push(MenuItemKind::Check(check));
|
|
}
|
|
WrapperMenuItem::SubMenu { text: name, items, .. } => {
|
|
let items = menu_items_from_wrapper(items);
|
|
let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::<Vec<&dyn IsMenuItem>>();
|
|
let submenu = Submenu::with_items(name, true, &items).unwrap();
|
|
menu_items.push(MenuItemKind::Submenu(submenu));
|
|
}
|
|
WrapperMenuItem::Separator => {
|
|
let separator = PredefinedMenuItem::separator();
|
|
menu_items.push(MenuItemKind::Predefined(separator));
|
|
}
|
|
}
|
|
}
|
|
menu_items
|
|
}
|
|
|
|
fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem {
|
|
match item {
|
|
MenuItemKind::MenuItem(i) => i,
|
|
MenuItemKind::Submenu(i) => i,
|
|
MenuItemKind::Predefined(i) => i,
|
|
MenuItemKind::Check(i) => i,
|
|
MenuItemKind::Icon(i) => i,
|
|
}
|
|
}
|
|
|
|
fn u64_to_menu_id(id: u64) -> String {
|
|
format!("{id:08x}")
|
|
}
|
|
|
|
fn menu_id_to_u64(id: &MenuId) -> Option<u64> {
|
|
u64::from_str_radix(&id.0, 16).ok()
|
|
}
|
|
|
|
fn replace_children<'a, T: Into<MenuContainer<'a>>>(menu: T, skip: usize, new_items: Vec<MenuItemKind>) {
|
|
let menu: MenuContainer = menu.into();
|
|
let items = menu.items();
|
|
for item in items.iter().skip(skip) {
|
|
menu.remove(menu_item_kind_to_dyn(item)).unwrap();
|
|
}
|
|
let items = new_items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::<Vec<&dyn IsMenuItem>>();
|
|
menu.append_items(items.as_ref()).unwrap();
|
|
}
|
|
|
|
enum MenuContainer<'a> {
|
|
Menu(&'a MudaMenu),
|
|
Submenu(&'a Submenu),
|
|
}
|
|
impl<'a> MenuContainer<'a> {
|
|
fn items(&self) -> Vec<MenuItemKind> {
|
|
match self {
|
|
MenuContainer::Menu(menu) => menu.items(),
|
|
MenuContainer::Submenu(submenu) => submenu.items(),
|
|
}
|
|
}
|
|
fn remove(&self, item: &dyn IsMenuItem) -> Result<()> {
|
|
match self {
|
|
MenuContainer::Menu(menu) => menu.remove(item),
|
|
MenuContainer::Submenu(submenu) => submenu.remove(item),
|
|
}
|
|
}
|
|
fn append_items(&self, items: &[&dyn IsMenuItem]) -> Result<()> {
|
|
match self {
|
|
MenuContainer::Menu(menu) => menu.append_items(items),
|
|
MenuContainer::Submenu(submenu) => submenu.append_items(items),
|
|
}
|
|
}
|
|
}
|
|
impl<'a> From<&'a MudaMenu> for MenuContainer<'a> {
|
|
fn from(menu: &'a MudaMenu) -> Self {
|
|
MenuContainer::Menu(menu)
|
|
}
|
|
}
|
|
impl<'a> From<&'a Submenu> for MenuContainer<'a> {
|
|
fn from(submenu: &'a Submenu) -> Self {
|
|
MenuContainer::Submenu(submenu)
|
|
}
|
|
}
|