tests.rs
raw
use super::*;
use crate::clipboard::NoClipboard;
use crate::event::{Key, KeyInput, Modifiers, PointerButton, PointerInput};
use crate::text::TextContext;
use guiduck_scene::geom::Size;
const VIEWPORT: Size = Size::new(400.0, 300.0);
fn sample_font() -> guiduck_scene::paint::Blob<u8> {
static FONT: &[u8] = include_bytes!("../../../../guiduck-samples/fonts/DejaVuSans.ttf");
static BLOB: std::sync::OnceLock<guiduck_scene::paint::Blob<u8>> = std::sync::OnceLock::new();
BLOB.get_or_init(|| guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT)))
.clone()
}
fn tree() -> WidgetTree {
WidgetTree::with_text_context(TextContext::hermetic([sample_font()]))
}
/// Every widget under `id`, by count — the measure of what actually exists.
fn descendants(tree: &WidgetTree, id: WidgetId) -> usize {
tree.children(id)
.iter()
.map(|child| 1 + descendants(tree, *child))
.sum()
}
fn total_widgets(tree: &WidgetTree) -> usize {
let base = tree.root().map_or(0, |root| 1 + descendants(tree, root));
base + tree
.overlay_roots()
.iter()
.map(|root| 1 + descendants(tree, *root))
.sum::<usize>()
}
/// A File menu with three plain rows.
fn file_menu() -> Menu {
Menu::new("File").content(ContentBuilder::new(|tree, parent| {
for label in ["New", "Open", "Quit"] {
tree.insert(
MenuItem::new(label).family("DejaVu Sans"),
taffy::Style::default(),
Some(parent),
);
}
}))
}
fn bar_with(menu: Menu) -> (WidgetTree, WidgetId, WidgetId) {
let mut tree = tree();
let bar = tree.insert(MenuBar::new(), taffy::Style::default(), None);
let title = tree.insert(
menu.family("DejaVu Sans"),
taffy::Style::default(),
Some(bar),
);
tree.render_frame(VIEWPORT);
(tree, bar, title)
}
fn click(tree: &mut WidgetTree, id: WidgetId) {
let rect = tree.layout(id);
let origin = tree.absolute_origin(id);
let pos = Point::new(
origin.x + rect.width() / 2.0,
origin.y + rect.height() / 2.0,
);
tree.dispatch_pointer(
PointerInput::Down {
time: std::time::Instant::now(),
pos,
button: PointerButton::Left,
},
&mut NoClipboard,
);
tree.dispatch_pointer(
PointerInput::Up {
pos,
button: PointerButton::Left,
},
&mut NoClipboard,
);
tree.render_frame(VIEWPORT);
}
fn press(tree: &mut WidgetTree, key: Key) {
tree.dispatch_key(
&KeyInput {
key,
modifiers: Modifiers::default(),
pressed: true,
},
&mut NoClipboard,
);
tree.render_frame(VIEWPORT);
}
#[test]
fn a_closed_menu_has_no_content_at_all() {
// The whole point of deferring: the rows do not exist, so they lay out
// nothing, shape no text, and subscribe to nothing.
let (tree, _bar, _title) = bar_with(file_menu());
assert_eq!(total_widgets(&tree), 2, "just the bar and its title");
assert!(!tree.menus_open());
}
#[test]
fn opening_builds_the_content_and_closing_takes_it_away() {
let (mut tree, _bar, title) = bar_with(file_menu());
click(&mut tree, title);
assert!(tree.menus_open());
// Bar, title, overlay root, panel, three rows.
assert_eq!(total_widgets(&tree), 7);
assert!(tree.widget::<Menu>(title).expect("a menu").is_open());
// Escape closes the topmost layer — the overlay's own dismissal.
press(&mut tree, Key::Escape);
assert!(!tree.menus_open());
assert_eq!(total_widgets(&tree), 2, "the content is gone, not hidden");
assert!(!tree.widget::<Menu>(title).expect("a menu").is_open());
}
#[test]
fn clicking_an_open_title_closes_it() {
let (mut tree, _bar, title) = bar_with(file_menu());
click(&mut tree, title);
assert!(tree.menus_open());
click(&mut tree, title);
assert!(!tree.menus_open(), "a title toggles its own menu");
}
#[test]
fn only_the_opened_path_is_built() {
// A submenu's rows are not built when its parent opens — only when the
// submenu itself does. A deep tree costs the path you walked, not the
// tree, which is what the pre-mounted design could never give.
let deep = Menu::new("File").content(ContentBuilder::new(|tree, parent| {
tree.insert(
MenuItem::new("New").family("DejaVu Sans"),
taffy::Style::default(),
Some(parent),
);
tree.insert(
Menu::new("Recent")
.submenu(true)
.family("DejaVu Sans")
.content(ContentBuilder::new(|tree, parent| {
for label in ["a.txt", "b.txt", "c.txt", "d.txt"] {
tree.insert(
MenuItem::new(label).family("DejaVu Sans"),
taffy::Style::default(),
Some(parent),
);
}
})),
taffy::Style::default(),
Some(parent),
);
}));
let (mut tree, _bar, title) = bar_with(deep);
click(&mut tree, title);
// Bar, title, overlay, panel, "New", "Recent" — and none of Recent's four.
assert_eq!(
total_widgets(&tree),
6,
"the submenu's rows do not exist yet"
);
let recent = *tree
.overlay_roots()
.iter()
.flat_map(|root| tree.children(*root).to_vec())
.flat_map(|panel| tree.children(panel).to_vec())
.find(|id| tree.widget::<Menu>(*id).is_some())
.as_ref()
.expect("the Recent row");
click(&mut tree, recent);
// Now the second layer exists: overlay, panel, four rows.
assert_eq!(total_widgets(&tree), 12);
assert!(
tree.widget::<Menu>(title).expect("a menu").is_open(),
"opening a submenu keeps its parent up"
);
}
#[test]
fn arrows_rove_a_highlight_without_moving_focus() {
let (mut tree, _bar, title) = bar_with(file_menu());
click(&mut tree, title);
let panel = tree.children(tree.overlay_roots()[0])[0];
let rows: Vec<WidgetId> = tree.children(panel).to_vec();
let focused = tree.focus();
assert_eq!(focused, Some(panel), "the panel holds focus, not the rows");
let highlighted = |tree: &WidgetTree, rows: &[WidgetId]| -> Vec<bool> {
rows.iter()
.map(|id| {
tree.widget::<MenuItem>(*id)
.expect("a row")
.is_highlighted()
})
.collect()
};
assert_eq!(highlighted(&tree, &rows), [false, false, false]);
press(&mut tree, Key::Down);
assert_eq!(highlighted(&tree, &rows), [true, false, false]);
press(&mut tree, Key::Down);
assert_eq!(highlighted(&tree, &rows), [false, true, false]);
press(&mut tree, Key::Up);
assert_eq!(highlighted(&tree, &rows), [true, false, false]);
// Up from the first row wraps to the last: a menu is a ring.
press(&mut tree, Key::Up);
assert_eq!(highlighted(&tree, &rows), [false, false, true]);
press(&mut tree, Key::Home);
assert_eq!(highlighted(&tree, &rows), [true, false, false]);
press(&mut tree, Key::End);
assert_eq!(highlighted(&tree, &rows), [false, false, true]);
// Roving never moved focus, which is what makes it cheap and what lets
// the overlay restore the right widget when the menu closes.
assert_eq!(tree.focus(), focused);
}
#[test]
fn hovering_a_row_makes_it_the_current_one() {
// The pointer and the arrows move the *same* highlight, so they cannot
// disagree about which row is current.
let (mut tree, _bar, title) = bar_with(file_menu());
click(&mut tree, title);
let panel = tree.children(tree.overlay_roots()[0])[0];
let rows: Vec<WidgetId> = tree.children(panel).to_vec();
press(&mut tree, Key::Down);
let rect = tree.layout(rows[2]);
let origin = tree.absolute_origin(rows[2]);
tree.dispatch_pointer(
PointerInput::Moved(Point::new(
origin.x + rect.width() / 2.0,
origin.y + rect.height() / 2.0,
)),
&mut NoClipboard,
);
tree.render_frame(VIEWPORT);
assert!(!tree.widget::<MenuItem>(rows[0]).unwrap().is_highlighted());
assert!(tree.widget::<MenuItem>(rows[2]).unwrap().is_highlighted());
}
#[test]
fn choosing_a_row_closes_the_whole_stack() {
let (mut tree, _bar, title) = bar_with(file_menu());
click(&mut tree, title);
let panel = tree.children(tree.overlay_roots()[0])[0];
let rows: Vec<WidgetId> = tree.children(panel).to_vec();
click(&mut tree, rows[1]);
assert!(!tree.menus_open(), "the command is done; the menu goes");
assert_eq!(total_widgets(&tree), 2);
}
#[test]
fn enter_chooses_the_highlighted_row() {
let (mut tree, _bar, title) = bar_with(file_menu());
click(&mut tree, title);
press(&mut tree, Key::Down);
press(&mut tree, Key::Enter);
// Enter and a click are one path: both end with the stack closed.
assert!(!tree.menus_open());
}