mod actions;
mod bridge;
pub mod egui_menu;
pub mod model;
#[cfg(target_os = "macos")]
pub(super) mod macos;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(super) mod linux;
pub use actions::MenuAction;
pub use bridge::{dispatch, drain_pending_actions, request_toggle};
pub use egui_menu::AppMenuUi;
use crate::profile::Profile;
use anyhow::{Result, anyhow};
use model::MenuEntry;
use muda::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu};
use std::collections::HashMap;
use std::sync::Arc;
use winit::window::Window;
pub struct MenuManager {
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
menu: Menu,
action_map: HashMap<MenuId, MenuAction>,
profiles_submenu: Submenu,
profile_menu_items: Vec<MenuItem>,
}
impl MenuManager {
pub fn new() -> Result<Self> {
let menu = Menu::new();
let mut action_map = HashMap::new();
let mut profiles_submenu = None;
#[cfg(target_os = "macos")]
macos::build_app_menu(&menu, &mut action_map)?;
for section in model::platform_menu_model() {
#[cfg(target_os = "macos")]
if section.title == model::HELP_SECTION_TITLE {
macos::build_window_menu(&menu, &mut action_map)?;
}
let submenu = Submenu::new(section.title, true);
for entry in §ion.entries {
match entry {
MenuEntry::Separator => {
submenu.append(&PredefinedMenuItem::separator())?;
}
MenuEntry::Item(spec) => {
let item = MenuItem::with_id(spec.id, spec.label, true, spec.accelerator);
action_map.insert(item.id().clone(), spec.action);
submenu.append(&item)?;
}
MenuEntry::Profiles => {
profiles_submenu = Some(submenu.clone());
}
}
}
menu.append(&submenu)?;
}
let profiles_submenu = profiles_submenu
.ok_or_else(|| anyhow!("menu model has no profiles insertion point"))?;
Ok(Self {
menu,
action_map,
profiles_submenu,
profile_menu_items: Vec::new(),
})
}
pub fn init_global(&self) -> Result<()> {
#[cfg(target_os = "macos")]
{
macos::init_for_nsapp(&self.menu)
}
#[cfg(not(target_os = "macos"))]
{
Ok(())
}
}
#[allow(unused_variables)] pub fn init_for_window(&self, window: &Arc<Window>) -> Result<()> {
#[cfg(target_os = "macos")]
{
macos::init_for_nsapp(&self.menu)
}
#[cfg(target_os = "windows")]
{
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
if let Ok(handle) = window.window_handle()
&& let RawWindowHandle::Win32(win32_handle) = handle.as_raw()
{
unsafe {
self.menu.init_for_hwnd(win32_handle.hwnd.get() as _)?;
}
log::info!("Initialized Windows menu bar for window");
}
return Ok(());
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
{
linux::init_for_window(window)
}
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
{
log::warn!("Menu bar not supported on this platform");
Ok(())
}
}
pub fn poll_events(&self) -> impl Iterator<Item = MenuAction> + '_ {
std::iter::from_fn(|| {
match MenuEvent::receiver().try_recv() {
Ok(event) => self.action_map.get(&event.id).copied(),
Err(_) => None,
}
})
}
pub fn update_profiles(&mut self, profiles: &[&Profile]) {
for item in self.profile_menu_items.drain(..) {
self.action_map.remove(item.id());
let _ = self.profiles_submenu.remove(&item);
}
for entry in model::profile_entries(profiles.iter().copied()) {
let item = MenuItem::with_id(entry.menu_id, &entry.label, true, None);
self.action_map.insert(item.id().clone(), entry.action);
if let Err(e) = self.profiles_submenu.append(&item) {
log::warn!("Failed to add profile menu item '{}': {}", entry.label, e);
continue;
}
self.profile_menu_items.push(item);
}
log::info!("Updated profiles menu with {} items", profiles.len());
}
pub fn update_profiles_from_manager(&mut self, manager: &crate::profile::ProfileManager) {
let profiles: Vec<&Profile> = manager.profiles_ordered();
self.update_profiles(&profiles);
}
}