pub enum MenuItem {
Action {
label: String,
shortcut: Option<String>,
action: Box<dyn Fn() + Send + Sync>,
},
Separator,
Submenu {
label: String,
menu: Menu,
},
}
impl std::fmt::Debug for MenuItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MenuItem::Action {
label, shortcut, ..
} => f
.debug_struct("MenuItem::Action")
.field("label", label)
.field("shortcut", shortcut)
.finish(),
MenuItem::Separator => write!(f, "MenuItem::Separator"),
MenuItem::Submenu { label, menu } => f
.debug_struct("MenuItem::Submenu")
.field("label", label)
.field("menu", menu)
.finish(),
}
}
}
#[derive(Debug)]
pub struct Menu {
label: String,
items: Vec<MenuItem>,
}
impl Menu {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
items: Vec::new(),
}
}
pub fn label(&self) -> &str {
&self.label
}
pub fn items(&self) -> &[MenuItem] {
&self.items
}
pub fn item(
&mut self,
label: impl Into<String>,
shortcut: Option<&str>,
action: impl Fn() + Send + Sync + 'static,
) -> &mut Self {
self.items.push(MenuItem::Action {
label: label.into(),
shortcut: shortcut.map(|s| s.to_string()),
action: Box::new(action),
});
self
}
pub fn separator(&mut self) -> &mut Self {
self.items.push(MenuItem::Separator);
self
}
pub fn submenu<F>(&mut self, label: impl Into<String>, build: F) -> &mut Self
where
F: FnOnce(&mut Menu),
{
let mut sub = Menu::new(label);
build(&mut sub);
self.items.push(MenuItem::Submenu {
label: sub.label.clone(),
menu: sub,
});
self
}
}
#[derive(Debug)]
pub struct MenuBar {
menus: Vec<Menu>,
}
impl MenuBar {
pub fn build<F>(build: F) -> Self
where
F: FnOnce(&mut MenuBarBuilder),
{
let mut builder = MenuBarBuilder { menus: Vec::new() };
build(&mut builder);
MenuBar {
menus: builder.menus,
}
}
pub fn menus(&self) -> &[Menu] {
&self.menus
}
pub fn menu_count(&self) -> usize {
self.menus.len()
}
pub fn find_menu(&self, label: &str) -> Option<&Menu> {
self.menus.iter().find(|m| m.label() == label)
}
}
pub struct MenuBarBuilder {
menus: Vec<Menu>,
}
impl MenuBarBuilder {
pub fn menu<F>(&mut self, label: impl Into<String>, build: F) -> &mut Self
where
F: FnOnce(&mut Menu),
{
let mut menu = Menu::new(label);
build(&mut menu);
self.menus.push(menu);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_empty_menu_bar() {
let bar = MenuBar::build(|_mb| {});
assert_eq!(bar.menu_count(), 0);
assert!(bar.menus().is_empty());
}
#[test]
fn build_single_menu() {
let bar = MenuBar::build(|mb| {
mb.menu("File", |m| {
m.item("Quit", None, || {});
});
});
assert_eq!(bar.menu_count(), 1);
let file = &bar.menus()[0];
assert_eq!(file.label(), "File");
assert_eq!(file.items().len(), 1);
}
#[test]
fn build_multiple_menus() {
let bar = MenuBar::build(|mb| {
mb.menu("File", |m| {
m.item("Open", Some("Ctrl+O"), || {});
m.item("Quit", Some("Ctrl+Q"), || {});
});
mb.menu("Edit", |m| {
m.item("Undo", Some("Ctrl+Z"), || {});
});
mb.menu("Help", |m| {
m.item("About", None, || {});
});
});
assert_eq!(bar.menu_count(), 3);
assert_eq!(bar.menus()[0].label(), "File");
assert_eq!(bar.menus()[1].label(), "Edit");
assert_eq!(bar.menus()[2].label(), "Help");
}
#[test]
fn item_shortcut_stored() {
let bar = MenuBar::build(|mb| {
mb.menu("File", |m| {
m.item("Save", Some("Ctrl+S"), || {});
});
});
let item = &bar.menus()[0].items()[0];
if let MenuItem::Action { shortcut, .. } = item {
assert_eq!(shortcut.as_deref(), Some("Ctrl+S"));
} else {
panic!("expected Action item");
}
}
#[test]
fn separator_item() {
let bar = MenuBar::build(|mb| {
mb.menu("File", |m| {
m.item("New", None, || {});
m.separator();
m.item("Quit", None, || {});
});
});
let items = bar.menus()[0].items();
assert_eq!(items.len(), 3);
assert!(matches!(items[1], MenuItem::Separator));
}
#[test]
fn nested_submenu() {
let bar = MenuBar::build(|mb| {
mb.menu("View", |m| {
m.submenu("Theme", |sub| {
sub.item("Dark", None, || {});
sub.item("Light", None, || {});
});
});
});
let items = bar.menus()[0].items();
assert_eq!(items.len(), 1);
if let MenuItem::Submenu { label, menu } = &items[0] {
assert_eq!(label, "Theme");
assert_eq!(menu.items().len(), 2);
} else {
panic!("expected Submenu item");
}
}
#[test]
fn find_menu_by_label() {
let bar = MenuBar::build(|mb| {
mb.menu("File", |m| {
m.item("Quit", None, || {});
});
mb.menu("Help", |m| {
m.item("About", None, || {});
});
});
assert!(bar.find_menu("File").is_some());
assert!(bar.find_menu("Help").is_some());
assert!(bar.find_menu("Missing").is_none());
}
#[test]
fn action_callback_is_callable() {
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0usize));
let c = counter.clone();
let bar = MenuBar::build(|mb| {
mb.menu("File", move |m| {
let c2 = c.clone();
m.item("Click", None, move || {
*c2.lock().unwrap() += 1;
});
});
});
if let MenuItem::Action { action, .. } = &bar.menus()[0].items()[0] {
action();
}
assert_eq!(*counter.lock().unwrap(), 1);
}
}