use crate::event::UiTarget;
use crate::state::UiState;
use crate::tree::{El, Rect};
pub fn hit_test(root: &El, ui_state: &UiState, point: (f32, f32)) -> Option<String> {
hit_test_target(root, ui_state, point).map(|target| target.key)
}
pub fn hit_test_target(root: &El, ui_state: &UiState, point: (f32, f32)) -> Option<UiTarget> {
match hit_test_rec(root, ui_state, point, None, (0.0, 0.0)) {
Hit::Target(target) => Some(target),
Hit::Blocked | Hit::Miss => None,
}
}
enum Hit {
Target(UiTarget),
Blocked,
Miss,
}
fn hit_test_rec(
node: &El,
ui_state: &UiState,
point: (f32, f32),
inherited_clip: Option<Rect>,
inherited_translate: (f32, f32),
) -> Hit {
if let Some(clip) = inherited_clip
&& !clip.contains(point.0, point.1)
{
return Hit::Miss;
}
let total_translate = (
inherited_translate.0 + node.translate.0,
inherited_translate.1 + node.translate.1,
);
let computed = ui_state.rect(&node.computed_id);
let translated_rect = translated(computed, total_translate);
let painted_rect = scaled_around_center(translated_rect, node.scale);
let child_clip = if node.clip {
match inherited_clip {
Some(clip) => Some(
clip.intersect(painted_rect)
.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0)),
),
None => Some(painted_rect),
}
} else {
inherited_clip
};
for child in node.children.iter().rev() {
match hit_test_rec(child, ui_state, point, child_clip, total_translate) {
Hit::Target(target) => return Hit::Target(target),
Hit::Blocked => return Hit::Blocked,
Hit::Miss => {}
}
}
if !painted_rect.contains(point.0, point.1) {
return Hit::Miss;
}
if let Some(key) = &node.key {
return Hit::Target(UiTarget {
key: key.clone(),
node_id: node.computed_id.clone(),
rect: painted_rect,
});
}
if node.block_pointer {
return Hit::Blocked;
}
Hit::Miss
}
pub(crate) fn scroll_target_at(root: &El, ui_state: &UiState, point: (f32, f32)) -> Option<String> {
let mut hit = None;
scroll_target_rec(root, ui_state, point, None, (0.0, 0.0), &mut hit);
hit
}
fn scroll_target_rec(
node: &El,
ui_state: &UiState,
point: (f32, f32),
inherited_clip: Option<Rect>,
inherited_translate: (f32, f32),
out: &mut Option<String>,
) {
if let Some(clip) = inherited_clip
&& !clip.contains(point.0, point.1)
{
return;
}
let total_translate = (
inherited_translate.0 + node.translate.0,
inherited_translate.1 + node.translate.1,
);
let computed = ui_state.rect(&node.computed_id);
let translated_rect = translated(computed, total_translate);
let painted_rect = scaled_around_center(translated_rect, node.scale);
if node.scrollable && painted_rect.contains(point.0, point.1) {
*out = Some(node.computed_id.clone());
}
let child_clip = if node.clip {
match inherited_clip {
Some(clip) => Some(
clip.intersect(painted_rect)
.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0)),
),
None => Some(painted_rect),
}
} else {
inherited_clip
};
for c in &node.children {
scroll_target_rec(c, ui_state, point, child_clip, total_translate, out);
}
}
fn translated(r: Rect, offset: (f32, f32)) -> Rect {
if offset.0 == 0.0 && offset.1 == 0.0 {
return r;
}
Rect::new(r.x + offset.0, r.y + offset.1, r.w, r.h)
}
fn scaled_around_center(r: Rect, s: f32) -> Rect {
if (s - 1.0).abs() < f32::EPSILON {
return r;
}
let cx = r.center_x();
let cy = r.center_y();
let w = r.w * s;
let h = r.h * s;
Rect::new(cx - w * 0.5, cy - h * 0.5, w, h)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::layout;
use crate::state::UiState;
use crate::tree::*;
use crate::{button, column, row};
fn lay_out_counter() -> (El, UiState) {
let mut tree = column([
crate::text("0"),
row([button("-").key("dec"), button("+").key("inc")]),
])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
(tree, state)
}
fn find_rect(node: &El, state: &UiState, key: &str) -> Option<Rect> {
if node.key.as_deref() == Some(key) {
return Some(state.rect(&node.computed_id));
}
node.children.iter().find_map(|c| find_rect(c, state, key))
}
fn find_text_rect(node: &El, state: &UiState) -> Option<Rect> {
if matches!(node.kind, Kind::Text) {
return Some(state.rect(&node.computed_id));
}
node.children.iter().find_map(|c| find_text_rect(c, state))
}
#[test]
fn hit_test_finds_keyed_button() {
let (tree, state) = lay_out_counter();
for key in &["dec", "inc"] {
let r = find_rect(&tree, &state, key).expect("button rect");
let center = (r.x + r.w * 0.5, r.y + r.h * 0.5);
let hit = hit_test(&tree, &state, center);
assert_eq!(hit.as_deref(), Some(*key));
}
}
#[test]
fn hit_test_misses_unkeyed_text() {
let (tree, state) = lay_out_counter();
let r = find_text_rect(&tree, &state).expect("text rect");
let center = (r.x + r.w * 0.5, r.y + r.h * 0.5);
assert!(hit_test(&tree, &state, center).is_none());
}
#[test]
fn hit_test_outside_returns_none() {
let (tree, state) = lay_out_counter();
assert!(hit_test(&tree, &state, (-10.0, -10.0)).is_none());
assert!(hit_test(&tree, &state, (9999.0, 9999.0)).is_none());
}
#[test]
fn hit_test_respects_clipping_ancestor() {
let mut tree = column([row([
button("-").key("visible"),
button("+").key("clipped").width(Size::Fixed(240.0)),
])
.clip()
.width(Size::Fixed(80.0))]);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 100.0));
let clipped = find_rect(&tree, &state, "clipped").expect("clipped button rect");
assert!(hit_test(&tree, &state, (clipped.center_x(), clipped.center_y())).is_none());
}
#[test]
fn hit_test_follows_ancestor_translate() {
let mut tree = row([
column([button("A").key("a")]).translate(120.0, 0.0),
button("B").key("b"),
]);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 100.0));
let untranslated = find_rect(&tree, &state, "a").expect("a layout rect");
let translated_center = (untranslated.center_x() + 120.0, untranslated.center_y());
let untranslated_center = (untranslated.center_x(), untranslated.center_y());
assert_eq!(
hit_test(&tree, &state, translated_center).as_deref(),
Some("a"),
"click at translated location should hit the translated button"
);
assert_ne!(
hit_test(&tree, &state, untranslated_center).as_deref(),
Some("a"),
"click at the un-translated layout slot must not hit the translated button"
);
}
#[test]
fn hit_test_child_lifted_above_parent_still_hits() {
let mut tree = row([crate::card("c", [crate::text("body")])
.key("swatch")
.width(Size::Fixed(120.0))
.height(Size::Fixed(120.0))
.scale(1.15)
.translate(0.0, -20.0)]);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
let layout_rect = find_rect(&tree, &state, "swatch").expect("swatch rect");
let painted_top_y = layout_rect.y - 20.0 - layout_rect.h * 0.075 + 1.0;
let painted_top_x = layout_rect.center_x();
assert_eq!(
hit_test(&tree, &state, (painted_top_x, painted_top_y)).as_deref(),
Some("swatch"),
"click on lifted top of scaled+translated child should hit"
);
}
#[test]
fn hit_test_translate_inherits_to_descendants() {
let mut tree = column([row([button("X").key("x")]).translate(0.0, 50.0)]);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
let pre = find_rect(&tree, &state, "x").expect("x layout rect");
let translated = (pre.center_x(), pre.center_y() + 50.0);
assert_eq!(
hit_test(&tree, &state, translated).as_deref(),
Some("x"),
"ancestor translate must accumulate to descendants"
);
}
#[test]
fn unkeyed_blocking_node_stops_fallthrough() {
use crate::tree::stack;
let mut tree = stack([
El::new(Kind::Scrim)
.key("dismiss")
.fill(crate::tokens::OVERLAY_SCRIM)
.fill_size(),
El::new(Kind::Modal)
.block_pointer()
.width(Size::Fixed(100.0))
.height(Size::Fixed(100.0)),
])
.align(Align::Center)
.justify(Justify::Center)
.fill_size();
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 300.0, 300.0));
assert!(hit_test(&tree, &state, (150.0, 150.0)).is_none());
assert_eq!(
hit_test(&tree, &state, (10.0, 10.0)).as_deref(),
Some("dismiss")
);
}
}