#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
#[allow(clippy::float_cmp)] #[allow(clippy::module_inception)] mod tests {
use super::*;
#[test]
fn test_dom_node_tag_roundtrip() {
let tag = HitTestTag::DomNode {
tag_id: TagId { inner: 42 },
};
let item_tag = tag.to_item_tag();
let decoded = HitTestTag::from_item_tag(item_tag).unwrap();
assert_eq!(tag, decoded);
}
#[test]
fn test_scrollbar_tag_roundtrip() {
let tag = HitTestTag::Scrollbar {
dom_id: DomId { inner: 1 },
node_id: NodeId::new(123),
component: ScrollbarComponent::VerticalThumb,
};
let item_tag = tag.to_item_tag();
let decoded = HitTestTag::from_item_tag(item_tag).unwrap();
assert_eq!(tag, decoded);
}
#[test]
fn test_dom_node_tag_not_confused_with_scrollbar() {
let dom_tag = HitTestTag::DomNode {
tag_id: TagId { inner: 673 },
};
let item_tag = dom_tag.to_item_tag();
assert_eq!(item_tag.1, TAG_TYPE_DOM_NODE);
let decoded = HitTestTag::from_item_tag(item_tag).unwrap();
assert!(decoded.is_dom_node());
assert!(!decoded.is_scrollbar());
}
#[test]
fn test_legacy_tag_compatibility() {
let legacy_tag = (42u64, 0u16);
let decoded = HitTestTag::from_item_tag(legacy_tag).unwrap();
assert!(decoded.is_dom_node());
assert_eq!(decoded.as_dom_node().unwrap().inner, 42);
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
}
#[test]
fn scroll_state_clamps_to_content_minus_viewport() {
let max = max_scroll_rect(&OverflowingScrollNode {
parent_rect: rect(0.0, 0.0, 100.0, 100.0),
child_rect: rect(0.0, 0.0, 100.0, 300.0),
..Default::default()
});
assert_eq!(max.size.height, 200.0);
let mut st = ScrollState::default();
st.set(0.0, 999.0, &max);
assert_eq!(st.scroll_position.y, 200.0); }
#[test]
fn scroll_state_no_scroll_when_content_fits() {
let max = max_scroll_rect(&OverflowingScrollNode {
parent_rect: rect(0.0, 0.0, 100.0, 100.0),
child_rect: rect(0.0, 0.0, 100.0, 100.0),
..Default::default()
});
let mut st = ScrollState::default();
st.add(50.0, 50.0, &max);
assert_eq!(st.scroll_position, LogicalPosition::zero());
}
#[test]
fn scroll_state_nan_delta_does_not_poison() {
let max = rect(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState::default();
st.add(f32::NAN, f32::NAN, &max);
assert_eq!(st.scroll_position, LogicalPosition::zero());
}
#[test]
fn selection_tag_out_of_range_domid_is_clamped_not_corrupting() {
let tag = HitTestTag::Selection {
dom_id: DomId { inner: 0x1_0007 }, container_node_id: NodeId::new(5),
text_run_index: 9,
};
let (value, ty) = tag.to_item_tag();
assert_eq!(ty, TAG_TYPE_SELECTION);
assert_eq!((value >> 16) & 0xFFFF_FFFF, 5);
assert_eq!(value >> 48, 0x0007);
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::unreadable_literal
)]
mod autotest_generated {
use super::*;
fn r(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
}
fn scroll_node_of(parent: LogicalRect, child: LogicalRect) -> OverflowingScrollNode {
OverflowingScrollNode {
parent_rect: parent,
child_rect: child,
..Default::default()
}
}
fn ext_id(raw: u64) -> ExternalScrollId {
ExternalScrollId(raw, PipelineId::DUMMY)
}
const ALL_CURSORS: [CursorType; 21] = [
CursorType::Default,
CursorType::Pointer,
CursorType::Text,
CursorType::Crosshair,
CursorType::Move,
CursorType::NotAllowed,
CursorType::Grab,
CursorType::Grabbing,
CursorType::EResize,
CursorType::WResize,
CursorType::NResize,
CursorType::SResize,
CursorType::EwResize,
CursorType::NsResize,
CursorType::NeswResize,
CursorType::NwseResize,
CursorType::ColResize,
CursorType::RowResize,
CursorType::Wait,
CursorType::Help,
CursorType::Progress,
];
const ALL_SCROLLBAR_COMPONENTS: [ScrollbarComponent; 4] = [
ScrollbarComponent::VerticalTrack,
ScrollbarComponent::VerticalThumb,
ScrollbarComponent::HorizontalTrack,
ScrollbarComponent::HorizontalThumb,
];
#[test]
fn hit_test_empty_is_neutral_element() {
let h = HitTest::empty();
assert!(h.is_empty());
assert_eq!(h.regular_hit_test_nodes.len(), 0);
assert_eq!(h.scroll_hit_test_nodes.len(), 0);
assert_eq!(h.scrollbar_hit_test_nodes.len(), 0);
assert_eq!(h.cursor_hit_test_nodes.len(), 0);
assert_eq!(HitTest::empty(), HitTest::empty());
}
#[test]
fn hit_test_is_empty_false_if_any_single_map_is_populated() {
let mut a = HitTest::empty();
a.regular_hit_test_nodes.insert(
NodeId::ZERO,
HitTestItem {
point_in_viewport: LogicalPosition::zero(),
point_relative_to_item: Default::default(),
is_focusable: false,
is_virtual_view_hit: None,
hit_depth: 0,
},
);
assert!(!a.is_empty());
let mut b = HitTest::empty();
b.scroll_hit_test_nodes.insert(
NodeId::new(usize::MAX),
ScrollHitTestItem {
point_in_viewport: LogicalPosition::zero(),
point_relative_to_item: Default::default(),
scroll_node: OverflowingScrollNode::default(),
},
);
assert!(!b.is_empty());
let mut c = HitTest::empty();
c.scrollbar_hit_test_nodes.insert(
ScrollbarHitId::VerticalTrack(DomId::ROOT_ID, NodeId::ZERO),
ScrollbarHitTestItem {
point_in_viewport: LogicalPosition::zero(),
point_relative_to_item: Default::default(),
orientation: ScrollbarOrientation::Vertical,
},
);
assert!(!c.is_empty());
let mut d = HitTest::empty();
d.cursor_hit_test_nodes.insert(
NodeId::ZERO,
CursorHitTestItem {
cursor_type: CursorType::Default,
hit_depth: u32::MAX,
point_in_viewport: LogicalPosition::new(f32::NAN, f32::INFINITY),
},
);
assert!(!d.is_empty());
}
#[test]
fn full_hit_test_empty_is_empty_regardless_of_focused_node() {
let none = FullHitTest::empty(None);
assert!(none.is_empty());
assert!(none.focused_node.is_none());
let focused = FullHitTest::empty(Some(DomNodeId::ROOT));
assert!(focused.is_empty());
assert!(focused.focused_node.is_some());
assert_eq!(focused.hovered_nodes.len(), 0);
}
#[test]
fn full_hit_test_is_empty_false_once_a_dom_is_hovered() {
let mut f = FullHitTest::empty(None);
f.hovered_nodes.insert(DomId::ROOT_ID, HitTest::empty());
assert!(!f.is_empty());
}
#[test]
fn pipeline_id_new_is_monotonic_and_second_field_is_zero() {
let a = PipelineId::new();
let b = PipelineId::new();
let c = PipelineId::default();
assert!(b.0 > a.0);
assert!(c.0 > b.0);
assert_eq!(a.1, 0);
assert_eq!(b.1, 0);
assert_eq!(PipelineId::DUMMY, PipelineId(0, 0));
}
#[test]
fn pipeline_id_display_and_debug_agree_on_edge_values() {
assert_eq!(alloc::format!("{}", PipelineId::DUMMY), "PipelineId(0, 0)");
let maxed = PipelineId(u32::MAX, u32::MAX);
let shown = alloc::format!("{maxed}");
assert_eq!(shown, "PipelineId(4294967295, 4294967295)");
assert_eq!(alloc::format!("{maxed:?}"), shown);
}
#[test]
fn document_id_display_handles_min_and_max() {
let zero = DocumentId {
namespace_id: IdNamespace(0),
id: 0,
};
let maxed = DocumentId {
namespace_id: IdNamespace(u32::MAX),
id: u32::MAX,
};
for d in [zero, maxed] {
let shown = alloc::format!("{d}");
assert!(!shown.is_empty());
assert!(shown.starts_with("DocumentId {"));
assert_eq!(alloc::format!("{d:?}"), shown);
}
assert!(alloc::format!("{maxed}").contains("4294967295"));
}
#[test]
fn external_scroll_id_display_omits_the_pipeline_but_keys_stay_distinct() {
let a = ExternalScrollId(7, PipelineId(1, 0));
let b = ExternalScrollId(7, PipelineId(2, 0));
assert_eq!(alloc::format!("{a}"), "ExternalScrollId(7)");
assert_eq!(alloc::format!("{a:?}"), alloc::format!("{b:?}"));
assert_ne!(a, b);
let mut states = ScrollStates::new();
states.0.insert(a, ScrollState::default());
states.0.insert(b, ScrollState::default());
assert_eq!(states.0.len(), 2);
}
#[test]
fn external_scroll_id_display_no_panic_at_u64_max() {
let shown = alloc::format!("{}", ExternalScrollId(u64::MAX, PipelineId::DUMMY));
assert_eq!(shown, "ExternalScrollId(18446744073709551615)");
}
#[test]
fn max_scroll_rect_is_content_minus_viewport_and_keeps_origin() {
let max = max_scroll_rect(&scroll_node_of(
r(0.0, 0.0, 100.0, 100.0),
r(-12.5, 7.25, 400.0, 300.0),
));
assert_eq!(max.size.width, 300.0);
assert_eq!(max.size.height, 200.0);
assert_eq!(max.origin.x, -12.5);
assert_eq!(max.origin.y, 7.25);
}
#[test]
fn max_scroll_rect_clamps_negative_range_to_zero() {
let max = max_scroll_rect(&scroll_node_of(
r(0.0, 0.0, 500.0, 500.0),
r(0.0, 0.0, 100.0, 100.0),
));
assert_eq!(max.size.width, 0.0);
assert_eq!(max.size.height, 0.0);
}
#[test]
fn max_scroll_rect_nan_and_infinite_sizes_do_not_produce_nan() {
let nan = max_scroll_rect(&scroll_node_of(
r(0.0, 0.0, 10.0, 10.0),
r(0.0, 0.0, f32::NAN, f32::NAN),
));
assert!(!nan.size.width.is_nan());
assert!(!nan.size.height.is_nan());
assert_eq!(nan.size.width, 0.0);
assert_eq!(nan.size.height, 0.0);
let both_inf = max_scroll_rect(&scroll_node_of(
r(0.0, 0.0, f32::INFINITY, f32::INFINITY),
r(0.0, 0.0, f32::INFINITY, f32::INFINITY),
));
assert_eq!(both_inf.size.width, 0.0);
assert_eq!(both_inf.size.height, 0.0);
let inf = max_scroll_rect(&scroll_node_of(
r(0.0, 0.0, 10.0, 10.0),
r(0.0, 0.0, f32::INFINITY, f32::INFINITY),
));
assert!(inf.size.width.is_infinite() && inf.size.width.is_sign_positive());
assert!(inf.size.height.is_infinite() && inf.size.height.is_sign_positive());
}
#[test]
fn max_scroll_rect_at_float_extremes_does_not_panic() {
let max = max_scroll_rect(&scroll_node_of(
r(f32::MIN, f32::MIN, f32::MIN_POSITIVE, f32::MAX),
r(f32::MAX, f32::MAX, f32::MAX, f32::MIN_POSITIVE),
));
assert!(max.size.width >= 0.0);
assert!(max.size.height >= 0.0);
assert!(!max.size.width.is_nan());
assert!(!max.size.height.is_nan());
}
#[test]
fn scroll_state_default_and_get_round_trip() {
let st = ScrollState::default();
assert_eq!(st.get(), LogicalPosition::zero());
let st = ScrollState {
scroll_position: LogicalPosition::new(3.5, -4.25),
};
assert_eq!(st.get().x, 3.5);
assert_eq!(st.get().y, -4.25);
}
#[test]
fn scroll_state_set_zero_is_identity_within_range() {
let max = r(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState::default();
st.set(0.0, 0.0, &max);
assert_eq!(st.get(), LogicalPosition::zero());
st.set(50.0, 150.0, &max);
assert_eq!(st.get().x, 50.0);
assert_eq!(st.get().y, 150.0);
}
#[test]
fn scroll_state_set_clamps_negative_to_zero_and_overshoot_to_max() {
let max = r(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState::default();
st.set(-1.0, -f32::MAX, &max);
assert_eq!(st.get().x, 0.0);
assert_eq!(st.get().y, 0.0);
st.set(f32::MAX, f32::MAX, &max);
assert_eq!(st.get().x, 100.0);
assert_eq!(st.get().y, 200.0);
st.set(f32::INFINITY, f32::INFINITY, &max);
assert_eq!(st.get().x, 100.0);
assert_eq!(st.get().y, 200.0);
st.set(f32::NEG_INFINITY, f32::NEG_INFINITY, &max);
assert_eq!(st.get().x, 0.0);
assert_eq!(st.get().y, 0.0);
}
#[test]
fn scroll_state_set_nan_position_collapses_to_zero() {
let max = r(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState {
scroll_position: LogicalPosition::new(40.0, 40.0),
};
st.set(f32::NAN, f32::NAN, &max);
assert!(!st.get().x.is_nan());
assert!(!st.get().y.is_nan());
assert_eq!(st.get(), LogicalPosition::zero());
}
#[test]
fn scroll_state_set_nan_max_range_collapses_to_zero() {
let max = r(0.0, 0.0, f32::NAN, f32::NAN);
let mut st = ScrollState::default();
st.set(75.0, 75.0, &max);
assert_eq!(st.get(), LogicalPosition::zero());
}
#[test]
fn scroll_state_set_negative_max_range_collapses_to_zero() {
let max = r(0.0, 0.0, -50.0, -50.0);
let mut st = ScrollState::default();
st.set(10.0, 10.0, &max);
assert_eq!(st.get(), LogicalPosition::zero());
}
#[test]
fn scroll_state_add_accumulates_then_saturates_at_the_range() {
let max = r(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState::default();
st.add(30.0, 30.0, &max);
st.add(30.0, 30.0, &max);
assert_eq!(st.get().x, 60.0);
assert_eq!(st.get().y, 60.0);
st.add(f32::MAX, f32::MAX, &max);
st.add(f32::MAX, f32::MAX, &max);
assert_eq!(st.get().x, 100.0);
assert_eq!(st.get().y, 200.0);
assert!(st.get().x.is_finite() && st.get().y.is_finite());
}
#[test]
fn scroll_state_add_negative_underflow_clamps_to_zero() {
let max = r(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState {
scroll_position: LogicalPosition::new(10.0, 10.0),
};
st.add(f32::MIN, f32::MIN, &max);
assert_eq!(st.get(), LogicalPosition::zero());
st.add(f32::NEG_INFINITY, f32::NEG_INFINITY, &max);
assert_eq!(st.get(), LogicalPosition::zero());
assert!(st.get().x.is_finite() && st.get().y.is_finite());
}
#[test]
fn scroll_state_add_inf_minus_inf_does_not_poison_position() {
let unbounded = r(0.0, 0.0, f32::INFINITY, f32::INFINITY);
let mut st = ScrollState::default();
st.add(f32::INFINITY, f32::INFINITY, &unbounded);
assert!(st.get().x.is_infinite());
st.add(f32::NEG_INFINITY, f32::NEG_INFINITY, &unbounded);
assert!(!st.get().x.is_nan());
assert!(!st.get().y.is_nan());
assert_eq!(st.get(), LogicalPosition::zero());
}
#[test]
fn scroll_state_add_nan_delta_from_nonzero_position_resets_to_zero() {
let max = r(0.0, 0.0, 100.0, 200.0);
let mut st = ScrollState {
scroll_position: LogicalPosition::new(50.0, 50.0),
};
st.add(f32::NAN, 0.0, &max);
assert_eq!(st.get().x, 0.0);
assert_eq!(st.get().y, 50.0);
}
#[test]
fn scroll_states_new_is_empty_and_lookup_misses_return_none() {
let states = ScrollStates::new();
assert_eq!(states.0.len(), 0);
assert!(states.get_scroll_position(&ext_id(0)).is_none());
assert!(states.get_scroll_position(&ext_id(u64::MAX)).is_none());
}
#[test]
fn scroll_states_set_scroll_position_creates_entry_and_clamps() {
let node = scroll_node_of(r(0.0, 0.0, 100.0, 100.0), r(0.0, 0.0, 100.0, 300.0));
let mut states = ScrollStates::new();
states.set_scroll_position(&node, LogicalPosition::new(999.0, 999.0));
let pos = states
.get_scroll_position(&node.parent_external_scroll_id)
.expect("entry must exist after set_scroll_position");
assert_eq!(states.0.len(), 1);
assert_eq!(pos.x, 0.0); assert_eq!(pos.y, 200.0);
states.set_scroll_position(&node, LogicalPosition::new(-5.0, -5.0));
assert_eq!(states.0.len(), 1);
let pos = states
.get_scroll_position(&node.parent_external_scroll_id)
.unwrap();
assert_eq!(pos, LogicalPosition::zero());
}
#[test]
fn scroll_states_set_scroll_position_with_nan_stays_defined() {
let node = scroll_node_of(r(0.0, 0.0, 100.0, 100.0), r(0.0, 0.0, 100.0, 300.0));
let mut states = ScrollStates::new();
states.set_scroll_position(&node, LogicalPosition::new(f32::NAN, f32::NAN));
let pos = states
.get_scroll_position(&node.parent_external_scroll_id)
.unwrap();
assert!(!pos.x.is_nan() && !pos.y.is_nan());
assert_eq!(pos, LogicalPosition::zero());
}
#[test]
fn scroll_states_scroll_node_accumulates_and_saturates() {
let node = scroll_node_of(r(0.0, 0.0, 100.0, 100.0), r(0.0, 0.0, 400.0, 300.0));
let mut states = ScrollStates::new();
states.scroll_node(&node, 0.0, 0.0);
assert_eq!(
states
.get_scroll_position(&node.parent_external_scroll_id)
.unwrap(),
LogicalPosition::zero()
);
states.scroll_node(&node, 10.0, 10.0);
states.scroll_node(&node, 10.0, 10.0);
let pos = states
.get_scroll_position(&node.parent_external_scroll_id)
.unwrap();
assert_eq!(pos.x, 20.0);
assert_eq!(pos.y, 20.0);
states.scroll_node(&node, f32::MAX, f32::INFINITY);
let pos = states
.get_scroll_position(&node.parent_external_scroll_id)
.unwrap();
assert_eq!(pos.x, 300.0);
assert_eq!(pos.y, 200.0);
states.scroll_node(&node, f32::NAN, f32::NAN);
let pos = states
.get_scroll_position(&node.parent_external_scroll_id)
.unwrap();
assert!(!pos.x.is_nan() && !pos.y.is_nan());
assert_eq!(states.0.len(), 1);
}
#[test]
fn scroll_states_keys_are_pipeline_qualified() {
let a = OverflowingScrollNode {
parent_rect: r(0.0, 0.0, 10.0, 10.0),
child_rect: r(0.0, 0.0, 10.0, 100.0),
parent_external_scroll_id: ExternalScrollId(1, PipelineId(1, 0)),
..Default::default()
};
let b = OverflowingScrollNode {
parent_external_scroll_id: ExternalScrollId(1, PipelineId(2, 0)),
..a
};
let mut states = ScrollStates::new();
states.scroll_node(&a, 0.0, 25.0);
states.scroll_node(&b, 0.0, 50.0);
assert_eq!(states.0.len(), 2);
assert_eq!(
states
.get_scroll_position(&a.parent_external_scroll_id)
.unwrap()
.y,
25.0
);
assert_eq!(
states
.get_scroll_position(&b.parent_external_scroll_id)
.unwrap()
.y,
50.0
);
}
#[test]
fn scrollbar_component_from_u8_covers_all_256_values() {
for v in 0u8..=255 {
match ScrollbarComponent::from_u8(v) {
Some(c) => {
assert!(v < 4, "value {v} unexpectedly decoded to {c:?}");
assert_eq!(c as u8, v);
}
None => assert!(v >= 4, "value {v} should have decoded"),
}
}
for c in ALL_SCROLLBAR_COMPONENTS {
assert_eq!(ScrollbarComponent::from_u8(c as u8), Some(c));
}
}
#[test]
fn cursor_type_from_u8_is_total_and_unknown_falls_back_to_default() {
for v in 0u8..=255 {
let c = CursorType::from_u8(v);
if v <= 20 {
assert_eq!(c as u8, v, "known discriminant {v} must round-trip");
} else {
assert_eq!(c, CursorType::Default, "unknown byte {v} must be Default");
}
}
assert_eq!(CursorType::default(), CursorType::Default);
for c in ALL_CURSORS {
assert_eq!(CursorType::from_u8(c as u8), c);
}
}
#[test]
fn dom_node_tag_round_trips_at_u64_boundaries() {
for inner in [0u64, 1, 673, u64::from(u32::MAX), u64::MAX] {
let tag = HitTestTag::DomNode {
tag_id: TagId { inner },
};
let item = tag.to_item_tag();
assert_eq!(item, (inner, TAG_TYPE_DOM_NODE));
assert_eq!(HitTestTag::from_item_tag(item), Some(tag));
assert_eq!(tag.as_dom_node().unwrap().inner, inner);
}
}
#[test]
fn scrollbar_tag_round_trips_for_every_component_at_field_boundaries() {
let ids = [
(0usize, 0usize),
(0, u32::MAX as usize),
(u32::MAX as usize, 0),
(u32::MAX as usize, u32::MAX as usize),
];
for component in ALL_SCROLLBAR_COMPONENTS {
for (dom, node) in ids {
let tag = HitTestTag::Scrollbar {
dom_id: DomId { inner: dom },
node_id: NodeId::new(node),
component,
};
let item = tag.to_item_tag();
assert_eq!(item.1 & 0xFF00, TAG_TYPE_SCROLLBAR);
assert_eq!(
HitTestTag::from_item_tag(item),
Some(tag),
"scrollbar round-trip failed for dom={dom} node={node} {component:?}"
);
}
}
}
#[test]
fn cursor_tag_round_trips_for_every_cursor_type() {
for cursor_type in ALL_CURSORS {
let tag = HitTestTag::Cursor {
dom_id: DomId {
inner: u32::MAX as usize,
},
node_id: NodeId::new(u32::MAX as usize),
cursor_type,
};
let item = tag.to_item_tag();
assert_eq!(item.1 & 0xFF00, TAG_TYPE_CURSOR);
assert_eq!(
HitTestTag::from_item_tag(item),
Some(tag),
"cursor round-trip failed for {cursor_type:?}"
);
}
}
#[test]
fn selection_tag_round_trips_at_every_field_boundary() {
let cases = [
(0usize, 0usize, 0u16),
(0xFFFF, u32::MAX as usize, u16::MAX),
(1, 1, 1),
(0xFFFF, 0, u16::MAX),
];
for (dom, node, run) in cases {
let tag = HitTestTag::Selection {
dom_id: DomId { inner: dom },
container_node_id: NodeId::new(node),
text_run_index: run,
};
let item = tag.to_item_tag();
assert_eq!(item.1, TAG_TYPE_SELECTION);
assert_eq!(
HitTestTag::from_item_tag(item),
Some(tag),
"selection round-trip failed for dom={dom} node={node} run={run}"
);
}
}
#[test]
fn selection_tag_oversized_node_id_is_masked_not_bled_into_dom_id() {
let tag = HitTestTag::Selection {
dom_id: DomId { inner: 0x00AB },
container_node_id: NodeId::new(u32::MAX as usize),
text_run_index: 0xBEEF,
};
let (value, _) = tag.to_item_tag();
assert_eq!(value >> 48, 0x00AB);
assert_eq!((value >> 16) & 0xFFFF_FFFF, u64::from(u32::MAX));
assert_eq!(value & 0xFFFF, 0xBEEF);
}
#[test]
fn tag_namespaces_do_not_collide_on_identical_payloads() {
let payload = 0x0000_0001_0000_0002u64;
let decoded: [HitTestTag; 4] = [
HitTestTag::from_item_tag((payload, TAG_TYPE_DOM_NODE)).unwrap(),
HitTestTag::from_item_tag((payload, TAG_TYPE_SCROLLBAR)).unwrap(),
HitTestTag::from_item_tag((payload, TAG_TYPE_SELECTION)).unwrap(),
HitTestTag::from_item_tag((payload, TAG_TYPE_CURSOR)).unwrap(),
];
assert!(decoded[0].is_dom_node());
assert!(decoded[1].is_scrollbar());
assert!(decoded[2].is_selection());
assert!(decoded[3].is_cursor());
for (i, a) in decoded.iter().enumerate() {
for b in decoded.iter().skip(i + 1) {
assert_ne!(a, b);
}
}
}
#[test]
fn from_item_tag_sweeps_every_type_marker_without_panicking() {
for hi in 0u16..=0xFF {
for lo in [0u16, 1, 3, 4, 20, 21, 0xFF] {
let tag_type = (hi << 8) | lo;
let decoded = HitTestTag::from_item_tag((0xDEAD_BEEF_CAFE_BABE, tag_type));
let expected_some = match hi {
0x00 => tag_type == 0, 0x01 | 0x03 | 0x04 => true, 0x02 => lo < 4, _ => false, };
assert_eq!(
decoded.is_some(),
expected_some,
"tag_type {tag_type:#06x} decoded to {decoded:?}"
);
}
}
}
#[test]
fn from_item_tag_rejects_invalid_scrollbar_components() {
for lo in 4u16..=0xFF {
let item = (0u64, TAG_TYPE_SCROLLBAR | lo);
assert!(
HitTestTag::from_item_tag(item).is_none(),
"scrollbar component byte {lo} should be rejected"
);
}
}
#[test]
fn from_item_tag_unknown_cursor_byte_degrades_to_default() {
for lo in [21u16, 100, 0xFF] {
let decoded = HitTestTag::from_item_tag((0, TAG_TYPE_CURSOR | lo)).unwrap();
assert_eq!(
decoded.as_cursor().unwrap().2,
CursorType::Default,
"cursor byte {lo} should fall back to Default"
);
}
}
#[test]
fn from_item_tag_dom_node_ignores_the_lower_byte() {
for lo in [0u16, 1, 0x7F, 0xFF] {
let decoded = HitTestTag::from_item_tag((99, TAG_TYPE_DOM_NODE | lo)).unwrap();
assert!(decoded.is_dom_node());
assert_eq!(decoded.as_dom_node().unwrap().inner, 99);
}
}
#[test]
fn from_item_tag_scroll_container_marker_is_not_decodable() {
assert!(HitTestTag::from_item_tag((0, TAG_TYPE_SCROLL_CONTAINER)).is_none());
assert!(HitTestTag::from_item_tag((u64::MAX, TAG_TYPE_SCROLL_CONTAINER)).is_none());
}
#[test]
fn from_item_tag_legacy_zero_type_only_matches_exact_zero() {
let legacy = HitTestTag::from_item_tag((u64::MAX, 0)).unwrap();
assert_eq!(legacy.as_dom_node().unwrap().inner, u64::MAX);
for lo in 1u16..=0xFF {
assert!(
HitTestTag::from_item_tag((0, lo)).is_none(),
"tag_type {lo:#06x} must not be treated as a legacy DOM tag"
);
}
}
#[cfg(target_pointer_width = "64")]
#[test]
fn scrollbar_encode_keeps_decoded_fields_inside_their_bit_windows() {
let tag = HitTestTag::Scrollbar {
dom_id: DomId { inner: 0 },
node_id: NodeId::new(1usize << 32),
component: ScrollbarComponent::VerticalTrack,
};
let (value, ty) = tag.to_item_tag();
assert_eq!(ty & 0xFF00, TAG_TYPE_SCROLLBAR);
let decoded = HitTestTag::from_item_tag((value, ty)).expect("must still decode");
let (dom, node, component) = decoded.as_scrollbar().unwrap();
assert!(dom.inner <= u32::MAX as usize);
assert!(node.index() <= u32::MAX as usize);
assert_eq!(component, ScrollbarComponent::VerticalTrack);
}
fn sample_tags() -> [HitTestTag; 4] {
[
HitTestTag::DomNode {
tag_id: TagId { inner: 7 },
},
HitTestTag::Scrollbar {
dom_id: DomId { inner: 1 },
node_id: NodeId::new(2),
component: ScrollbarComponent::HorizontalThumb,
},
HitTestTag::Cursor {
dom_id: DomId { inner: 3 },
node_id: NodeId::new(4),
cursor_type: CursorType::Grabbing,
},
HitTestTag::Selection {
dom_id: DomId { inner: 5 },
container_node_id: NodeId::new(6),
text_run_index: 8,
},
]
}
#[test]
fn exactly_one_predicate_is_true_per_variant() {
for tag in sample_tags() {
let flags = [
tag.is_dom_node(),
tag.is_scrollbar(),
tag.is_cursor(),
tag.is_selection(),
];
assert_eq!(
flags.iter().filter(|b| **b).count(),
1,
"predicates not mutually exclusive for {tag:?}"
);
}
}
#[test]
fn accessors_agree_with_predicates_and_return_none_otherwise() {
for tag in sample_tags() {
assert_eq!(tag.as_dom_node().is_some(), tag.is_dom_node());
assert_eq!(tag.as_scrollbar().is_some(), tag.is_scrollbar());
assert_eq!(tag.as_cursor().is_some(), tag.is_cursor());
assert_eq!(tag.as_selection().is_some(), tag.is_selection());
let some_count = usize::from(tag.as_dom_node().is_some())
+ usize::from(tag.as_scrollbar().is_some())
+ usize::from(tag.as_cursor().is_some())
+ usize::from(tag.as_selection().is_some());
assert_eq!(some_count, 1, "accessor overlap for {tag:?}");
}
}
#[test]
fn accessors_return_the_constructed_payload() {
let [dom, scrollbar, cursor, selection] = sample_tags();
assert_eq!(dom.as_dom_node().unwrap().inner, 7);
let (d, n, c) = scrollbar.as_scrollbar().unwrap();
assert_eq!((d.inner, n.index()), (1, 2));
assert_eq!(c, ScrollbarComponent::HorizontalThumb);
let (d, n, c) = cursor.as_cursor().unwrap();
assert_eq!((d.inner, n.index()), (3, 4));
assert_eq!(c, CursorType::Grabbing);
let (d, n, run) = selection.as_selection().unwrap();
assert_eq!((d.inner, n.index()), (5, 6));
assert_eq!(run, 8);
}
#[test]
fn hit_test_tag_display_is_non_empty_and_variant_tagged() {
let [dom, scrollbar, cursor, selection] = sample_tags();
for (tag, prefix) in [
(dom, "DomNode("),
(scrollbar, "Scrollbar("),
(cursor, "Cursor("),
(selection, "Selection("),
] {
let shown = alloc::format!("{tag}");
assert!(!shown.is_empty());
assert!(
shown.starts_with(prefix),
"expected {shown:?} to start with {prefix:?}"
);
}
}
#[test]
fn hit_test_tag_display_handles_extreme_ids() {
let tags = [
HitTestTag::DomNode {
tag_id: TagId { inner: u64::MAX },
},
HitTestTag::Scrollbar {
dom_id: DomId { inner: usize::MAX },
node_id: NodeId::new(usize::MAX),
component: ScrollbarComponent::VerticalThumb,
},
HitTestTag::Cursor {
dom_id: DomId { inner: usize::MAX },
node_id: NodeId::new(usize::MAX),
cursor_type: CursorType::Progress,
},
HitTestTag::Selection {
dom_id: DomId { inner: usize::MAX },
container_node_id: NodeId::new(usize::MAX),
text_run_index: u16::MAX,
},
];
for tag in tags {
assert!(!alloc::format!("{tag}").is_empty());
assert!(!alloc::format!("{tag:?}").is_empty());
let _ = tag.to_item_tag();
}
}
}
#[cfg(test)]
mod hovered_node_answers {
use alloc::{collections::BTreeMap, vec, vec::Vec};
use crate::{
dom::{DomId, DomNodeId, NodeId},
geom::LogicalPosition,
hit_test::{FullHitTest, HitTest, HitTestItem, OptionDomNodeId},
styled_dom::NodeHierarchyItemId,
};
fn item(depth: u32) -> HitTestItem {
HitTestItem {
point_in_viewport: LogicalPosition::zero(),
point_relative_to_item: crate::spaces::ContentBoxLocal::new(LogicalPosition::zero()),
is_focusable: false,
is_virtual_view_hit: None,
hit_depth: depth,
}
}
fn hit_test(nodes: &[(usize, u32)]) -> HitTest {
let mut regular = BTreeMap::new();
for (id, depth) in nodes {
regular.insert(NodeId::new(*id), item(*depth));
}
HitTest {
regular_hit_test_nodes: regular,
scroll_hit_test_nodes: BTreeMap::new(),
scrollbar_hit_test_nodes: BTreeMap::new(),
cursor_hit_test_nodes: BTreeMap::new(),
}
}
fn nid(node: usize) -> DomNodeId {
DomNodeId {
dom: DomId::ROOT_ID,
node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
}
}
fn full(nodes: &[(usize, u32)]) -> FullHitTest {
let mut hovered = BTreeMap::new();
hovered.insert(DomId::ROOT_ID, hit_test(nodes));
FullHitTest {
hovered_nodes: hovered,
focused_node: OptionDomNodeId::None,
}
}
#[test]
fn hovered_nodes_come_back_front_to_back_by_depth_not_by_id() {
let hit = full(&[(2, 5), (9, 0), (4, 2)]);
assert_eq!(hit.hovered_node_ids(), vec![nid(9), nid(4), nid(2)]);
assert_eq!(hit.topmost_node(), Some(nid(9)));
}
#[test]
fn nothing_under_the_pointer_answers_none() {
let empty = FullHitTest {
hovered_nodes: BTreeMap::new(),
focused_node: OptionDomNodeId::None,
};
assert!(empty.hovered_node_ids().is_empty());
assert_eq!(empty.topmost_node(), None);
let none_hit = full(&[]);
assert!(none_hit.hovered_node_ids().is_empty());
assert_eq!(none_hit.topmost_node(), None);
}
#[test]
fn only_regular_hits_count() {
let mut hit = hit_test(&[(1, 0)]);
hit.cursor_hit_test_nodes.insert(
NodeId::new(7),
crate::hit_test::CursorHitTestItem {
cursor_type: crate::hit_test::CursorType::default(),
hit_depth: 0,
point_in_viewport: LogicalPosition::zero(),
},
);
let mut hovered = BTreeMap::new();
hovered.insert(DomId::ROOT_ID, hit);
let full = FullHitTest {
hovered_nodes: hovered,
focused_node: OptionDomNodeId::None,
};
assert_eq!(full.hovered_node_ids(), vec![nid(1)]);
}
#[test]
fn a_depth_tie_is_resolved_stably() {
let hit = full(&[(1, 0), (2, 0), (3, 0)]);
assert_eq!(hit.topmost_node(), Some(nid(1)));
assert_eq!(hit.hovered_node_ids(), vec![nid(1), nid(2), nid(3)]);
}
}