use super::actions::MenuAction;
use super::model::{self, MenuEntry, MenuItemSpec, MenuSection};
use super::state::MenuState;
use crate::profile::Profile;
use anyhow::Result;
use muda::{CheckMenuItem, IsMenuItem, Menu, MenuId, MenuItem, PredefinedMenuItem, Submenu};
use par_term_keybindings::KeybindingRegistry;
use std::collections::HashMap;
enum NativeItem {
Plain(MenuItem, MenuItemSpec),
Toggle(CheckMenuItem, MenuItemSpec),
}
struct DynamicSlot {
submenu: Submenu,
position: usize,
items: Vec<MenuItem>,
}
pub(super) struct NativeMenu {
pub(super) menu: Menu,
pub(super) action_map: HashMap<MenuId, MenuAction>,
items: Vec<NativeItem>,
submenus: Vec<(Submenu, super::state::Requires)>,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(super) window_menu: Option<Submenu>,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(super) help_menu: Option<Submenu>,
profiles: Option<DynamicSlot>,
arrangements: Option<DynamicSlot>,
registry: KeybindingRegistry,
captured: bool,
applied: Option<MenuState>,
}
impl NativeMenu {
pub(super) fn build(
sections: &[MenuSection],
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] quit_accelerator: Option<
muda::accelerator::Accelerator,
>,
registry: KeybindingRegistry,
captured: bool,
) -> Result<Self> {
let mut built = Self {
menu: Menu::new(),
action_map: HashMap::new(),
items: Vec::new(),
submenus: Vec::new(),
window_menu: None,
help_menu: None,
profiles: None,
arrangements: None,
registry,
captured,
applied: None,
};
#[cfg(target_os = "macos")]
super::macos::build_app_menu(&built.menu, &mut built.action_map, quit_accelerator)?;
for section in sections {
let submenu = Submenu::new(section.title, true);
built.append_entries(&submenu, §ion.entries)?;
built.menu.append(&submenu)?;
if section.title == model::WINDOW_SECTION_TITLE {
built.window_menu = Some(submenu);
} else if section.title == model::HELP_SECTION_TITLE {
built.help_menu = Some(submenu);
}
}
Ok(built)
}
fn append_entries(&mut self, parent: &Submenu, entries: &[MenuEntry]) -> Result<()> {
for entry in entries {
match entry {
MenuEntry::Separator => parent.append(&PredefinedMenuItem::separator())?,
MenuEntry::Item(spec) => self.append_item(parent, spec)?,
MenuEntry::Submenu(sub) => {
let child = Submenu::with_id(sub.id, sub.title, true);
self.append_entries(&child, &sub.entries)?;
parent.append(&child)?;
self.submenus.push((child, sub.requires));
}
MenuEntry::Profiles => {
self.profiles = Some(DynamicSlot {
submenu: parent.clone(),
position: parent.items().len(),
items: Vec::new(),
});
}
MenuEntry::Arrangements => {
self.arrangements = Some(DynamicSlot {
submenu: parent.clone(),
position: parent.items().len(),
items: Vec::new(),
});
}
MenuEntry::BringAllToFront => {
parent.append(&PredefinedMenuItem::bring_all_to_front(None))?;
}
}
}
Ok(())
}
fn append_item(&mut self, parent: &Submenu, spec: &MenuItemSpec) -> Result<()> {
if spec.check.is_some() {
let item = CheckMenuItem::with_id(spec.id, spec.label, true, false, spec.accelerator);
self.action_map.insert(item.id().clone(), spec.action);
parent.append(&item)?;
self.items.push(NativeItem::Toggle(item, spec.clone()));
} else {
let item = MenuItem::with_id(spec.id, spec.label, true, spec.accelerator);
self.action_map.insert(item.id().clone(), spec.action);
parent.append(&item)?;
self.items.push(NativeItem::Plain(item, spec.clone()));
}
Ok(())
}
pub(super) fn apply(&mut self, state: &MenuState) {
if self.applied.as_ref() == Some(state) {
return;
}
for item in &self.items {
match item {
NativeItem::Plain(item, spec) => {
item.set_enabled(state.enabled(spec));
if super::state::label_varies(spec.requires) {
item.set_text(state.label(spec));
}
}
NativeItem::Toggle(item, spec) => {
item.set_enabled(state.enabled(spec));
item.set_checked(spec.check.is_some_and(|c| state.checked(c)));
}
}
}
for (submenu, requires) in &self.submenus {
submenu.set_enabled(state.satisfies(*requires));
}
let dynamic_enabled = state.dynamic_enabled();
if let Some(slot) = &mut self.profiles {
for item in &slot.items {
item.set_enabled(dynamic_enabled);
}
}
let wanted = model::arrangement_entries_from(&state.arrangements);
if let Some(slot) = &mut self.arrangements {
fill_slot(slot, &mut self.action_map, &wanted, false, self.captured);
for item in &slot.items {
item.set_enabled(dynamic_enabled);
}
}
self.applied = Some(state.clone());
}
pub(super) fn invalidate(&mut self) {
self.applied = None;
}
pub(super) fn set_profiles(&mut self, profiles: &[&Profile]) {
let registry = &self.registry;
let wanted = model::profile_entries(profiles.iter().copied(), registry);
if let Some(slot) = &mut self.profiles {
fill_slot(slot, &mut self.action_map, &wanted, true, self.captured);
}
self.applied = None;
}
}
fn fill_slot(
slot: &mut DynamicSlot,
action_map: &mut HashMap<MenuId, MenuAction>,
wanted: &[model::DynamicEntry],
trailing_separator: bool,
captured: bool,
) {
let current: Vec<String> = slot.items.iter().map(|i| i.id().0.clone()).collect();
let target: Vec<String> = wanted.iter().map(|e| e.menu_id.clone()).collect();
let labels_match = slot
.items
.iter()
.zip(wanted)
.all(|(item, entry)| item.text() == entry.label);
if current == target && labels_match {
return;
}
let old_len = slot.items.len() + usize::from(trailing_separator && !slot.items.is_empty());
for item in slot.items.drain(..) {
action_map.remove(item.id());
}
for _ in 0..old_len {
let _ = slot.submenu.remove_at(slot.position);
}
let mut position = slot.position;
for entry in wanted {
let accelerator = if captured { None } else { entry.accelerator };
let item = MenuItem::with_id(entry.menu_id.as_str(), &entry.label, true, accelerator);
if let Err(e) = slot.submenu.insert(&item, position) {
log::warn!("menu: failed to add {:?}: {e}", entry.label);
continue;
}
action_map.insert(item.id().clone(), entry.action);
slot.items.push(item);
position += 1;
}
if trailing_separator && !slot.items.is_empty() {
let _ = slot.submenu.insert(
&PredefinedMenuItem::separator() as &dyn IsMenuItem,
position,
);
}
}