Pointer
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user