use std::cell::RefCell;
use std::rc::Rc;
use crate::platform::menu::{NativeMenu, NativeMenuBar, NativeMenuItem};
use crate::platform::{KeyCode, Modifiers};
pub use crate::platform::menu::{Shortcut, mnemonic};
pub(crate) type MenuAction<S> = Rc<RefCell<dyn FnMut(&mut S)>>;
pub struct MenuItem<S> {
pub(crate) kind: ItemKind<S>,
}
pub(crate) enum ItemKind<S> {
Action {
label: String,
action: MenuAction<S>,
shortcut: Option<Shortcut>,
enabled: bool,
checked: Option<bool>,
},
Separator,
Submenu(Menu<S>),
}
impl<S> MenuItem<S> {
pub fn new(label: impl Into<String>, action: impl FnMut(&mut S) + 'static) -> Self {
Self {
kind: ItemKind::Action {
label: label.into(),
action: Rc::new(RefCell::new(action)),
shortcut: None,
enabled: true,
checked: None,
},
}
}
pub fn separator() -> Self {
Self {
kind: ItemKind::Separator,
}
}
pub fn submenu(menu: Menu<S>) -> Self {
Self {
kind: ItemKind::Submenu(menu),
}
}
pub fn shortcut(mut self, shortcut: Shortcut) -> Self {
if let ItemKind::Action { shortcut: s, .. } = &mut self.kind {
*s = Some(shortcut);
}
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
if let ItemKind::Action { enabled: e, .. } = &mut self.kind {
*e = enabled;
}
self
}
pub fn checked(mut self, checked: bool) -> Self {
if let ItemKind::Action { checked: c, .. } = &mut self.kind {
*c = Some(checked);
}
self
}
pub fn label(&self) -> &str {
match &self.kind {
ItemKind::Action { label, .. } => label,
ItemKind::Submenu(menu) => &menu.label,
ItemKind::Separator => "",
}
}
pub fn is_separator(&self) -> bool {
matches!(self.kind, ItemKind::Separator)
}
pub fn is_enabled(&self) -> bool {
match &self.kind {
ItemKind::Action { enabled, .. } => *enabled,
ItemKind::Submenu(_) => true,
ItemKind::Separator => false,
}
}
pub fn is_checked(&self) -> Option<bool> {
match &self.kind {
ItemKind::Action { checked, .. } => *checked,
_ => None,
}
}
pub fn get_shortcut(&self) -> Option<Shortcut> {
match &self.kind {
ItemKind::Action { shortcut, .. } => *shortcut,
_ => None,
}
}
pub fn get_submenu(&self) -> Option<&Menu<S>> {
match &self.kind {
ItemKind::Submenu(menu) => Some(menu),
_ => None,
}
}
pub fn activate(&self, state: &mut S) -> bool {
match &self.kind {
ItemKind::Action {
action,
enabled: true,
..
} => {
(action.borrow_mut())(state);
true
}
_ => false,
}
}
pub(crate) fn action(&self) -> Option<MenuAction<S>> {
match &self.kind {
ItemKind::Action {
action,
enabled: true,
..
} => Some(action.clone()),
_ => None,
}
}
}
impl<S> std::fmt::Debug for MenuItem<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
ItemKind::Action {
label,
shortcut,
enabled,
checked,
..
} => f
.debug_struct("MenuItem")
.field("label", label)
.field("shortcut", shortcut)
.field("enabled", enabled)
.field("checked", checked)
.finish_non_exhaustive(),
ItemKind::Separator => f.write_str("Separator"),
ItemKind::Submenu(menu) => f.debug_tuple("Submenu").field(menu).finish(),
}
}
}
pub struct Menu<S> {
pub(crate) label: String,
pub(crate) items: Vec<MenuItem<S>>,
}
impl<S> Menu<S> {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
items: Vec::new(),
}
}
pub fn item(mut self, item: MenuItem<S>) -> Self {
self.items.push(item);
self
}
pub fn separator(self) -> Self {
self.item(MenuItem::separator())
}
pub fn submenu(self, menu: Menu<S>) -> Self {
self.item(MenuItem::submenu(menu))
}
pub fn label(&self) -> &str {
&self.label
}
pub fn items(&self) -> &[MenuItem<S>] {
&self.items
}
pub fn item_at(&self, path: &[usize]) -> Option<&MenuItem<S>> {
let (&first, rest) = path.split_first()?;
let item = self.items.get(first)?;
if rest.is_empty() {
Some(item)
} else {
item.get_submenu()?.item_at(rest)
}
}
pub fn find_shortcut(&self, key: KeyCode, modifiers: Modifiers) -> Option<&MenuItem<S>> {
self.items.iter().find_map(|item| match &item.kind {
ItemKind::Action {
shortcut: Some(s),
enabled: true,
..
} if s.matches(key, modifiers) => Some(item),
ItemKind::Submenu(menu) => menu.find_shortcut(key, modifiers),
_ => None,
})
}
pub fn commands(&self) -> Vec<(u32, &MenuItem<S>)> {
let mut out = Vec::new();
self.number(&mut 1, &mut out);
out
}
pub fn command(&self, id: u32) -> Option<&MenuItem<S>> {
self.commands()
.into_iter()
.find_map(|(i, item)| (i == id).then_some(item))
}
pub fn to_native(&self) -> NativeMenu {
self.native_from(&mut 1)
}
fn number<'a>(&'a self, next: &mut u32, out: &mut Vec<(u32, &'a MenuItem<S>)>) {
for item in &self.items {
match &item.kind {
ItemKind::Action { .. } => {
out.push((*next, item));
*next += 1;
}
ItemKind::Submenu(menu) => menu.number(next, out),
ItemKind::Separator => {}
}
}
}
fn native_from(&self, next: &mut u32) -> NativeMenu {
let items = self
.items
.iter()
.map(|item| match &item.kind {
ItemKind::Action {
label,
shortcut,
enabled,
checked,
..
} => {
let id = *next;
*next += 1;
NativeMenuItem::Action {
id,
label: label.clone(),
shortcut: *shortcut,
enabled: *enabled,
checked: *checked,
}
}
ItemKind::Separator => NativeMenuItem::Separator,
ItemKind::Submenu(menu) => NativeMenuItem::Submenu(menu.native_from(next)),
})
.collect();
NativeMenu {
label: self.label.clone(),
items,
}
}
}
impl<S> std::fmt::Debug for Menu<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Menu")
.field("label", &self.label)
.field("items", &self.items)
.finish()
}
}
pub struct MenuBar<S> {
pub(crate) menus: Vec<Menu<S>>,
}
impl<S> Default for MenuBar<S> {
fn default() -> Self {
Self { menus: Vec::new() }
}
}
impl<S> MenuBar<S> {
pub fn new() -> Self {
Self::default()
}
pub fn menu(mut self, menu: Menu<S>) -> Self {
self.menus.push(menu);
self
}
pub fn menus(&self) -> &[Menu<S>] {
&self.menus
}
pub fn commands(&self) -> Vec<(u32, &MenuItem<S>)> {
let mut out = Vec::new();
let mut next = 1;
for menu in &self.menus {
menu.number(&mut next, &mut out);
}
out
}
pub fn command(&self, id: u32) -> Option<&MenuItem<S>> {
self.commands()
.into_iter()
.find_map(|(i, item)| (i == id).then_some(item))
}
pub fn activate(&self, id: u32, state: &mut S) -> bool {
self.command(id).is_some_and(|item| item.activate(state))
}
pub fn find_shortcut(&self, key: KeyCode, modifiers: Modifiers) -> Option<&MenuItem<S>> {
self.menus
.iter()
.find_map(|m| m.find_shortcut(key, modifiers))
}
pub fn to_native(&self, app_name: &str) -> NativeMenuBar {
let mut next = 1;
NativeMenuBar {
app_name: app_name.to_string(),
menus: self
.menus
.iter()
.map(|m| m.native_from(&mut next))
.collect(),
}
}
}
impl<S> std::fmt::Debug for MenuBar<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MenuBar")
.field("menus", &self.menus)
.finish()
}
}
pub struct Tray<S> {
tooltip: String,
icon: Option<crate::render::Pixmap>,
menu: Option<Menu<S>>,
on_activate: Option<MenuAction<S>>,
}
impl<S> Tray<S> {
pub fn new(tooltip: impl Into<String>) -> Self {
Self {
tooltip: tooltip.into(),
icon: None,
menu: None,
on_activate: None,
}
}
pub fn icon(mut self, icon: crate::render::Pixmap) -> Self {
self.icon = Some(icon);
self
}
pub fn menu(mut self, menu: Menu<S>) -> Self {
self.menu = Some(menu);
self
}
pub fn on_activate(mut self, action: impl FnMut(&mut S) + 'static) -> Self {
self.on_activate = Some(Rc::new(RefCell::new(action)));
self
}
pub fn to_native(&self) -> crate::platform::NativeTray {
fn strip(items: &mut [NativeMenuItem]) {
for item in items {
match item {
NativeMenuItem::Action { shortcut, .. } => *shortcut = None,
NativeMenuItem::Submenu(m) => strip(&mut m.items),
NativeMenuItem::Separator => {}
}
}
}
let mut items = match &self.menu {
Some(menu) => {
let mut next = crate::platform::TRAY_ID_BASE;
menu.native_from(&mut next).items
}
None => Vec::new(),
};
strip(&mut items);
crate::platform::NativeTray {
tooltip: self.tooltip.clone(),
icon: self.icon.clone(),
menu: items,
}
}
pub fn activate_command(&self, id: u32, state: &mut S) -> bool {
let Some(menu) = &self.menu else {
return false;
};
let mut out = Vec::new();
let mut next = crate::platform::TRAY_ID_BASE;
menu.number(&mut next, &mut out);
out.into_iter()
.find(|(i, _)| *i == id)
.is_some_and(|(_, item)| item.activate(state))
}
pub fn activate(&self, state: &mut S) -> bool {
match &self.on_activate {
Some(action) => {
(action.borrow_mut())(state);
true
}
None => false,
}
}
}
impl<S> std::fmt::Debug for Tray<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tray")
.field("tooltip", &self.tooltip)
.field("icon", &self.icon.as_ref().map(|i| i.size()))
.field("menu", &self.menu)
.field("on_activate", &self.on_activate.is_some())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tray_numbers_from_the_tray_base_without_shortcuts() {
let base = crate::platform::TRAY_ID_BASE;
let tray = Tray::new("T")
.menu(
Menu::new("")
.item(
MenuItem::new("a", |s: &mut Vec<&str>| s.push("a"))
.shortcut(Shortcut::cmd('a')),
)
.submenu(
Menu::new("More").item(MenuItem::new("b", |s: &mut Vec<&str>| s.push("b"))),
),
)
.on_activate(|s: &mut Vec<&str>| s.push("click"));
let native = tray.to_native();
assert_eq!(native.tooltip, "T");
match &native.menu[0] {
NativeMenuItem::Action { id, shortcut, .. } => {
assert_eq!((*id, *shortcut), (base, None))
}
other => panic!("{other:?}"),
}
match &native.menu[1] {
NativeMenuItem::Submenu(m) => match &m.items[0] {
NativeMenuItem::Action { id, .. } => assert_eq!(*id, base + 1),
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
}
let mut log = Vec::new();
assert!(tray.activate_command(base + 1, &mut log));
assert!(!tray.activate_command(1, &mut log));
assert!(tray.activate(&mut log));
assert_eq!(log, vec!["b", "click"]);
}
fn bar() -> MenuBar<Vec<&'static str>> {
MenuBar::new()
.menu(
Menu::new("&File")
.item(
MenuItem::new("&New", |s: &mut Vec<_>| s.push("new"))
.shortcut(Shortcut::cmd('n')),
)
.separator()
.submenu(
Menu::new("&Recent")
.item(MenuItem::new("one", |s: &mut Vec<_>| s.push("one")))
.item(
MenuItem::new("two", |s: &mut Vec<_>| s.push("two")).enabled(false),
),
)
.item(MenuItem::new("&Quit", |s: &mut Vec<_>| s.push("quit"))),
)
.menu(
Menu::new("&View").item(
MenuItem::new("&Wrap", |s: &mut Vec<_>| s.push("wrap"))
.checked(true)
.shortcut(Shortcut::key(KeyCode::F5)),
),
)
}
#[test]
fn ids_number_commands_depth_first() {
let b = bar();
let labels: Vec<(u32, &str)> = b
.commands()
.iter()
.map(|(i, it)| (*i, it.label()))
.collect();
assert_eq!(
labels,
vec![
(1, "&New"),
(2, "one"),
(3, "two"),
(4, "&Quit"),
(5, "&Wrap")
]
);
let native = b.to_native("Demo");
assert_eq!(native.menus.len(), 2);
match &native.menus[0].items[2] {
NativeMenuItem::Submenu(m) => match &m.items[1] {
NativeMenuItem::Action { id, enabled, .. } => {
assert_eq!((*id, *enabled), (3, false))
}
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
}
match &native.menus[1].items[0] {
NativeMenuItem::Action { id, checked, .. } => {
assert_eq!((*id, *checked), (5, Some(true)))
}
other => panic!("{other:?}"),
}
}
#[test]
fn a_menu_numbers_its_own_commands() {
let b = bar();
let file = &b.menus()[0];
let native = file.to_native();
assert_eq!(native.label, "&File");
match &native.items[2] {
NativeMenuItem::Submenu(m) => match &m.items[0] {
NativeMenuItem::Action { id, .. } => assert_eq!(*id, 2),
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
}
assert_eq!(file.command(2).map(|i| i.label()), Some("one"));
assert_eq!(file.command(4).map(|i| i.label()), Some("&Quit"));
assert!(file.command(5).is_none());
}
#[test]
fn activation_skips_disabled_commands() {
let b = bar();
let mut log = Vec::new();
assert!(b.activate(2, &mut log));
assert!(!b.activate(3, &mut log), "disabled");
assert!(!b.activate(99, &mut log));
assert_eq!(log, vec!["one"]);
assert_eq!(b.menus()[0].item_at(&[2, 0]).unwrap().label(), "one");
}
#[test]
fn shortcuts_find_enabled_commands() {
let b = bar();
let mut log = Vec::new();
let ctrl = Modifiers {
ctrl: !cfg!(target_os = "macos"),
meta: cfg!(target_os = "macos"),
..Modifiers::default()
};
b.find_shortcut(KeyCode::Char('n'), ctrl)
.unwrap()
.activate(&mut log);
b.find_shortcut(KeyCode::F5, Modifiers::default())
.unwrap()
.activate(&mut log);
assert!(
b.find_shortcut(KeyCode::Char('n'), Modifiers::default())
.is_none()
);
assert_eq!(log, vec!["new", "wrap"]);
}
}