Example 02 realized

This commit is contained in:
2026-03-21 23:25:28 -04:00
parent c9966b79ef
commit bc287f615d
6 changed files with 1016 additions and 34 deletions

View File

@@ -116,6 +116,10 @@ impl InteractionTree {
pub fn scroll_metrics_for_element(&self, element_id: ElementId) -> Option<&ScrollMetrics> {
scroll_metrics_for_element_node(&self.root, element_id)
}
pub fn rect_for_element(&self, element_id: ElementId) -> Option<Rect> {
rect_for_element_node(&self.root, element_id)
}
}
pub fn layout_snapshot(version: u64, logical_size: UiSize, root: &Element) -> LayoutSnapshot {
@@ -309,7 +313,9 @@ fn layout_element(
perf_stats,
);
let provisional_content_height = content_size.height.max(viewport_rect.size.height);
let mut offset_y = scroll_box.offset_y.max(0.0);
let provisional_max_offset_y =
(provisional_content_height - viewport_rect.size.height).max(0.0);
let mut offset_y = scroll_box.offset_y.max(0.0).min(provisional_max_offset_y);
if viewport_rect.size.width > 0.0 && viewport_rect.size.height > 0.0 {
scene.push_clip(viewport_rect, 0.0);
@@ -509,6 +515,18 @@ fn scroll_metrics_for_element_node(
None
}
fn rect_for_element_node(node: &LayoutNode, element_id: ElementId) -> Option<Rect> {
if node.element_id == Some(element_id) {
return Some(node.rect);
}
for child in &node.children {
if let Some(rect) = rect_for_element_node(child, element_id) {
return Some(rect);
}
}
None
}
fn point_hits_node_shape(node: &LayoutNode, point: crate::scene::Point) -> bool {
node.rect.contains(point)
&& (node.corner_radius <= 0.0
@@ -1598,6 +1616,51 @@ mod tests {
assert!(scroll_metrics.scrollbar_thumb.is_some());
}
#[test]
fn scroll_box_clamps_stale_offset_before_laying_out_reflowed_content() {
let scrollbox_id = ElementId::new(19);
let root = Element::column().child(
Element::scroll_box(240.0)
.id(scrollbox_id)
.width(320.0)
.height(120.0)
.padding(Edges::all(8.0))
.child(Element::paragraph(
"When the viewport becomes wider, wrapped scroll-box content can get much \
shorter. Layout should clamp any now-invalid stale offset before positioning \
the children so the viewport does not open up an empty gap at the bottom.",
TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF))
.with_line_height(22.0)
.with_wrap(TextWrap::Word),
)),
);
let snapshot = layout_snapshot(1, UiSize::new(360.0, 220.0), &root);
let scroll_metrics = snapshot
.interaction_tree
.scroll_metrics_for_element(scrollbox_id)
.expect("scroll box should expose scroll metrics");
let visible_text = snapshot
.scene
.items
.iter()
.find_map(|item| match item {
DisplayItem::Text(text) => Some(text),
_ => None,
})
.expect("scroll box should still emit text");
let text_bounds = visible_text
.bounds
.expect("prepared text should expose bounds for wrapped content");
let text_bottom = visible_text.origin.y + text_bounds.height;
let viewport_bottom =
scroll_metrics.viewport_rect.origin.y + scroll_metrics.viewport_rect.size.height;
assert!(scroll_metrics.offset_y <= scroll_metrics.max_offset_y);
assert!(text_bottom >= viewport_bottom - 1.0);
}
#[test]
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
let root = Element::column()