#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuItem {
About,
Hide,
HideOthers,
ShowAll,
Quit,
Cut,
Copy,
Paste,
SelectAll,
Fullscreen,
Minimize,
CloseWindow,
Separator,
}
impl MenuItem {
#[cfg_attr(not(test), expect(dead_code))]
pub const fn accelerator(self) -> Option<&'static str> {
match self {
MenuItem::Hide => Some("Cmd+H"),
MenuItem::HideOthers => Some("Cmd+Alt+H"),
MenuItem::Quit => Some("Cmd+Q"),
MenuItem::Cut => Some("Cmd+X"),
MenuItem::Copy => Some("Cmd+C"),
MenuItem::Paste => Some("Cmd+V"),
MenuItem::SelectAll => Some("Cmd+A"),
MenuItem::Fullscreen => Some("Ctrl+Cmd+F"),
MenuItem::Minimize => Some("Cmd+M"),
MenuItem::CloseWindow => Some("Cmd+W"),
MenuItem::About | MenuItem::ShowAll | MenuItem::Separator => None,
}
}
#[cfg_attr(not(test), expect(dead_code))]
pub const fn is_separator(self) -> bool {
matches!(self, MenuItem::Separator)
}
}
pub struct MenuSection {
pub title: &'static str,
pub items: &'static [MenuItem],
}
use MenuItem::*;
pub const MENU_LAYOUT: &[MenuSection] = &[
MenuSection {
title: "TinyView",
items: &[About, Separator, Hide, HideOthers, ShowAll, Separator, Quit],
},
MenuSection {
title: "Edit",
items: &[Cut, Copy, Paste, SelectAll],
},
MenuSection {
title: "View",
items: &[Fullscreen],
},
MenuSection {
title: "Window",
items: &[Minimize, Separator, CloseWindow],
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn required_shortcuts_present_with_expected_accelerators() {
let expected = [
(Quit, "Cmd+Q"),
(Fullscreen, "Ctrl+Cmd+F"),
(CloseWindow, "Cmd+W"),
(Minimize, "Cmd+M"),
(Copy, "Cmd+C"),
(Paste, "Cmd+V"),
];
for (item, accel) in expected {
assert!(
layout_contains(item),
"{item:?} must appear in the menu layout"
);
assert_eq!(
item.accelerator(),
Some(accel),
"{item:?} should map to {accel}"
);
}
}
#[test]
fn first_section_is_app_menu_and_holds_quit() {
let app = &MENU_LAYOUT[0];
assert_eq!(app.title, "TinyView");
assert!(app.items.contains(&Quit), "Quit belongs in the app menu");
}
#[test]
fn separators_are_never_dangling_or_doubled() {
for section in MENU_LAYOUT {
let items = section.items;
assert!(
!items.first().is_some_and(|i| i.is_separator()),
"{}: must not start with a separator",
section.title
);
assert!(
!items.last().is_some_and(|i| i.is_separator()),
"{}: must not end with a separator",
section.title
);
for pair in items.windows(2) {
assert!(
!(pair[0].is_separator() && pair[1].is_separator()),
"{}: has two consecutive separators",
section.title
);
}
}
}
#[test]
fn accelerators_are_unique() {
let mut seen: Vec<&str> = Vec::new();
for section in MENU_LAYOUT {
for item in section.items {
if let Some(accel) = item.accelerator() {
assert!(
!seen.contains(&accel),
"accelerator {accel} is assigned to more than one item"
);
seen.push(accel);
}
}
}
}
#[test]
fn every_section_is_titled_and_non_empty() {
for section in MENU_LAYOUT {
assert!(!section.title.is_empty(), "section title must not be empty");
assert!(
!section.items.is_empty(),
"{}: section must have at least one item",
section.title
);
}
}
#[test]
fn separator_has_no_accelerator() {
assert!(Separator.is_separator());
assert_eq!(Separator.accelerator(), None);
}
#[test]
fn actionable_items_have_accelerators_except_about_and_show_all() {
for section in MENU_LAYOUT {
for &item in section.items {
if item.is_separator() {
continue;
}
let has_accel = item.accelerator().is_some();
let expected = !matches!(item, About | ShowAll);
assert_eq!(
has_accel, expected,
"{item:?}: accelerator presence does not match the spec"
);
}
}
}
fn layout_contains(target: MenuItem) -> bool {
MENU_LAYOUT
.iter()
.flat_map(|s| s.items.iter())
.any(|&i| i == target)
}
}