#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
mod accelerator_contract {
use super::*;
use crate::window::{VirtualKeyCode as K, VirtualKeyCodeVec};
fn keyboard(down: &[K], pressed: K) -> KeyboardState {
KeyboardState {
pressed_virtual_keycodes: VirtualKeyCodeVec::from_vec(down.to_vec()),
current_virtual_keycode: Some(pressed).into(),
..KeyboardState::default()
}
}
fn combo(keys: &[K]) -> VirtualKeyCodeCombo {
VirtualKeyCodeCombo {
keys: VirtualKeyCodeVec::from_vec(keys.to_vec()),
}
}
fn primary() -> K {
if cfg!(target_os = "macos") {
K::LWin
} else {
K::LControl
}
}
#[test]
fn a_primary_chord_matches_the_platforms_primary_modifier_from_one_definition() {
let save = combo(&[K::LWin, K::S]);
assert!(accelerator_matches(
&save,
&keyboard(&[primary(), K::S], K::S),
K::S
));
assert!(!accelerator_matches(&save, &keyboard(&[K::S], K::S), K::S));
assert!(!accelerator_matches(
&save,
&keyboard(&[primary(), K::O], K::O),
K::O
));
}
#[test]
fn the_match_is_exact_so_ctrl_s_and_ctrl_shift_s_coexist() {
let save = combo(&[K::LWin, K::S]);
let save_as = combo(&[K::LWin, K::LShift, K::S]);
let ctrl_shift_s = keyboard(&[primary(), K::LShift, K::S], K::S);
assert!(
!accelerator_matches(&save, &ctrl_shift_s, K::S),
"Shift held: not plain Ctrl+S"
);
assert!(accelerator_matches(&save_as, &ctrl_shift_s, K::S));
let ctrl_s = keyboard(&[primary(), K::S], K::S);
assert!(accelerator_matches(&save, &ctrl_s, K::S));
assert!(
!accelerator_matches(&save_as, &ctrl_s, K::S),
"Shift missing: not Ctrl+Shift+S"
);
assert!(accelerator_matches(
&combo(&[K::RShift, K::LWin, K::S]),
&ctrl_shift_s,
K::S
));
assert!(!accelerator_matches(
&combo(&[K::A, K::S]),
&keyboard(&[K::A, K::S], K::S),
K::S
));
}
#[test]
fn find_accelerated_item_walks_submenus_and_skips_disabled_items() {
let mut greyed = StringMenuItem::create("Greyed".into());
greyed.accelerator = Some(combo(&[K::LWin, K::G])).into();
greyed.menu_item_state = MenuItemState::Greyed;
let mut save = StringMenuItem::create("Save".into());
save.accelerator = Some(combo(&[K::LWin, K::S])).into();
let file = StringMenuItem::create("File".into()).with_children(
alloc::vec![
MenuItem::String(greyed),
MenuItem::Separator,
MenuItem::String(save)
]
.into(),
);
let menu = Menu::create(alloc::vec![MenuItem::String(file)].into());
let hit = menu
.find_accelerated_item(&keyboard(&[primary(), K::S], K::S), K::S)
.expect("Save is nested under File");
assert_eq!(hit.label.as_str(), "Save");
assert!(
menu.find_accelerated_item(&keyboard(&[primary(), K::G], K::G), K::G)
.is_none(),
"a greyed item's accelerator is dead"
);
assert!(menu
.find_accelerated_item(&keyboard(&[K::S], K::S), K::S)
.is_none());
}
}
#[cfg(test)]
mod autotest_generated {
use alloc::{format, string::String, vec, vec::Vec};
use super::*;
use crate::{
refany::RefAny,
window::{ContextMenuMouseButton, VirtualKeyCodeCombo, VirtualKeyCodeVec},
};
const ALL_POSITIONS: [MenuPopupPosition; 10] = [
MenuPopupPosition::BottomLeftOfCursor,
MenuPopupPosition::BottomRightOfCursor,
MenuPopupPosition::TopLeftOfCursor,
MenuPopupPosition::TopRightOfCursor,
MenuPopupPosition::BottomOfHitRect,
MenuPopupPosition::LeftOfHitRect,
MenuPopupPosition::TopOfHitRect,
MenuPopupPosition::RightOfHitRect,
MenuPopupPosition::AutoCursor,
MenuPopupPosition::AutoHitRect,
];
fn item(label: &str) -> MenuItem {
MenuItem::String(StringMenuItem::create(label.into()))
}
fn item_vec(labels: &[&str]) -> MenuItemVec {
labels.iter().map(|l| item(l)).collect::<Vec<_>>().into()
}
fn labels_of(v: &MenuItemVec) -> Vec<&str> {
v.as_slice()
.iter()
.map(|i| match i {
MenuItem::String(s) => s.label.as_str(),
MenuItem::Separator => "<sep>",
MenuItem::BreakLine => "<br>",
})
.collect()
}
fn all_distinct(hashes: &[u64]) -> bool {
let mut sorted = hashes.to_vec();
sorted.sort_unstable();
sorted.dedup();
sorted.len() == hashes.len()
}
#[test]
fn menu_create_defaults_and_extreme_inputs() {
for items in [
MenuItemVec::new(),
MenuItemVec::from_const_slice(&[]),
item_vec(&["a"]),
vec![MenuItem::Separator; 10_000].into(),
] {
let expected_len = items.len();
let menu = Menu::create(items);
assert_eq!(menu.items.len(), expected_len);
assert_eq!(menu.position, MenuPopupPosition::AutoCursor);
assert_eq!(menu.context_mouse_btn, ContextMenuMouseButton::Right);
let _ = menu.get_hash();
}
}
#[test]
fn menu_create_preserves_item_order_and_contents() {
let source = vec![
item("File"),
MenuItem::Separator,
item("Edit"),
MenuItem::BreakLine,
item(""),
];
let menu = Menu::create(source.clone().into());
assert_eq!(menu.items.as_slice(), source.as_slice());
assert_eq!(
labels_of(&menu.items),
["File", "<sep>", "Edit", "<br>", ""]
);
}
#[test]
fn menu_create_of_empty_vec_equals_default() {
assert_eq!(Menu::create(MenuItemVec::new()), Menu::default());
assert_eq!(
Menu::create(MenuItemVec::new()).get_hash(),
Menu::default().get_hash()
);
}
#[test]
fn menu_item_vec_roundtrip_through_library_owned_vec() {
let source = vec![item("a"), MenuItem::Separator, item("\u{1F600}")];
let decoded = MenuItemVec::from(source.clone()).into_library_owned_vec();
assert_eq!(decoded, source);
assert!(MenuItemVec::from_const_slice(&[])
.into_library_owned_vec()
.is_empty());
}
#[test]
fn menu_with_position_sets_field_and_keeps_items() {
for pos in ALL_POSITIONS {
let menu = Menu::create(item_vec(&["a", "b"])).with_position(pos);
assert_eq!(menu.position, pos);
assert_eq!(menu.items.len(), 2);
assert_eq!(labels_of(&menu.items), ["a", "b"]);
assert_eq!(menu.context_mouse_btn, ContextMenuMouseButton::Right);
}
}
#[test]
fn menu_with_position_last_call_wins() {
let menu = Menu::create(MenuItemVec::new())
.with_position(MenuPopupPosition::TopOfHitRect)
.with_position(MenuPopupPosition::LeftOfHitRect)
.with_position(MenuPopupPosition::AutoHitRect);
assert_eq!(menu.position, MenuPopupPosition::AutoHitRect);
}
#[test]
fn menu_get_hash_is_deterministic() {
let build = || {
Menu::create(item_vec(&["File", "Edit"])).with_position(MenuPopupPosition::TopOfHitRect)
};
let a = build();
let b = build();
assert_eq!(a.get_hash(), a.get_hash(), "repeated calls must agree");
assert_eq!(
a.get_hash(),
b.get_hash(),
"independently built equal menus must agree"
);
}
#[test]
fn menu_get_hash_on_empty_and_default_does_not_panic() {
let heap_empty = Menu::create(MenuItemVec::new());
let static_empty = Menu::create(MenuItemVec::from_const_slice(&[]));
assert_eq!(heap_empty, static_empty);
assert_eq!(heap_empty.get_hash(), static_empty.get_hash());
assert_eq!(Menu::default().get_hash(), heap_empty.get_hash());
}
#[test]
fn menu_get_hash_reacts_to_position_and_mouse_button() {
let hashes: Vec<u64> = ALL_POSITIONS
.iter()
.map(|p| Menu::create(item_vec(&["a"])).with_position(*p).get_hash())
.collect();
assert!(all_distinct(&hashes), "each popup position must hash apart");
let btn_hashes: Vec<u64> = [
ContextMenuMouseButton::Right,
ContextMenuMouseButton::Middle,
ContextMenuMouseButton::Left,
]
.iter()
.map(|b| {
let mut menu = Menu::create(item_vec(&["a"]));
menu.context_mouse_btn = *b;
menu.get_hash()
})
.collect();
assert!(all_distinct(&btn_hashes));
}
#[test]
fn menu_get_hash_distinguishes_nesting_from_flattening() {
let nested = Menu::create(MenuItemVec::from_item(MenuItem::String(
StringMenuItem::create("a".into()).with_child(item("b")),
)));
let flat = Menu::create(item_vec(&["a", "b"]));
assert_ne!(nested, flat);
assert_ne!(nested.get_hash(), flat.get_hash());
}
#[test]
fn menu_get_hash_distinguishes_item_kinds_and_states() {
let sep = Menu::create(MenuItemVec::from_item(MenuItem::Separator));
let br = Menu::create(MenuItemVec::from_item(MenuItem::BreakLine));
assert_ne!(sep.get_hash(), br.get_hash());
let with_state = |state: MenuItemState| {
let mut s = StringMenuItem::create("x".into());
s.menu_item_state = state;
Menu::create(MenuItemVec::from_item(MenuItem::String(s))).get_hash()
};
assert!(all_distinct(&[
with_state(MenuItemState::Normal),
with_state(MenuItemState::Greyed),
with_state(MenuItemState::Disabled),
]));
let with_icon = |icon: OptionMenuItemIcon| {
let mut s = StringMenuItem::create("x".into());
s.icon = icon;
Menu::create(MenuItemVec::from_item(MenuItem::String(s))).get_hash()
};
assert!(all_distinct(&[
with_icon(OptionMenuItemIcon::None),
with_icon(Some(MenuItemIcon::Checkbox(false)).into()),
with_icon(Some(MenuItemIcon::Checkbox(true)).into()),
]));
}
#[test]
fn menu_get_hash_separates_absent_accelerator_from_empty_one() {
let mut with_empty_combo = StringMenuItem::create("x".into());
with_empty_combo.accelerator = Some(VirtualKeyCodeCombo {
keys: VirtualKeyCodeVec::new(),
})
.into();
let none = Menu::create(MenuItemVec::from_item(MenuItem::String(
StringMenuItem::create("x".into()),
)));
let empty = Menu::create(MenuItemVec::from_item(MenuItem::String(with_empty_combo)));
assert_ne!(none.get_hash(), empty.get_hash());
}
#[test]
fn menu_get_hash_reacts_to_unicode_and_nul_labels() {
let labels = [
"",
"a",
"a\u{0}b",
"a\u{0}",
"\u{0}a",
"e\u{301}", "\u{e9}", "\u{202e}x", "\u{1F469}\u{200D}\u{1F4BB}", "\u{10FFFF}",
];
let hashes: Vec<u64> = labels
.iter()
.map(|l| Menu::create(item_vec(&[l])).get_hash())
.collect();
assert!(all_distinct(&hashes), "distinct labels must hash apart");
for l in labels {
let a = Menu::create(item_vec(&[l]));
let b = Menu::create(item_vec(&[l]));
assert_eq!(a.get_hash(), b.get_hash());
}
}
#[test]
fn menu_get_hash_handles_large_menus() {
let mut labels: Vec<String> = (0..10_000).map(|i| format!("item-{i}")).collect();
let big: MenuItemVec = labels
.iter()
.map(|l| item(l.as_str()))
.collect::<Vec<_>>()
.into();
let menu = Menu::create(big);
assert_eq!(menu.items.len(), 10_000);
assert_eq!(menu.get_hash(), menu.get_hash());
labels[9_999] = String::from("item-flipped");
let flipped = Menu::create(
labels
.iter()
.map(|l| item(l.as_str()))
.collect::<Vec<_>>()
.into(),
);
assert_ne!(menu.get_hash(), flipped.get_hash());
}
#[test]
fn menu_get_hash_survives_deeply_nested_submenus() {
const DEPTH: usize = 200;
let mut nested = StringMenuItem::create("leaf".into());
for i in 0..DEPTH {
nested = StringMenuItem::create(format!("lvl-{i}").as_str().into())
.with_child(MenuItem::String(nested));
}
let menu = Menu::create(MenuItemVec::from_item(MenuItem::String(nested)));
assert_eq!(menu.get_hash(), menu.get_hash());
}
#[test]
fn menu_get_hash_is_stable_across_clone_without_callbacks() {
let menu = Menu::create(item_vec(&["File", "Edit"]))
.with_position(MenuPopupPosition::BottomOfHitRect);
let cloned = menu.clone();
assert_eq!(menu, cloned);
assert_eq!(menu.get_hash(), cloned.get_hash());
}
#[test]
fn menu_get_hash_is_stable_across_clone_with_callback() {
let menu = Menu::create(MenuItemVec::from_item(MenuItem::String(
StringMenuItem::create("Save".into()).with_callback(RefAny::new(1u32), 0xDEAD_usize),
)));
let cloned = menu.clone();
assert_eq!(
menu, cloned,
"a clone shares the same RefAny data, so it must compare equal"
);
assert_eq!(menu.get_hash(), cloned.get_hash());
}
#[test]
fn string_menu_item_create_defaults() {
let it = StringMenuItem::create("File".into());
assert_eq!(it.label.as_str(), "File");
assert!(it.accelerator.is_none());
assert!(it.callback.is_none());
assert_eq!(it.menu_item_state, MenuItemState::Normal);
assert!(it.icon.is_none());
assert!(it.children.is_empty());
assert_eq!(it.children.len(), 0);
}
#[test]
fn string_menu_item_create_preserves_extreme_labels() {
let huge = "\u{1F600}".repeat(256 * 1024); for label in [
String::new(),
String::from("a\u{0}b"),
String::from("\u{202e}\u{200d}\u{feff}"),
"x".repeat(1024 * 1024),
huge,
] {
let it = StringMenuItem::create(label.as_str().into());
assert_eq!(
it.label.as_str(),
label.as_str(),
"label must survive byte-for-byte"
);
assert_eq!(it.label.as_str().len(), label.len());
assert!(it.children.is_empty());
}
}
#[test]
fn with_children_sets_replaces_and_clears() {
let it = StringMenuItem::create("File".into()).with_children(item_vec(&["New", "Open"]));
assert_eq!(it.children.len(), 2);
assert_eq!(labels_of(&it.children), ["New", "Open"]);
let it = it.with_children(item_vec(&["Only"]));
assert_eq!(it.children.len(), 1);
assert_eq!(labels_of(&it.children), ["Only"]);
let it = it.with_children(MenuItemVec::new());
assert!(it.children.is_empty());
assert_eq!(it.children.get(0), None);
}
#[test]
fn with_children_keeps_other_fields_and_accepts_large_input() {
let big: MenuItemVec = (0..5_000)
.map(|i| item(format!("c{i}").as_str()))
.collect::<Vec<_>>()
.into();
let it = StringMenuItem::create("root".into()).with_children(big);
assert_eq!(it.label.as_str(), "root");
assert_eq!(it.children.len(), 5_000);
assert_eq!(it.menu_item_state, MenuItemState::Normal);
assert!(it.callback.is_none());
match it.children.get(4_999) {
Some(MenuItem::String(s)) => assert_eq!(s.label.as_str(), "c4999"),
other => panic!("unexpected tail item: {other:?}"),
}
assert_eq!(it.children.get(5_000), None, "index at len must be None");
}
#[test]
fn with_child_copies_out_of_the_static_backing() {
let it = StringMenuItem::create("File".into()).with_child(item("New"));
assert_eq!(it.children.len(), 1);
assert_eq!(labels_of(&it.children), ["New"]);
assert!(it.children.capacity() >= 1);
assert!(StringMenuItem::create("Edit".into()).children.is_empty());
}
#[test]
fn with_child_appends_in_order_and_after_with_children() {
let it = StringMenuItem::create("File".into())
.with_child(item("a"))
.with_child(MenuItem::Separator)
.with_child(item("b"));
assert_eq!(labels_of(&it.children), ["a", "<sep>", "b"]);
let it = StringMenuItem::create("File".into())
.with_children(item_vec(&["x"]))
.with_child(item("y"));
assert_eq!(labels_of(&it.children), ["x", "y"]);
}
#[test]
fn with_child_does_not_alias_a_clone() {
let base = StringMenuItem::create("File".into()).with_child(item("shared"));
let extended = base.clone().with_child(item("extra"));
assert_eq!(
base.children.len(),
1,
"clone must not write back into the original"
);
assert_eq!(extended.children.len(), 2);
assert_eq!(labels_of(&extended.children), ["shared", "extra"]);
}
#[test]
fn with_child_repeated_many_times_keeps_contents() {
const N: usize = 2_000;
let mut it = StringMenuItem::create("root".into());
for i in 0..N {
it = it.with_child(item(format!("c{i}").as_str()));
}
assert_eq!(it.children.len(), N);
assert_eq!(labels_of(&it.children)[0], "c0");
assert_eq!(labels_of(&it.children)[N - 1], "c1999");
assert_eq!(it.label.as_str(), "root");
}
#[test]
fn with_callback_stores_pointer_value_and_data() {
for cb in [0_usize, 1, usize::MAX, usize::MAX - 1] {
let mut it =
StringMenuItem::create("Save".into()).with_callback(RefAny::new(42u64), cb);
assert!(it.callback.is_some());
{
let stored = it.callback.as_ref().expect("callback set");
assert_eq!(
stored.callback.cb, cb,
"raw fn-pointer usize must survive verbatim"
);
assert!(
stored.callback.ctx.is_none(),
"native Rust callbacks carry no FFI ctx"
);
}
let data = it.callback.as_mut().expect("callback set");
let value = data.refany.downcast_ref::<u64>().expect("payload is u64");
assert_eq!(*value, 42u64);
}
}
#[test]
fn with_callback_keeps_other_fields_and_last_call_wins() {
let it = StringMenuItem::create("Save".into())
.with_children(item_vec(&["kid"]))
.with_callback(RefAny::new(1u8), 111_usize)
.with_callback(RefAny::new(2u8), 222_usize);
let stored = it.callback.as_ref().expect("callback set");
assert_eq!(stored.callback.cb, 222_usize);
assert_eq!(it.label.as_str(), "Save");
assert_eq!(it.children.len(), 1, "callback must not disturb children");
assert_eq!(it.menu_item_state, MenuItemState::Normal);
}
#[test]
fn with_callback_changes_menu_hash() {
let plain = Menu::create(MenuItemVec::from_item(MenuItem::String(
StringMenuItem::create("Save".into()),
)));
let with_cb = Menu::create(MenuItemVec::from_item(MenuItem::String(
StringMenuItem::create("Save".into()).with_callback(RefAny::new(1u32), 7_usize),
)));
assert_ne!(plain.get_hash(), with_cb.get_hash());
}
}