tests.rs
raw
use guiduck_scene::geom::{Point, Rect, Size};
use guiduck_scene::paint::color::palette::css;
use guiduck_scene::{DisplayItem, FragmentStore};
use taffy::prelude::{length, percent};
use super::*;
use crate::clipboard::NoClipboard;
use crate::event::{EventKind, PointerButton, PointerInput};
use crate::text::TextContext;
use crate::widget::{Text, TextInput};
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");
guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT))
}
fn tree() -> WidgetTree {
WidgetTree::with_text_context(TextContext::hermetic([sample_font()]))
}
fn full_size() -> taffy::Style {
taffy::Style {
size: taffy::Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
..Default::default()
}
}
fn fixed(width: f32, height: f32) -> taffy::Style {
taffy::Style {
size: taffy::Size {
width: length(width),
height: length(height),
},
flex_shrink: 0.0,
..Default::default()
}
}
/// A base tree with a white background and one 100×40 button-ish child at
/// the top-left (padding 10).
fn base_tree() -> (WidgetTree, WidgetId) {
let mut tree = tree();
let root = tree.insert(
Container::new().background(css::WHITE),
taffy::Style {
padding: taffy::Rect::length(10.0_f32),
..full_size()
},
None,
);
let button = tree.insert(
Container::new().background(css::STEEL_BLUE),
fixed(100.0, 40.0),
Some(root),
);
(tree, button)
}
fn press(tree: &mut WidgetTree, pos: Point) {
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,
);
}
#[test]
fn frame_composes_overlays_above_the_base_at_their_positions() {
let (mut tree, _) = base_tree();
let overlay = tree.open_overlay(fixed(80.0, 60.0), OverlayOptions::dialog());
tree.insert(
Container::new().background(css::MISTY_ROSE),
full_size(),
Some(overlay),
);
let frame = tree.render_frame(VIEWPORT).expect("frame");
let items = &tree.fragments().get(frame).expect("frame exists").items;
let children: Vec<_> = items
.iter()
.filter_map(|item| match item {
DisplayItem::Child {
transform,
fragment,
} => Some((*transform, *fragment)),
_ => None,
})
.collect();
assert_eq!(children.len(), 2, "base plus one overlay");
assert_eq!(
children[0].0,
guiduck_scene::geom::Affine::IDENTITY,
"base draws first, at identity"
);
// An 80×60 dialog centers in 400×300.
assert_eq!(
children[1].0,
guiduck_scene::geom::Affine::translate((160.0, 120.0))
);
// The retained frame stays structurally identical to the oracle.
let mut fresh = FragmentStore::new();
let fresh_frame = tree.paint_fresh(&mut fresh).expect("root");
assert!(tree.fragments().trees_equal(frame, &fresh, fresh_frame));
}
#[test]
fn hit_testing_prefers_the_topmost_overlay_and_modals_block() {
let (mut tree, button) = base_tree();
tree.render_frame(VIEWPORT);
// The button is hittable before any overlay exists.
assert_eq!(tree.hit_test(Point::new(30.0, 20.0)), Some(button));
let overlay = tree.open_overlay(fixed(80.0, 60.0), OverlayOptions::dialog());
let content = tree.insert(
Container::new().background(css::MISTY_ROSE),
full_size(),
Some(overlay),
);
tree.render_frame(VIEWPORT);
// Inside the centered dialog (160..240 × 120..180): the overlay wins.
assert_eq!(tree.hit_test(Point::new(200.0, 150.0)), Some(content));
// Outside it: the modal is a barrier, nothing beneath is hittable.
assert_eq!(tree.hit_test(Point::new(30.0, 20.0)), None);
tree.close_overlay(overlay);
tree.render_frame(VIEWPORT);
assert_eq!(tree.hit_test(Point::new(30.0, 20.0)), Some(button));
}
#[test]
fn outside_press_dismisses_a_popup_and_is_consumed() {
let (mut tree, button) = base_tree();
let clicked = std::rc::Rc::new(std::cell::Cell::new(0));
{
let clicked = clicked.clone();
tree.on_event(button, EventKind::Click, move |_ctx, _ev| {
clicked.set(clicked.get() + 1);
});
}
let closed = std::rc::Rc::new(std::cell::Cell::new(false));
let popup = {
let closed = closed.clone();
tree.open_overlay(
fixed(80.0, 60.0),
OverlayOptions::popup_at(Point::new(250.0, 100.0)).on_close(move || closed.set(true)),
)
};
tree.render_frame(VIEWPORT);
// Press on the button, outside the popup: the popup closes and the
// press is consumed — the button must NOT click.
press(&mut tree, Point::new(30.0, 20.0));
assert!(closed.get(), "popup dismissed by the outside press");
assert!(!tree.is_overlay(popup));
assert_eq!(clicked.get(), 0, "the dismissing press activates nothing");
// The next press, with the popup gone, clicks normally.
tree.render_frame(VIEWPORT);
press(&mut tree, Point::new(30.0, 20.0));
assert_eq!(clicked.get(), 1);
}
#[test]
fn escape_dismisses_the_topmost_dismissable_overlay() {
let (mut tree, _) = base_tree();
let dialog = tree.open_overlay(fixed(80.0, 60.0), OverlayOptions::dialog());
tree.render_frame(VIEWPORT);
let consumed = tree.dispatch_key(
&crate::event::KeyInput {
key: crate::event::Key::Escape,
modifiers: Default::default(),
pressed: true,
},
&mut NoClipboard,
);
assert!(consumed);
assert!(!tree.is_overlay(dialog));
}
#[test]
fn a_modal_traps_focus_and_restores_it_on_close() {
let mut tree = tree();
let root = tree.insert(Container::new(), full_size(), None);
let base_input = tree.insert(
TextInput::new(13.0).family("DejaVu Sans"),
fixed(120.0, 24.0),
Some(root),
);
tree.render_frame(VIEWPORT);
assert_eq!(tree.focus(), Some(base_input), "focus invariant");
let dialog = tree.open_overlay(fixed(200.0, 100.0), OverlayOptions::dialog());
let dialog_input = tree.insert(
TextInput::new(13.0).family("DejaVu Sans"),
fixed(120.0, 24.0),
Some(dialog),
);
tree.render_frame(VIEWPORT);
assert_eq!(
tree.focus(),
Some(dialog_input),
"opening a modal moves focus into it"
);
// Tab cycles within the modal (one focusable: back to itself), never
// out to the base input.
tree.focus_step(false);
assert_eq!(tree.focus(), Some(dialog_input));
tree.close_overlay(dialog);
tree.render_frame(VIEWPORT);
assert_eq!(
tree.focus(),
Some(base_input),
"closing the modal restores the prior focus"
);
}
#[test]
fn a_modal_without_focusables_parks_focus() {
let mut tree = tree();
let root = tree.insert(Container::new(), full_size(), None);
let base_input = tree.insert(
TextInput::new(13.0).family("DejaVu Sans"),
fixed(120.0, 24.0),
Some(root),
);
tree.render_frame(VIEWPORT);
assert_eq!(tree.focus(), Some(base_input));
let dialog = tree.open_overlay(fixed(200.0, 100.0), OverlayOptions::dialog());
tree.insert(
Text::new("sure?").family("DejaVu Sans"),
taffy::Style::default(),
Some(dialog),
);
tree.render_frame(VIEWPORT);
assert_eq!(
tree.focus(),
None,
"keys must not leak beneath the modal while it is open"
);
tree.close_overlay(dialog);
tree.render_frame(VIEWPORT);
assert_eq!(tree.focus(), Some(base_input));
}
#[test]
fn anchored_placement_flips_at_the_viewport_edge() {
let (mut tree, _button) = base_tree();
// An anchor near the bottom of the viewport.
let root = tree.root().expect("root");
let anchor = tree.insert(
Container::new(),
taffy::Style {
margin: taffy::Rect {
top: length(220.0_f32),
..taffy::Rect::length(0.0_f32)
},
..fixed(100.0, 40.0)
},
Some(root),
);
tree.render_frame(VIEWPORT);
let anchor_rect =
Rect::from_origin_size(tree.absolute_origin(anchor), tree.layout(anchor).size());
let popup = tree.open_overlay(
fixed(80.0, 60.0),
OverlayOptions::popup(anchor, AnchorSide::Below),
);
tree.render_frame(VIEWPORT);
let position = tree
.overlays
.iter()
.find(|o| o.root == popup)
.expect("open")
.position;
// Below would end at y ≈ anchor.y1 + 60 > 300, so it flips above.
assert_eq!(
position,
Point::new(anchor_rect.x0, anchor_rect.y0 - 60.0),
"flipped above the anchor (anchor at {anchor_rect:?})"
);
}
#[test]
fn overlay_widgets_report_layered_absolute_origins() {
let (mut tree, _) = base_tree();
let overlay = tree.open_overlay(
fixed(80.0, 60.0),
OverlayOptions::popup_at(Point::new(250.0, 100.0)),
);
let content = tree.insert(
Container::new(),
taffy::Style {
margin: taffy::Rect {
left: length(5.0_f32),
top: length(7.0_f32),
..taffy::Rect::length(0.0_f32)
},
..fixed(20.0, 20.0)
},
Some(overlay),
);
tree.render_frame(VIEWPORT);
assert_eq!(
tree.absolute_origin(content),
Point::new(255.0, 107.0),
"overlay position plus local layout"
);
// And the hit test agrees with the reported origin.
assert_eq!(tree.hit_test(Point::new(256.0, 108.0)), Some(content));
}