use kui_core::testing::{click, edit_key, hover};
use kui_core::{
BarMenu, Core, EditKey, Key, Menu, MenuBar, MenuItem, Mods, NodeSpec, Rect, Role, Size,
TextStyle, Value, Vec2,
};
fn frame_with(core: &mut Core, bar: Option<MenuBar>) -> Key {
let mut ui = core.frame(Size::new(600.0, 400.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
if let Some(bar) = bar {
kui_core::widgets::menu_bar(&mut ui, bar);
}
let card = ui.with_keyed("card", NodeSpec::column().grow_width(), |ui| {
ui.text("a note", TextStyle::new(14.0));
});
ui.finish();
card
}
fn frame(core: &mut Core) -> Key {
frame_with(core, None)
}
fn items() -> Vec<MenuItem> {
vec![
MenuItem::new("Open").id("open"),
MenuItem::submenu(
"Move to",
vec![
MenuItem::new("Inbox").id("move.inbox"),
MenuItem::new("Archive").id("move.archive").enabled(false),
MenuItem::submenu(
"Projects",
vec![MenuItem::new("kui").id("move.kui").accel("mod+shift+k")],
),
],
),
MenuItem::new("Delete").id("delete"),
]
}
fn open(core: &mut Core) -> Key {
let card = frame(core);
core.open_menu(Menu::new(card, Vec2::new(20.0, 20.0), items()));
frame(core);
card
}
fn row(core: &mut Core, label: &str) -> Option<kui_core::AccessNode> {
core.access_tree()
.nodes
.iter()
.find(|n| n.role == Role::MenuItem && n.name.as_deref() == Some(label))
.cloned()
}
fn center(r: Rect) -> Vec2 {
Vec2::new(r.x + r.w / 2.0, r.y + r.h / 2.0)
}
fn row_center(core: &mut Core, label: &str) -> Vec2 {
center(
row(core, label)
.unwrap_or_else(|| panic!("no menu row named {label:?}"))
.rect,
)
}
fn menus(core: &mut Core) -> Vec<String> {
core.access_tree()
.nodes
.iter()
.filter(|n| n.role == Role::Menu)
.map(|n| n.name.clone().unwrap_or_default())
.collect()
}
fn key(core: &mut Core, k: EditKey) -> Vec<kui_core::UiEvent> {
edit_key(core, k, Mods::default())
}
fn focused_row(core: &mut Core) -> Option<String> {
core.access_tree()
.nodes
.iter()
.find(|n| n.focused)
.and_then(|n| n.name.clone())
}
#[test]
fn a_submenu_row_draws_closed_and_reads_as_collapsed() {
let mut core = Core::new();
open(&mut core);
assert_eq!(menus(&mut core), ["Menu"], "only the menu itself is drawn");
let move_to = row(&mut core, "Move to").expect("the row is drawn");
assert_eq!(
move_to.expanded,
Some(false),
"a reader hears it opens something"
);
assert!(
row(&mut core, "Inbox").is_none(),
"nothing inside is drawn yet"
);
assert_eq!(core.menu_submenus(), &[] as &[usize]);
}
#[test]
fn the_pointer_opens_a_submenu_and_another_row_closes_it() {
let mut core = Core::new();
open(&mut core);
let at = row_center(&mut core, "Move to");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
assert_eq!(menus(&mut core), ["Menu", "Move to"]);
let parent = row(&mut core, "Move to").unwrap();
assert_eq!(parent.expanded, Some(true));
let inbox = row(&mut core, "Inbox")
.expect("the submenu's rows are drawn")
.rect;
assert!(
inbox.x >= parent.rect.x + parent.rect.w,
"beside its row: {inbox:?} against {:?}",
parent.rect
);
assert!(
(inbox.y - parent.rect.y).abs() < 1.0,
"its first row level with it: {} against {}",
inbox.y,
parent.rect.y
);
let at = row_center(&mut core, "Projects");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1, 2]);
assert!(row(&mut core, "kui").is_some());
let at = row_center(&mut core, "Inbox");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
assert!(row(&mut core, "kui").is_none());
let at = row_center(&mut core, "Delete");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[] as &[usize]);
assert_eq!(menus(&mut core), ["Menu"]);
}
#[test]
fn choosing_a_row_inside_posts_it_and_closes_every_level() {
let mut core = Core::new();
let card = open(&mut core);
let at = row_center(&mut core, "Move to");
let evs = click(&mut core, at);
assert!(
evs.is_empty(),
"the submenu's row is nobody's click: {evs:?}"
);
assert!(core.menu().is_some(), "still open");
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1], "and the click opened it");
let at = row_center(&mut core, "Projects");
click(&mut core, at);
frame(&mut core);
let at = row_center(&mut core, "kui");
let evs = click(&mut core, at);
assert_eq!(evs.len(), 1, "{evs:?}");
assert_eq!(evs[0].key, card, "on the node the menu is about");
assert_eq!(evs[0].kind(), Some("menu"));
assert_eq!(evs[0].payload.get_str("item"), Some("move.kui"));
assert!(core.menu().is_none(), "every level closes");
assert_eq!(core.menu_submenus(), &[] as &[usize]);
}
#[test]
fn a_disabled_row_inside_is_inert() {
let mut core = Core::new();
open(&mut core);
let at = row_center(&mut core, "Move to");
click(&mut core, at);
frame(&mut core);
let at = row_center(&mut core, "Archive");
let evs = click(&mut core, at);
assert!(evs.is_empty(), "{evs:?}");
assert!(core.menu().is_some());
}
#[test]
fn the_arrows_open_and_close_a_submenu_and_escape_closes_one_level() {
let mut core = Core::new();
open(&mut core);
assert_eq!(
focused_row(&mut core).as_deref(),
Some("Open"),
"the menu opens on its first row"
);
key(&mut core, EditKey::Down);
frame(&mut core);
assert_eq!(focused_row(&mut core).as_deref(), Some("Move to"));
let evs = key(&mut core, EditKey::Right);
assert!(evs.is_empty(), "{evs:?}");
assert_eq!(core.menu_submenus(), &[1], "opened by the key");
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
assert_eq!(focused_row(&mut core).as_deref(), Some("Inbox"));
key(&mut core, EditKey::Down);
frame(&mut core);
assert_eq!(focused_row(&mut core).as_deref(), Some("Projects"));
key(&mut core, EditKey::Right);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1, 2]);
assert_eq!(focused_row(&mut core).as_deref(), Some("kui"));
key(&mut core, EditKey::Left);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
assert_eq!(focused_row(&mut core).as_deref(), Some("Projects"));
let evs = key(&mut core, EditKey::Escape);
assert!(evs.is_empty(), "{evs:?}");
frame(&mut core);
assert!(core.menu().is_some(), "the menu is still open");
assert_eq!(core.menu_submenus(), &[] as &[usize]);
assert_eq!(focused_row(&mut core).as_deref(), Some("Move to"));
key(&mut core, EditKey::Escape);
assert!(core.menu().is_none());
let mut core = Core::new();
open(&mut core);
key(&mut core, EditKey::Down);
frame(&mut core);
assert_eq!(focused_row(&mut core).as_deref(), Some("Move to"));
key(&mut core, EditKey::Enter);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
assert_eq!(focused_row(&mut core).as_deref(), Some("Inbox"));
let evs = key(&mut core, EditKey::Enter);
assert_eq!(evs.len(), 1, "{evs:?}");
assert_eq!(evs[0].payload.get_str("item"), Some("move.inbox"));
}
#[test]
fn a_resting_pointer_does_not_undo_the_keyboard() {
let mut core = Core::new();
open(&mut core);
let at = row_center(&mut core, "Open");
hover(&mut core, at);
frame(&mut core);
key(&mut core, EditKey::Down);
frame(&mut core);
key(&mut core, EditKey::Right);
frame(&mut core);
frame(&mut core);
assert_eq!(
core.menu_submenus(),
&[1],
"still open under a pointer on Open"
);
}
#[test]
fn a_row_inside_reads_its_accelerator_in_the_platforms_spelling() {
let mut core = Core::new();
open(&mut core);
let kui = MenuItem::at_path(&core.menu().unwrap().items, &[1, 2, 0])
.unwrap()
.accel
.clone();
let want = if cfg!(target_os = "macos") {
"\u{21e7}\u{2318}K"
} else {
"Ctrl+Shift+K"
};
assert_eq!(kui.as_deref(), Some(want));
}
#[test]
fn a_host_reports_a_row_inside_by_its_path() {
let mut core = Core::new();
let card = open(&mut core);
core.set_native_menus(true);
assert_eq!(
core.activate_menu_item(1),
None,
"the row that opens a submenu"
);
assert_eq!(core.activate_menu_path(&[1]), None);
assert_eq!(core.activate_menu_path(&[1, 1]), None, "a dead row inside");
assert!(core.menu().is_some());
let evs = core.activate_menu_path(&[1, 2, 0]).expect("taken");
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, card);
assert_eq!(evs[0].payload.get_str("item"), Some("move.kui"));
assert!(core.menu().is_none());
}
#[test]
fn a_rows_items_are_its_submenu() {
let v = Value::List(vec![Value::map([
("label", Value::str("Sort by")),
(
"items",
Value::List(vec![
Value::map([
("label", Value::str("Name")),
("checked", Value::Bool(true)),
]),
Value::map([("label", Value::str("Date"))]),
]),
),
])]);
let rows = MenuItem::list_from_value(&v).unwrap();
assert!(rows[0].has_submenu());
assert_eq!(rows[0].submenu.len(), 2);
assert!(rows[0].submenu[0].checked);
assert!(MenuItem::KEYS.contains(&"items"));
let bad = Value::List(vec![Value::map([
("label", Value::str("Sort by")),
(
"items",
Value::List(vec![Value::map([("role", Value::str("custom"))])]),
),
])]);
assert!(MenuItem::list_from_value(&bad).is_err());
}
#[test]
fn the_drawn_bar_opens_submenus_too() {
let bar = || {
MenuBar::new(vec![BarMenu::new(
"View",
vec![
MenuItem::new("Zoom in").id("zoom"),
MenuItem::submenu(
"Sort by",
vec![
MenuItem::new("Name").id("sort.name").checked(true),
MenuItem::new("Date").id("sort.date"),
],
),
],
)])
};
let mut core = Core::new();
frame_with(&mut core, Some(bar()));
let at = row_center(&mut core, "View");
click(&mut core, at);
frame_with(&mut core, Some(bar()));
assert_eq!(core.menu_bar_open(), Some(0));
let at = row_center(&mut core, "Sort by");
hover(&mut core, at);
frame_with(&mut core, Some(bar()));
assert_eq!(core.menu_bar_submenus(), &[1]);
let at = row_center(&mut core, "Date");
let evs = click(&mut core, at);
assert_eq!(evs.len(), 1, "{evs:?}");
assert_eq!(evs[0].payload.get_str("item"), Some("sort.date"));
assert_eq!(core.menu_bar_open(), None, "the bar's menu closes");
assert_eq!(core.menu_bar_submenus(), &[] as &[usize]);
let evs = core.activate_menu_bar_path(0, &[1, 0]);
assert_eq!(evs[0].payload.get_str("item"), Some("sort.name"));
assert!(
core.activate_menu_bar_path(0, &[1]).is_empty(),
"the submenu's own row"
);
}
#[test]
fn a_host_cannot_choose_a_row_the_menu_would_not_reach() {
let rows = || {
vec![
MenuItem::new("Undo").id("undo").enabled(false),
MenuItem::submenu("Sort by", vec![MenuItem::new("Name").id("sort.name")])
.enabled(false),
MenuItem::submenu("Group by", vec![MenuItem::new("Day").id("group.day")]),
]
};
let bar = MenuBar::new(vec![
BarMenu::new("View", rows()),
BarMenu::new("Dead", vec![MenuItem::new("Go").id("go")]).enabled(false),
]);
let mut core = Core::new();
frame_with(&mut core, Some(bar));
assert!(
core.activate_menu_bar_path(0, &[0]).is_empty(),
"a dead row"
);
assert!(
core.activate_menu_bar_item(0, 0).is_empty(),
"a dead row, by index"
);
assert!(
core.activate_menu_bar_path(0, &[1, 0]).is_empty(),
"under a dead row"
);
assert!(
core.activate_menu_bar_path(1, &[0]).is_empty(),
"in a dead menu"
);
let evs = core.activate_menu_bar_path(0, &[2, 0]);
assert_eq!(evs[0].payload.get_str("item"), Some("group.day"));
let card = frame(&mut core);
core.open_menu(Menu::new(card, Vec2::new(20.0, 20.0), rows()));
frame(&mut core);
assert_eq!(core.activate_menu_path(&[1, 0]), None, "under a dead row");
assert!(core.menu().is_some(), "and the menu stays open");
let evs = core.activate_menu_path(&[2, 0]).expect("taken");
assert_eq!(evs[0].payload.get_str("item"), Some("group.day"));
}
#[test]
fn an_app_drawn_menu_leaves_no_submenu_for_escape_to_spend() {
let draw = |core: &mut Core, with_menu: bool| -> Key {
let mut ui = core.frame(Size::new(600.0, 400.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let dialog = ui.leaf_keyed(
"dialog",
NodeSpec::column()
.size(200.0, 100.0)
.float(kui_core::FloatConfig::viewport())
.modal(Value::Null)
.label("Dialog"),
);
if with_menu {
kui_core::widgets::context_menu(&mut ui, Vec2::new(20.0, 20.0), &items());
}
ui.finish();
dialog
};
let mut core = Core::new();
draw(&mut core, true);
let at = row_center(&mut core, "Move to");
hover(&mut core, at);
draw(&mut core, true);
hover(&mut core, Vec2::new(500.0, 350.0));
let dialog = draw(&mut core, false);
let evs = key(&mut core, EditKey::Escape);
assert_eq!(evs.len(), 1, "{evs:?}");
assert_eq!(evs[0].key, dialog);
assert_eq!(evs[0].kind(), Some("dismiss"));
}
#[test]
fn passing_over_a_row_on_the_way_into_a_submenu_keeps_it_open() {
let mut core = Core::new();
core.set_time(10.0);
open(&mut core);
let at = row_center(&mut core, "Move to");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
core.set_time(10.05);
let at = row_center(&mut core, "Delete");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1], "passing over Delete");
assert!(core.animating(), "the wait owes its frames");
core.set_time(10.1);
let at = row_center(&mut core, "Projects");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1, 2], "reached, and opened");
core.set_time(11.0);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1, 2], "nothing left waiting");
core.set_time(12.0);
let at = row_center(&mut core, "Delete");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1, 2]);
core.set_time(12.2);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1, 2], "not yet");
core.set_time(12.4);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[] as &[usize], "rested long enough");
assert_eq!(menus(&mut core), ["Menu"]);
assert!(!core.animating(), "and nothing owed after");
}
#[test]
fn a_row_the_keyboard_closed_reopens_when_the_pointer_comes_back() {
let mut core = Core::new();
open(&mut core);
let at = row_center(&mut core, "Move to");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
key(&mut core, EditKey::Escape);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[] as &[usize]);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[] as &[usize], "a resting pointer");
hover(&mut core, Vec2::new(590.0, 390.0));
frame(&mut core);
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1], "open again");
}
#[test]
fn the_pointer_leaving_the_menu_drops_a_waiting_switch() {
let mut core = Core::new();
core.set_time(10.0);
open(&mut core);
let at = row_center(&mut core, "Move to");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
core.set_time(10.05);
let at = row_center(&mut core, "Delete");
hover(&mut core, at);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1]);
assert!(core.animating(), "waiting");
core.set_time(10.1);
hover(&mut core, Vec2::new(590.0, 390.0));
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1], "still open");
assert!(!core.animating(), "nothing waiting once the pointer is off");
core.set_time(11.0);
frame(&mut core);
assert_eq!(core.menu_submenus(), &[1], "no switch ripened off the menu");
}
#[test]
fn a_bar_the_app_stops_drawing_owes_nothing_for_a_waiting_switch() {
let bar = || {
MenuBar::new(vec![BarMenu::new(
"View",
vec![
MenuItem::new("Zoom in").id("zoom"),
MenuItem::submenu("Sort by", vec![MenuItem::new("Name").id("sort.name")]),
],
)])
};
let mut core = Core::new();
core.set_time(10.0);
frame_with(&mut core, Some(bar()));
let at = row_center(&mut core, "View");
click(&mut core, at);
frame_with(&mut core, Some(bar()));
let at = row_center(&mut core, "Sort by");
hover(&mut core, at);
frame_with(&mut core, Some(bar()));
assert_eq!(core.menu_bar_submenus(), &[1]);
core.set_time(10.05);
let at = row_center(&mut core, "Zoom in");
hover(&mut core, at);
frame_with(&mut core, Some(bar()));
assert_eq!(core.menu_bar_submenus(), &[1], "waiting");
assert!(core.animating());
frame_with(&mut core, None);
assert!(
!core.animating(),
"nothing owed for a bar that is not drawn"
);
core.set_time(11.0);
frame_with(&mut core, None);
assert!(!core.animating());
assert_eq!(core.menu_bar_submenus(), &[1], "nothing switched either");
}