use std::cell::Cell as StdCell;
use std::rc::Rc;
use teksilo_canvas::{EdgeInsets, Point, Rect, SizeProposal};
use teksilo_tokens::{InputTokens, PointerKind, PointerKindMask, TargetDensity};
use crate::build_context::BuildContext;
use crate::pointer::hit_slop::{HitContext, HitSlop, circle_distance, rect_distance};
use crate::widget::{LayoutContext, LayoutResponse, Widget, WidgetPlacement};
use crate::widget_builder::HandlerSet;
use crate::widget_id::WidgetId;
use crate::widget_tree::WidgetTree;
#[derive(Default)]
struct Cell {
outset: f32,
outset_kinds: Option<PointerKindMask>,
slop: Option<HitSlop>,
round: bool,
shaped_disc: bool,
no_distance: bool,
tappable: bool,
pass_through: bool,
transparent: bool,
node_no_slop: bool,
node_slop: Option<HitSlop>,
enabled: Option<bool>,
clips: bool,
size: Option<teksilo_canvas::Size>,
taps: Option<Rc<StdCell<u32>>>,
children: Vec<WidgetId>,
}
impl std::fmt::Debug for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cell").finish()
}
}
impl Cell {
fn new() -> Self {
Self::default()
}
fn tappable(mut self) -> Self {
self.tappable = true;
self
}
fn outset(mut self, dp: f32) -> Self {
self.outset = dp;
self
}
fn outset_kinds(mut self, mask: PointerKindMask) -> Self {
self.outset_kinds = Some(mask);
self
}
fn round(mut self) -> Self {
self.round = true;
self
}
fn shaped_disc(mut self) -> Self {
self.shaped_disc = true;
self
}
fn widget_slop(mut self, slop: HitSlop) -> Self {
self.slop = Some(slop);
self
}
fn node_slop(mut self, slop: HitSlop) -> Self {
self.node_slop = Some(slop);
self
}
fn node_no_slop(mut self) -> Self {
self.node_no_slop = true;
self
}
fn no_distance(mut self) -> Self {
self.no_distance = true;
self
}
fn pass_through(mut self) -> Self {
self.pass_through = true;
self
}
fn transparent(mut self) -> Self {
self.transparent = true;
self
}
fn disabled(mut self) -> Self {
self.enabled = Some(false);
self
}
fn clips(mut self) -> Self {
self.clips = true;
self
}
fn counting(mut self, counter: &Rc<StdCell<u32>>) -> Self {
self.tappable = true;
self.taps = Some(counter.clone());
self
}
fn size(mut self, width: f32, height: f32) -> Self {
self.size = Some(teksilo_canvas::Size::new(width, height));
self
}
fn child(mut self, id: WidgetId) -> Self {
self.children.push(id);
self
}
}
impl Widget for Cell {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let mut set = HandlerSet::new();
if self.tappable {
let counter = self.taps.clone();
set = set.on_tap(move |_e, _c| {
if let Some(ref counter) = counter {
counter.set(counter.get() + 1);
}
});
}
if self.pass_through {
set = set.event_pass_through(true);
}
if self.transparent {
set = set.hit_transparent(true);
}
if self.node_no_slop {
set = set.no_hit_slop();
}
if let Some(slop) = self.node_slop {
set = set.hit_slop(slop);
}
if self.clips {
set = set.clips_children(true);
}
ctx.apply_self_handlers(set);
if let Some(enabled) = self.enabled {
let id = ctx.self_id();
ctx.enabled_when(id, enabled);
}
self.children.clone()
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
match self.size {
Some(size) => size.into(),
None => proposal.resolve(0.0, 0.0).into(),
}
}
fn children(&self) -> Vec<WidgetId> {
self.children.clone()
}
fn hit_outset(&self, kind: teksilo_tokens::PointerKind, _tokens: &InputTokens) -> EdgeInsets {
let mask = self.outset_kinds.unwrap_or(PointerKindMask::DIRECT);
if self.outset > 0.0 && mask.contains(kind) {
EdgeInsets::uniform(self.outset)
} else {
EdgeInsets::ZERO
}
}
fn hit_slop(
&self,
_kind: teksilo_tokens::PointerKind,
_tokens: &InputTokens,
) -> Option<HitSlop> {
self.slop
}
fn hit_shape(&self, local: Point, bounds: Rect) -> bool {
if !self.shaped_disc {
return true;
}
let radius = bounds.width.min(bounds.height) / 2.0;
circle_distance(bounds.center(), radius, local) <= 0.0
}
fn hit_distance(&self, local: Point, bounds: Rect) -> Option<f32> {
if self.no_distance {
return None;
}
if self.round {
let radius = bounds.width.min(bounds.height) / 2.0;
return Some(circle_distance(bounds.center(), radius, local));
}
Some(rect_distance(bounds, local))
}
}
#[derive(Debug)]
struct Board {
placed: Vec<(WidgetId, Rect)>,
vetoed: Vec<(WidgetId, Rect)>,
asked: Rc<StdCell<u32>>,
}
impl Board {
fn new() -> Self {
Self {
placed: Vec::new(),
vetoed: Vec::new(),
asked: Rc::new(StdCell::new(0)),
}
}
fn at(mut self, id: WidgetId, rect: Rect) -> Self {
self.placed.push((id, rect));
self
}
fn veto(mut self, id: WidgetId, rect: Rect) -> Self {
self.vetoed.push((id, rect));
self
}
fn counting(mut self, counter: Rc<StdCell<u32>>) -> Self {
self.asked = counter;
self
}
}
impl Widget for Board {
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for (child, (_, rect)) in children.iter_mut().zip(&self.placed) {
child.origin = Point::new(bounds.x + rect.x, bounds.y + rect.y);
child.size = rect.size();
}
}
fn children(&self) -> Vec<WidgetId> {
self.placed.iter().map(|(id, _)| *id).collect()
}
fn accepts_child_hit(&self, child: WidgetId, point: Point) -> bool {
self.asked.set(self.asked.get() + 1);
!self
.vetoed
.iter()
.any(|(id, rect)| *id == child && rect.contains(point))
}
}
fn touch() -> InputTokens {
InputTokens::for_density(TargetDensity::Touch)
}
fn at(x: f32, y: f32) -> Point {
Point::new(x, y)
}
fn touch_pointer() -> crate::pointer::PointerInfo {
crate::pointer::PointerInfo::touch(
crate::pointer::PointerId::MOUSE,
crate::pointer::EventTime::ZERO,
)
}
fn mouse_pointer() -> crate::pointer::PointerInfo {
crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO)
}
fn grip_over_row(outset: f32) -> (WidgetTree, WidgetId, WidgetId) {
let mut tree = WidgetTree::new();
let grip = tree.add(Cell::new().tappable().outset(outset));
let row = tree.add(Cell::new().tappable());
let board = tree.add(
Board::new()
.at(grip, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(row, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
let _ = board;
tree.layout(SizeProposal::exact(300.0, 200.0));
(tree, grip, row)
}
#[test]
fn a_grip_outset_wins_over_the_row_it_overlaps() {
let (tree, grip, row) = grip_over_row(9.0);
let finger = touch_pointer();
assert_eq!(tree.hit_test_for(at(114.0, 100.0), &finger), Some(grip));
assert_eq!(tree.hit_test_for(at(92.0, 100.0), &finger), Some(grip));
assert_eq!(tree.hit_test_for(at(140.0, 100.0), &finger), Some(row));
assert_eq!(tree.hit_test_for(at(103.0, 100.0), &finger), Some(grip));
}
#[test]
fn a_grip_outset_wins_over_the_pane_it_overlaps() {
let mut tree = WidgetTree::new();
let grip = tree.add(Cell::new().tappable().outset(9.0));
let left = tree.add(Cell::new().tappable());
let right = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(grip, Rect::new(147.0, 0.0, 6.0, 200.0))
.at(left, Rect::new(0.0, 0.0, 147.0, 200.0))
.at(right, Rect::new(153.0, 0.0, 147.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
let finger = touch_pointer();
assert_eq!(tree.hit_test_for(at(140.0, 100.0), &finger), Some(grip));
assert_eq!(tree.hit_test_for(at(160.0, 100.0), &finger), Some(grip));
assert_eq!(tree.hit_test_for(at(100.0, 100.0), &finger), Some(left));
assert_eq!(tree.hit_test_for(at(200.0, 100.0), &finger), Some(right));
}
#[test]
fn a_grip_outset_wins_over_an_adjacent_grip_by_being_nearer() {
let mut tree = WidgetTree::new();
let a = tree.add(Cell::new().tappable().outset(9.0));
let b = tree.add(Cell::new().tappable().outset(9.0));
let row = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(a, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(b, Rect::new(126.0, 0.0, 6.0, 200.0))
.at(row, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
let finger = touch_pointer();
assert_eq!(tree.hit_test_for(at(110.0, 100.0), &finger), Some(a));
assert_eq!(tree.hit_test_for(at(122.0, 100.0), &finger), Some(b));
assert_eq!(tree.hit_test_for(at(116.0, 100.0), &finger), Some(row));
}
#[test]
fn an_outset_is_zero_for_a_precise_pointer_unless_the_widget_opts_in() {
let (tree, _grip, row) = grip_over_row(9.0);
let mouse = mouse_pointer();
assert_eq!(tree.hit_test_for(at(114.0, 100.0), &mouse), Some(row));
assert_eq!(tree.hit_test(at(114.0, 100.0)), Some(row));
let mut tree = WidgetTree::new();
let grip = tree.add(
Cell::new()
.tappable()
.outset(5.0)
.outset_kinds(PointerKindMask::ALL),
);
let row = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(grip, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(row, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
assert_eq!(tree.hit_test(at(109.0, 100.0)), Some(grip));
assert_eq!(tree.hit_test(at(112.0, 100.0)), Some(row));
}
#[test]
fn an_outset_never_escapes_the_parent() {
let mut tree = WidgetTree::new();
let grip = tree.add(Cell::new().tappable().outset(20.0));
let inner = tree.add(Board::new().at(grip, Rect::new(0.0, 0.0, 6.0, 200.0)));
let outside = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(outside, Rect::new(100.0, 0.0, 200.0, 200.0))
.at(inner, Rect::new(0.0, 0.0, 100.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
let finger = touch_pointer();
assert_eq!(tree.hit_test_for(at(16.0, 100.0), &finger), Some(grip));
assert_eq!(tree.hit_test_for(at(105.0, 100.0), &finger), Some(outside));
}
#[test]
fn an_outset_moves_no_bounds() {
let (plain, plain_grip, plain_row) = grip_over_row(0.0);
let (outset, outset_grip, outset_row) = grip_over_row(9.0);
assert_eq!(plain.bounds(plain_grip), outset.bounds(outset_grip));
assert_eq!(plain.bounds(plain_row), outset.bounds(outset_row));
}
#[test]
fn no_hit_slop_silences_the_outset_as_well() {
fn build(declared: bool, silenced: bool) -> (WidgetTree, WidgetId, WidgetId) {
let mut tree = WidgetTree::new();
let mut cell = Cell::new().tappable();
if declared {
cell = cell.outset(9.0);
}
if silenced {
cell = cell.node_no_slop();
}
let grip = tree.add(cell);
let row = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(grip, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(row, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
(tree, grip, row)
}
fn label(hit: Option<WidgetId>, grip: WidgetId, row: WidgetId) -> &'static str {
match hit {
Some(id) if id == grip => "grip",
Some(id) if id == row => "row",
Some(_) => "other",
None => "none",
}
}
let (bare, bare_grip, bare_row) = build(false, false);
let (loud, loud_grip, loud_row) = build(true, false);
let (quiet, quiet_grip, quiet_row) = build(true, true);
let finger = touch_pointer();
let mut differed = false;
for x in 90..120 {
let p = at(x as f32 + 0.5, 100.0);
let undeclared = label(bare.hit_test_for(p, &finger), bare_grip, bare_row);
let declared = label(loud.hit_test_for(p, &finger), loud_grip, loud_row);
let silenced = label(quiet.hit_test_for(p, &finger), quiet_grip, quiet_row);
assert_eq!(
undeclared, silenced,
"no_hit_slop must reproduce the undeclared behaviour at {p:?}"
);
differed |= undeclared != declared;
}
assert!(
differed,
"the fixture must actually exercise the outset somewhere, or it proves nothing"
);
}
#[test]
fn a_pass_through_node_gets_no_outset() {
let mut tree = WidgetTree::new();
let ghost = tree.add(Cell::new().outset(9.0).pass_through());
let row = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(row, Rect::new(0.0, 0.0, 300.0, 200.0))
.at(ghost, Rect::new(100.0, 0.0, 6.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
assert_eq!(
tree.hit_test_for(at(112.0, 100.0), &touch_pointer()),
Some(row)
);
}
fn isolated_dot(cell: Cell) -> (WidgetTree, WidgetId) {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let dot = tree.add(cell);
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(dot, Rect::new(92.0, 92.0, 16.0, 16.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
(tree, dot)
}
#[test]
fn an_isolated_small_target_catches_a_near_miss() {
let (tree, dot) = isolated_dot(Cell::new().tappable());
let finger = touch_pointer();
assert_eq!(tree.hit_test_for(at(113.0, 100.0), &finger), Some(dot));
assert_ne!(tree.hit_test_for(at(117.0, 100.0), &finger), Some(dot));
assert_eq!(tree.hit_test_for(at(100.0, 100.0), &finger), Some(dot));
}
#[test]
fn a_mouse_press_is_never_re_attributed() {
let (tree, dot) = isolated_dot(Cell::new().tappable());
let mouse = mouse_pointer();
assert_ne!(tree.hit_test_for(at(113.0, 100.0), &mouse), Some(dot));
assert_eq!(
tree.hit_test(at(113.0, 100.0)),
tree.hit_test_for(at(113.0, 100.0), &mouse)
);
}
#[test]
fn a_row_label_five_dp_from_an_inline_checkbox_still_bubbles_to_the_row() {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let row_taps = Rc::new(StdCell::new(0));
let box_taps = Rc::new(StdCell::new(0));
let checkbox = tree.add(Cell::new().counting(&box_taps));
let label = tree.add(Cell::new());
let row = tree.add(Cell::new().counting(&row_taps).child(checkbox).child(label));
tree.add(Board::new().at(row, Rect::new(0.0, 0.0, 300.0, 48.0)));
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let checkbox = tree.add(Cell::new().counting(&box_taps));
let label = tree.add(Cell::new());
let inner = tree.add(
Board::new()
.at(checkbox, Rect::new(8.0, 16.0, 16.0, 16.0))
.at(label, Rect::new(29.0, 0.0, 271.0, 48.0)),
);
let row = tree.add(Cell::new().counting(&row_taps).child(inner));
tree.add(Board::new().at(row, Rect::new(0.0, 0.0, 300.0, 48.0)));
tree.layout(SizeProposal::exact(300.0, 48.0));
let finger = touch_pointer();
let hit = tree.hit_test_for(at(29.0, 24.0), &finger);
assert_eq!(hit, Some(label), "the exact pass still lands on the label");
let owner = std::iter::successors(hit, |id| tree.arena.get(*id).and_then(|n| n.parent))
.find(|id| tree.arena.takes_a_press(*id));
assert_eq!(
owner,
Some(row),
"the bubble owner is the row, not the checkbox"
);
assert_ne!(hit, Some(checkbox));
}
#[test]
fn with_no_owner_on_the_path_the_near_miss_lands_on_the_control() {
let box_taps = Rc::new(StdCell::new(0));
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let checkbox = tree.add(Cell::new().counting(&box_taps));
let label = tree.add(Cell::new());
let row = tree.add(
Board::new()
.at(checkbox, Rect::new(8.0, 16.0, 16.0, 16.0))
.at(label, Rect::new(29.0, 0.0, 271.0, 48.0)),
);
tree.add(Board::new().at(row, Rect::new(0.0, 0.0, 300.0, 48.0)));
tree.layout(SizeProposal::exact(300.0, 48.0));
assert_eq!(
tree.hit_test_for(at(29.0, 24.0), &touch_pointer()),
Some(checkbox)
);
}
#[test]
fn a_disabled_control_is_never_a_candidate() {
let (tree, dot) = isolated_dot(Cell::new().tappable().disabled());
assert_ne!(
tree.hit_test_for(at(113.0, 100.0), &touch_pointer()),
Some(dot)
);
}
#[test]
fn a_read_only_surface_is_never_a_candidate() {
let (tree, dot) = isolated_dot(Cell::new().tappable());
let t = touch();
let point = at(113.0, 100.0);
let open = HitContext::new(PointerKind::Touch, &t);
assert_eq!(
tree.arena.hit_test_at_with_slop(point, None, &open),
Some(dot)
);
let refuses = |id: WidgetId| id == dot;
let closed = HitContext::new(PointerKind::Touch, &t).read_only_probe(&refuses);
assert_ne!(
tree.arena.hit_test_at_with_slop(point, None, &closed),
Some(dot)
);
}
#[test]
fn a_hit_transparent_subtree_is_pruned_from_the_pass() {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let inner = tree.add(Cell::new().tappable());
let badge = tree.add(Cell::new().transparent().child(inner));
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(badge, Rect::new(92.0, 92.0, 16.0, 16.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
let hit = tree.hit_test_for(at(113.0, 100.0), &touch_pointer());
assert_ne!(hit, Some(inner));
assert_ne!(hit, Some(badge));
}
#[test]
fn event_pass_through_is_not_a_candidate_but_its_children_are() {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let inner = tree.add(Cell::new().tappable());
let ghost = tree.add(Cell::new().tappable().pass_through().child(inner));
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(ghost, Rect::new(92.0, 92.0, 16.0, 16.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
let t = touch();
let hit = HitContext::new(PointerKind::Touch, &t);
let ids: Vec<WidgetId> = tree
.arena
.hit_candidates(tree.arena.roots()[0], at(113.0, 100.0), None, &hit)
.into_iter()
.map(|c| c.id)
.collect();
assert!(
ids.contains(&inner),
"the child of a pass-through node stays eligible"
);
assert!(
!ids.contains(&ghost),
"the pass-through node itself does not"
);
}
#[test]
fn slop_never_escapes_a_clips_children_ancestor() {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let dot = tree.add(Cell::new().tappable());
let inner = tree.add(Board::new().at(dot, Rect::new(84.0, 92.0, 16.0, 16.0)));
let clipper = tree.add(Cell::new().clips().child(inner));
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(clipper, Rect::new(0.0, 0.0, 100.0, 200.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
let finger = touch_pointer();
assert_eq!(tree.hit_test_for(at(96.0, 100.0), &finger), Some(dot));
assert_ne!(tree.hit_test_for(at(104.0, 100.0), &finger), Some(dot));
}
#[test]
fn candidates_are_restricted_to_the_topmost_overlay_layer() {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let page_dot = tree.add(Cell::new().tappable());
let anchor = tree.add(Cell::new());
tree.add(
Board::new()
.at(page_dot, Rect::new(40.0, 100.0, 16.0, 16.0))
.at(anchor, Rect::new(60.0, 60.0, 16.0, 16.0)),
);
let panel = tree.add(Cell::new().size(120.0, 120.0));
tree.show_overlay(crate::overlay::OverlayRequest {
content_id: panel,
anchor,
placement: crate::overlay::OverlayPlacement::Below,
dismiss: crate::overlay::DismissBehavior::Manual,
layer: crate::overlay::OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
});
tree.layout(SizeProposal::exact(200.0, 200.0));
let panel_bounds = tree.bounds(panel);
assert!(
panel_bounds.width > 0.0 && panel_bounds.height > 0.0,
"the fixture needs a real overlay rectangle, got {panel_bounds:?}"
);
let dot_bounds = tree.bounds(page_dot);
let probe = at(dot_bounds.right() + 4.5, dot_bounds.center().y);
assert!(
panel_bounds.contains(probe),
"the probe {probe:?} must lie inside the overlay {panel_bounds:?}"
);
let finger = touch_pointer();
let hit = tree.hit_test_for(probe, &finger);
assert_ne!(
hit,
Some(page_dot),
"the page behind the overlay is out of reach"
);
assert_eq!(hit, Some(panel));
let overlay_id = tree.active_overlays()[0];
tree.dismiss_overlay(overlay_id);
tree.layout(SizeProposal::exact(200.0, 200.0));
assert_eq!(tree.hit_test_for(probe, &finger), Some(page_dot));
}
#[test]
fn a_transform_inverse_maps_the_point_and_scales_the_radius() {
fn tree_with_scale(scale: f32) -> (WidgetTree, WidgetId) {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let dot = tree.add(Cell::new().tappable());
let scaled = tree.add(Board::new().at(dot, Rect::new(0.0, 0.0, 16.0, 16.0)));
tree.set_transform(scaled, teksilo_canvas::Transform2D::scale(scale, scale));
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(scaled, Rect::new(0.0, 0.0, 16.0, 16.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
(tree, dot)
}
let finger = touch_pointer();
let (tree, dot) = tree_with_scale(1.0);
assert_eq!(tree.hit_test_for(at(23.0, 8.0), &finger), Some(dot));
assert_ne!(tree.hit_test_for(at(25.0, 8.0), &finger), Some(dot));
let (tree, dot) = tree_with_scale(2.0);
assert_eq!(tree.hit_test_for(at(35.0, 16.0), &finger), Some(dot));
assert_eq!(tree.hit_test_for(at(39.0, 16.0), &finger), Some(dot));
assert_ne!(tree.hit_test_for(at(41.0, 16.0), &finger), Some(dot));
}
#[test]
fn a_round_control_measures_to_its_disc() {
let (round, dot_round) = isolated_dot(Cell::new().tappable().round());
let (square, dot_square) = isolated_dot(Cell::new().tappable());
let finger = touch_pointer();
assert_eq!(
round.hit_test_for(at(113.0, 100.0), &finger),
Some(dot_round)
);
assert_eq!(
square.hit_test_for(at(113.0, 100.0), &finger),
Some(dot_square)
);
let corner = at(113.0, 113.0);
assert_eq!(square.hit_test_for(corner, &finger), Some(dot_square));
assert_ne!(round.hit_test_for(corner, &finger), Some(dot_round));
}
fn wrapped_dot(cell: Cell) -> (WidgetTree, WidgetId) {
use crate::widget_builder::WidgetBuilder;
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let dot = tree.add(cell.on_tap(|_e, _ctx| {}));
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(dot, Rect::new(92.0, 92.0, 16.0, 16.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
(tree, dot)
}
#[test]
fn a_wrapped_widgets_hit_distance_still_measures_to_its_disc() {
let (round, dot) = wrapped_dot(Cell::new().round());
let finger = touch_pointer();
assert_eq!(round.hit_test_for(at(113.0, 100.0), &finger), Some(dot));
assert_ne!(round.hit_test_for(at(113.0, 113.0), &finger), Some(dot));
}
#[test]
fn a_wrapped_widgets_hit_shape_still_refuses_a_press_outside_it() {
let (shaped, dot) = wrapped_dot(Cell::new().shaped_disc());
let mouse = crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO);
assert_eq!(shaped.hit_test_for(at(100.0, 100.0), &mouse), Some(dot));
assert_ne!(shaped.hit_test_for(at(93.0, 93.0), &mouse), Some(dot));
}
#[test]
fn a_widget_can_withdraw_by_reporting_no_distance() {
let (tree, dot) = isolated_dot(Cell::new().tappable().no_distance());
assert_ne!(
tree.hit_test_for(at(113.0, 100.0), &touch_pointer()),
Some(dot)
);
}
#[test]
fn an_inert_node_is_never_a_candidate() {
let (tree, dot) = isolated_dot(Cell::new());
assert_ne!(
tree.hit_test_for(at(113.0, 100.0), &touch_pointer()),
Some(dot)
);
}
#[test]
fn the_nearest_candidate_wins() {
let mut tree = WidgetTree::new();
tree.set_input_density(TargetDensity::Touch);
let near = tree.add(Cell::new().tappable());
let far = tree.add(Cell::new().tappable());
let back = tree.add(Cell::new());
tree.add(
Board::new()
.at(back, Rect::new(0.0, 0.0, 200.0, 200.0))
.at(near, Rect::new(84.0, 92.0, 16.0, 16.0))
.at(far, Rect::new(106.0, 92.0, 16.0, 16.0)),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
assert_eq!(
tree.hit_test_for(at(102.0, 100.0), &touch_pointer()),
Some(near)
);
assert_eq!(
tree.hit_test_for(at(104.0, 100.0), &touch_pointer()),
Some(far)
);
}
#[test]
fn the_precedence_chain_resolves_in_order() {
let point = at(113.0, 100.0);
let finger = touch_pointer();
let (tree, dot) = isolated_dot(Cell::new().tappable());
assert_eq!(tree.hit_test_for(point, &finger), Some(dot));
let (tree, dot) = isolated_dot(Cell::new().tappable().widget_slop(HitSlop {
radius: 2.0,
up_to: 44.0,
}));
assert_ne!(tree.hit_test_for(point, &finger), Some(dot));
let (tree, dot) = isolated_dot(
Cell::new()
.tappable()
.widget_slop(HitSlop {
radius: 2.0,
up_to: 44.0,
})
.node_slop(HitSlop {
radius: 8.0,
up_to: 44.0,
}),
);
assert_eq!(tree.hit_test_for(point, &finger), Some(dot));
let (tree, dot) = isolated_dot(
Cell::new()
.tappable()
.widget_slop(HitSlop {
radius: 8.0,
up_to: 44.0,
})
.node_slop(HitSlop {
radius: 8.0,
up_to: 44.0,
})
.node_no_slop(),
);
assert_ne!(tree.hit_test_for(point, &finger), Some(dot));
}
#[test]
fn mouse_hit_tests_are_unchanged_by_either_declaration() {
fn build(declare: bool) -> (WidgetTree, WidgetId, WidgetId) {
let mut tree = WidgetTree::new();
let grip = if declare {
Cell::new().tappable().outset(9.0).node_slop(HitSlop {
radius: 16.0,
up_to: 64.0,
})
} else {
Cell::new().tappable()
};
let a = tree.add(grip);
let b = tree.add(Cell::new().tappable());
tree.add(
Board::new()
.at(a, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(b, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
(tree, a, b)
}
fn label(hit: Option<WidgetId>, a: WidgetId, b: WidgetId) -> &'static str {
match hit {
Some(id) if id == a => "grip",
Some(id) if id == b => "row",
Some(_) => "other",
None => "none",
}
}
let (bare, bare_a, bare_b) = build(false);
let (rich, rich_a, rich_b) = build(true);
let mouse = mouse_pointer();
for x in 0..300 {
for y in [0, 47, 100, 199] {
let p = at(x as f32 + 0.5, y as f32 + 0.5);
let plain = label(bare.hit_test(p), bare_a, bare_b);
let declared = label(rich.hit_test(p), rich_a, rich_b);
assert_eq!(plain, declared, "mouse hit differs at {p:?}");
let via_pointer = label(rich.hit_test_for(p, &mouse), rich_a, rich_b);
assert_eq!(
declared, via_pointer,
"hit_test_for(mouse) differs from hit_test at {p:?}"
);
}
}
}
#[test]
fn compact_layout_is_unchanged_by_either_declaration() {
let build = |declare: bool| {
let mut tree = WidgetTree::new();
let cell = if declare {
Cell::new().tappable().outset(9.0).node_slop(HitSlop {
radius: 16.0,
up_to: 64.0,
})
} else {
Cell::new().tappable()
};
let a = tree.add(cell);
let b = tree.add(Cell::new().tappable());
let board = tree.add(
Board::new()
.at(a, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(b, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
(tree.bounds(a), tree.bounds(b), tree.bounds(board))
};
assert_eq!(build(false), build(true));
assert_eq!(
InputTokens::default().density,
TargetDensity::Compact,
"the default ladder must stay Compact for this to mean anything"
);
}
#[test]
fn a_vetoed_child_falls_through_to_the_sibling_beneath_it() {
let mut tree = WidgetTree::new();
let under = tree.add(Cell::new().tappable());
let over = tree.add(Cell::new().tappable());
let _board = tree.add(
Board::new()
.at(under, Rect::new(0.0, 0.0, 300.0, 200.0))
.at(over, Rect::new(0.0, 0.0, 300.0, 200.0))
.veto(over, Rect::new(0.0, 0.0, 150.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
let mouse = mouse_pointer();
assert_eq!(
tree.hit_test_for(at(50.0, 100.0), &mouse),
Some(under),
"inside the vetoed region the point must fall through to the sibling \
beneath, exactly as a `hit_shape` rejection does",
);
assert_eq!(
tree.hit_test_for(at(250.0, 100.0), &mouse),
Some(over),
"one region is refused, not the child — outside it the top child still \
wins",
);
}
#[test]
fn a_vetoed_child_falls_through_to_the_parent_when_nothing_is_beneath() {
let mut tree = WidgetTree::new();
let card = tree.add(Cell::new().tappable());
let board = tree.add(
Board::new()
.at(card, Rect::new(50.0, 50.0, 100.0, 100.0))
.veto(card, Rect::new(0.0, 0.0, 300.0, 200.0)),
);
tree.layout(SizeProposal::exact(300.0, 200.0));
let mouse = mouse_pointer();
assert_eq!(tree.hit_test_for(at(100.0, 100.0), &mouse), Some(board));
}
#[test]
fn the_outset_pre_pass_does_not_ask_the_veto_about_children_without_one() {
let build = |siblings: usize| {
let mut tree = WidgetTree::new();
let mut board = Board::new();
let asked = Rc::new(StdCell::new(0));
board = board.counting(asked.clone());
for _ in 0..siblings {
let child = tree.add(Cell::new().tappable());
board = board.at(child, Rect::new(0.0, 0.0, 300.0, 200.0));
}
let top = tree.add(Cell::new().tappable());
board = board.at(top, Rect::new(0.0, 0.0, 300.0, 200.0));
let _board = tree.add(board);
tree.layout(SizeProposal::exact(300.0, 200.0));
asked.set(0);
let hit = tree.hit_test_for(at(150.0, 100.0), &touch_pointer());
assert_eq!(hit, Some(top), "precondition: the topmost child wins");
asked.get()
};
assert_eq!(build(1), 1, "two children");
assert_eq!(
build(5),
1,
"six children, and still one question: the pre-pass asks nobody when \
nobody declares an outset",
);
}
#[test]
fn an_outset_grip_does_not_win_a_point_its_parent_vetoed() {
let build = |vetoed: bool| {
let mut tree = WidgetTree::new();
let grip = tree.add(Cell::new().tappable().outset(8.0));
let row = tree.add(Cell::new().tappable());
let mut board = Board::new()
.at(grip, Rect::new(100.0, 0.0, 6.0, 200.0))
.at(row, Rect::new(0.0, 0.0, 300.0, 200.0));
if vetoed {
board = board.veto(grip, Rect::new(0.0, 0.0, 300.0, 200.0));
}
let _board = tree.add(board);
tree.layout(SizeProposal::exact(300.0, 200.0));
let finger = touch_pointer();
(tree.hit_test_for(at(112.0, 100.0), &finger), grip, row)
};
let (unvetoed, grip, _) = build(false);
assert_eq!(
unvetoed,
Some(grip),
"precondition: without the veto the grip wins its ring",
);
let (vetoed, grip, row) = build(true);
assert_ne!(vetoed, Some(grip));
assert_eq!(vetoed, Some(row), "the ring falls through like the body");
}