This commit is contained in:
2026-03-20 20:28:48 -04:00
parent f71e03317d
commit d79a3bb728
9 changed files with 702 additions and 52 deletions

View File

@@ -1,6 +1,6 @@
use crate::scene::{Rect, SceneSnapshot, UiSize};
use crate::text::TextSystem;
use crate::tree::{Edges, Element, FlexDirection};
use crate::tree::{Edges, Element, ElementId, FlexDirection};
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
let mut text_system = TextSystem::new();
@@ -13,8 +13,74 @@ pub fn layout_scene_with_text_system(
root: &Element,
text_system: &mut TextSystem,
) -> SceneSnapshot {
layout_snapshot_with_text_system(version, logical_size, root, text_system).scene
}
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutSnapshot {
pub scene: SceneSnapshot,
pub interaction_tree: InteractionTree,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct LayoutPath(Vec<u32>);
impl LayoutPath {
pub fn root() -> Self {
Self(Vec::new())
}
pub fn segments(&self) -> &[u32] {
&self.0
}
pub(crate) fn child(&self, index: usize) -> Self {
let mut segments = self.0.clone();
segments.push(index as u32);
Self(segments)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct HitTarget {
pub path: LayoutPath,
pub element_id: Option<ElementId>,
pub rect: Rect,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutNode {
pub path: LayoutPath,
pub element_id: Option<ElementId>,
pub rect: Rect,
pub pointer_events: bool,
pub children: Vec<LayoutNode>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct InteractionTree {
pub root: LayoutNode,
}
impl InteractionTree {
pub fn hit_test(&self, point: crate::scene::Point) -> Option<HitTarget> {
hit_test_node(&self.root, point)
}
}
pub fn layout_snapshot(version: u64, logical_size: UiSize, root: &Element) -> LayoutSnapshot {
let mut text_system = TextSystem::new();
layout_snapshot_with_text_system(version, logical_size, root, &mut text_system)
}
pub fn layout_snapshot_with_text_system(
version: u64,
logical_size: UiSize,
root: &Element,
text_system: &mut TextSystem,
) -> LayoutSnapshot {
let mut scene = SceneSnapshot::new(version, logical_size);
layout_element(
let interaction_root = layout_element(
root,
Rect::new(
0.0,
@@ -22,20 +88,35 @@ pub fn layout_scene_with_text_system(
logical_size.width.max(0.0),
logical_size.height.max(0.0),
),
LayoutPath::root(),
&mut scene,
text_system,
);
scene
LayoutSnapshot {
scene,
interaction_tree: InteractionTree {
root: interaction_root,
},
}
}
fn layout_element(
element: &Element,
rect: Rect,
path: LayoutPath,
scene: &mut SceneSnapshot,
text_system: &mut TextSystem,
) {
) -> LayoutNode {
let mut interaction = LayoutNode {
path,
element_id: element.id,
rect,
pointer_events: element.style.pointer_events,
children: Vec::new(),
};
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
return;
return interaction;
}
if let Some(color) = element.style.background {
@@ -51,16 +132,16 @@ fn layout_element(
text.style.clone().with_bounds(content.size),
));
}
return;
return interaction;
}
if element.children.is_empty() {
return;
return interaction;
}
let content = inset_rect(rect, element.style.padding);
if content.size.width <= 0.0 || content.size.height <= 0.0 {
return;
return interaction;
}
let gap_count = element.children.len().saturating_sub(1) as f32;
@@ -107,7 +188,7 @@ fn layout_element(
let remaining_main = (available_main - fixed_total).max(0.0);
let mut cursor = main_axis_origin(content, element.style.direction);
for (child, measured) in element.children.iter().zip(measured_children) {
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
let child_main = if measured.is_flex {
if flex_total <= 0.0 {
0.0
@@ -124,9 +205,39 @@ fn layout_element(
child_main.max(0.0),
measured.cross,
);
layout_element(child, child_rect, scene, text_system);
interaction.children.push(layout_element(
child,
child_rect,
interaction.path.child(index),
scene,
text_system,
));
cursor += child_main.max(0.0) + element.style.gap;
}
interaction
}
fn hit_test_node(node: &LayoutNode, point: crate::scene::Point) -> Option<HitTarget> {
if !node.rect.contains(point) {
return None;
}
for child in node.children.iter().rev() {
if let Some(hit) = hit_test_node(child, point) {
return Some(hit);
}
}
if node.pointer_events {
return Some(HitTarget {
path: node.path.clone(),
element_id: node.element_id,
rect: node.rect,
});
}
None
}
#[derive(Clone, Copy, Debug)]
@@ -337,10 +448,10 @@ fn child_rect(
#[cfg(test)]
mod tests {
use super::layout_scene;
use crate::scene::{Color, DisplayItem, Quad, Rect, UiSize};
use super::{layout_scene, layout_snapshot};
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
use crate::text::{TextStyle, TextWrap};
use crate::tree::{Edges, Element};
use crate::tree::{Edges, Element, ElementId};
#[test]
fn row_layout_apportions_fixed_and_flex_children() {
@@ -464,4 +575,54 @@ mod tests {
.any(|item| matches!(item, DisplayItem::Text(_)))
);
}
#[test]
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
let root = Element::column()
.id(ElementId::new(1))
.children([Element::new()
.id(ElementId::new(2))
.height(80.0)
.background(Color::rgb(0x22, 0x33, 0x44))
.child(
Element::new()
.id(ElementId::new(3))
.width(120.0)
.height(40.0)
.background(Color::rgb(0x44, 0x55, 0x66)),
)]);
let snapshot = layout_snapshot(1, UiSize::new(320.0, 200.0), &root);
let hit = snapshot
.interaction_tree
.hit_test(Point::new(20.0, 20.0))
.expect("point should hit nested child");
assert_eq!(hit.element_id, Some(ElementId::new(3)));
assert_eq!(hit.path.segments(), &[0, 0]);
}
#[test]
fn interaction_tree_skips_pointer_disabled_node_and_falls_back_to_parent() {
let root = Element::column().id(ElementId::new(1)).child(
Element::new()
.id(ElementId::new(2))
.height(80.0)
.background(Color::rgb(0x22, 0x33, 0x44))
.child(
Element::new()
.id(ElementId::new(3))
.width(120.0)
.height(40.0)
.pointer_events(false)
.background(Color::rgb(0x44, 0x55, 0x66)),
),
);
let snapshot = layout_snapshot(1, UiSize::new(320.0, 200.0), &root);
let hit = snapshot
.interaction_tree
.hit_test(Point::new(20.0, 20.0))
.expect("point should still hit parent");
assert_eq!(hit.element_id, Some(ElementId::new(2)));
}
}