use crate::error::Result;
#[derive(Debug, Clone)]
#[must_use = "Menu must be installed with install_menus()"]
pub struct Menu {
pub title: String,
pub items: Vec<MenuItem>,
}
#[derive(Debug, Clone)]
#[must_use = "MenuItem must be added to a Menu"]
pub struct MenuItem {
pub title: String,
pub shortcut: Option<String>,
pub separator: bool,
pub callback_id: Option<usize>,
}
impl Menu {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
items: Vec::new(),
}
}
pub fn item(mut self, item: MenuItem) -> Self {
self.items.push(item);
self
}
pub fn separator(mut self) -> Self {
self.items.push(MenuItem::separator());
self
}
}
impl MenuItem {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
shortcut: None,
separator: false,
callback_id: None,
}
}
pub fn shortcut(mut self, key: impl Into<String>) -> Self {
self.shortcut = Some(key.into());
self
}
pub fn on_action(mut self, callback_id: usize) -> Self {
self.callback_id = Some(callback_id);
self
}
pub fn separator() -> Self {
Self {
title: String::new(),
shortcut: None,
separator: true,
callback_id: None,
}
}
}
#[cfg(not(test))]
pub fn install_menus(menus: &[Menu]) -> Result<()> {
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
unsafe {
let ns_app: *mut Object = msg_send![
Class::get("NSApplication").ok_or("NSApplication not found")?,
sharedApplication
];
let menu_bar: *mut Object = msg_send![Class::get("NSMenu").ok_or("NSMenu not found")?, new];
for menu in menus {
let title_cstr = std::ffi::CString::new(menu.title.as_str())?;
let title_ns: *mut Object =
msg_send![objc::class!(NSString), stringWithUTF8String: title_cstr.as_ptr()];
let sub_menu: *mut Object = msg_send![objc::class!(NSMenu), alloc];
let sub_menu: *mut Object = msg_send![sub_menu, initWithTitle: title_ns];
for item in &menu.items {
if item.separator {
let sep: *mut Object = msg_send![objc::class!(NSMenuItem), separatorItem];
let _: () = msg_send![sub_menu, addItem: sep];
} else {
let item_title = std::ffi::CString::new(item.title.as_str())?;
let item_ns: *mut Object = msg_send![objc::class!(NSString), stringWithUTF8String: item_title.as_ptr()];
let key = item.shortcut.as_deref().unwrap_or("");
let key_cstr = std::ffi::CString::new(key)?;
let key_ns: *mut Object =
msg_send![objc::class!(NSString), stringWithUTF8String: key_cstr.as_ptr()];
let mi: *mut Object = msg_send![objc::class!(NSMenuItem), alloc];
if let Some(cb_id) = item.callback_id {
let handler = crate::event::action_handler();
let action_sel = objc::sel!(handleAction:);
let mi: *mut Object = msg_send![mi, initWithTitle: item_ns action: action_sel keyEquivalent: key_ns];
let _: () = msg_send![mi, setTarget: handler];
let _: () = msg_send![mi, setTag: cb_id as isize];
crate::event::map_tag(cb_id as isize, cb_id);
let _: () = msg_send![sub_menu, addItem: mi];
} else {
let mi: *mut Object = msg_send![mi, initWithTitle: item_ns action: std::ptr::null::<objc::runtime::Sel>() keyEquivalent: key_ns];
let _: () = msg_send![sub_menu, addItem: mi];
}
}
}
let bar_item: *mut Object = msg_send![objc::class!(NSMenuItem), new];
let _: () = msg_send![bar_item, setSubmenu: sub_menu];
let _: () = msg_send![menu_bar, addItem: bar_item];
}
let _: () = msg_send![ns_app, setMainMenu: menu_bar];
}
Ok(())
}
#[cfg(test)]
pub fn install_menus(_menus: &[Menu]) -> Result<()> {
Ok(())
}
pub fn default_menus(app_name: &str) -> Vec<Menu> {
vec![
Menu::new(app_name)
.item(MenuItem::new(format!("About {}", app_name)))
.separator()
.item(MenuItem::new(format!("Quit {}", app_name)).shortcut("q")),
Menu::new("Edit")
.item(MenuItem::new("Undo").shortcut("z"))
.item(MenuItem::new("Redo").shortcut("Z"))
.separator()
.item(MenuItem::new("Cut").shortcut("x"))
.item(MenuItem::new("Copy").shortcut("c"))
.item(MenuItem::new("Paste").shortcut("v"))
.item(MenuItem::new("Select All").shortcut("a")),
Menu::new("Window")
.item(MenuItem::new("Minimize").shortcut("m"))
.item(MenuItem::new("Zoom")),
Menu::new("Help").item(MenuItem::new(format!("{} Help", app_name))),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_menu_creation() {
let m = Menu::new("File")
.item(MenuItem::new("Open").shortcut("o"))
.separator()
.item(MenuItem::new("Quit").shortcut("q"));
assert_eq!(m.title, "File");
assert_eq!(m.items.len(), 3);
assert!(!m.items[0].separator);
assert!(m.items[1].separator);
assert_eq!(m.items[2].shortcut.as_deref(), Some("q"));
}
#[test]
fn test_default_menus() {
let menus = default_menus("TestApp");
assert_eq!(menus.len(), 4);
assert_eq!(menus[0].title, "TestApp");
assert_eq!(menus[1].title, "Edit");
}
#[test]
fn menu_item_on_action() {
let mi = MenuItem::new("Save").shortcut("s").on_action(404);
assert_eq!(mi.callback_id, Some(404));
}
#[test]
fn install_menus_mock_succeeds() {
let menus = vec![Menu::new("File").item(MenuItem::new("Close"))];
assert!(install_menus(&menus).is_ok());
}
}