use std::collections::BTreeMap;
use azul_core::{
dom::{DomId, NodeId},
geom::{LogicalPosition, LogicalRect, LogicalSize},
styled_dom::StyledDom,
};
use crate::solver3::{getters::{get_overflow_x, get_overflow_y}, layout_tree::LayoutNodeHot, PositionVec};
const CLIP_UNBOUNDED: f32 = 1.0e7;
#[derive(Debug)]
pub struct CpuHitTester {
node_rects: BTreeMap<DomId, Vec<HitTestEntry>>,
}
#[derive(Debug, Clone)]
struct HitTestEntry {
node_id: NodeId,
rect: LogicalRect,
clip: Option<LogicalRect>,
pointer_events_none: bool,
}
impl Default for CpuHitTester {
fn default() -> Self {
Self::new()
}
}
impl CpuHitTester {
#[must_use] pub const fn new() -> Self {
Self {
node_rects: BTreeMap::new(),
}
}
#[must_use] pub fn node_rects_total(&self) -> usize {
self.node_rects.values().map(Vec::len).sum()
}
pub fn rebuild_from_layout(
&mut self,
layout_results: &BTreeMap<DomId, crate::window::DomLayoutResult>,
) {
self.node_rects.clear();
let mut placements: BTreeMap<DomId, LogicalRect> = BTreeMap::new();
for _ in 0..4 {
let mut changed = false;
for (host_dom, lr) in layout_results {
let host_offset = if host_dom.inner == 0 {
Some(LogicalPosition::zero())
} else {
placements.get(host_dom).map(|r| r.origin)
};
let Some(host_offset) = host_offset else { continue };
for item in &lr.display_list.items {
if let crate::solver3::display_list::DisplayListItem::VirtualView {
child_dom_id,
bounds,
..
} = item
{
let b = *bounds.inner();
let absolute = LogicalRect {
origin: LogicalPosition {
x: b.origin.x + host_offset.x,
y: b.origin.y + host_offset.y,
},
size: b.size,
};
if placements.get(child_dom_id) != Some(&absolute) {
placements.insert(*child_dom_id, absolute);
changed = true;
}
}
}
}
if !changed {
break;
}
}
for (dom_id, layout_result) in layout_results {
let mut entries = Vec::new();
let positions = &layout_result.calculated_positions;
let nodes = &layout_result.layout_tree.nodes;
let styled_dom = &layout_result.styled_dom;
let (offset, dom_clip) = placements.get(dom_id).map_or_else(|| (LogicalPosition::zero(), None), |b| (b.origin, Some(*b)));
for (idx, node) in nodes.iter().enumerate() {
let Some(node_id) = node.dom_node_id else {
continue; };
let pos = match positions.get(idx) {
Some(p) => *p,
None => continue,
};
let Some(size) = node.used_size else {
continue;
};
let rect = LogicalRect {
origin: LogicalPosition {
x: pos.x + offset.x,
y: pos.y + offset.y,
},
size,
};
let clip = compute_node_clip(styled_dom, nodes, positions, idx, offset, dom_clip);
entries.push(HitTestEntry {
node_id,
rect,
clip,
pointer_events_none: false,
});
}
self.node_rects.insert(*dom_id, entries);
}
}
#[must_use] pub fn hit_test(
&self,
position: LogicalPosition,
) -> Vec<(DomId, NodeId)> {
let mut results = Vec::new();
for (dom_id, entries) in &self.node_rects {
for entry in entries.iter().rev() {
if entry.pointer_events_none {
continue;
}
if let Some(ref clip) = entry.clip {
if !point_in_rect(position, clip) {
continue;
}
}
if point_in_rect(position, &entry.rect) {
results.push((*dom_id, entry.node_id));
}
}
}
results
}
}
fn point_in_rect(point: LogicalPosition, rect: &LogicalRect) -> bool {
point.x >= rect.origin.x
&& point.x < rect.origin.x + rect.size.width
&& point.y >= rect.origin.y
&& point.y < rect.origin.y + rect.size.height
}
#[allow(clippy::similar_names)] fn compute_node_clip(
styled_dom: &StyledDom,
nodes: &[LayoutNodeHot],
positions: &PositionVec,
node_index: usize,
offset: LogicalPosition,
dom_clip: Option<LogicalRect>,
) -> Option<LogicalRect> {
let (mut min_x, mut min_y, mut max_x, mut max_y) = (
f32::NEG_INFINITY,
f32::NEG_INFINITY,
f32::INFINITY,
f32::INFINITY,
);
let mut has_clip = false;
if let Some(dc) = dom_clip {
min_x = dc.min_x();
min_y = dc.min_y();
max_x = dc.max_x();
max_y = dc.max_y();
has_clip = true;
}
let styled_nodes = styled_dom.styled_nodes.as_container();
let mut cur = nodes.get(node_index).and_then(|n| n.parent);
let mut guard = 0usize;
while let Some(anc) = cur {
guard += 1;
if guard > nodes.len() {
break;
}
let Some(anc_node) = nodes.get(anc) else { break };
cur = anc_node.parent;
let Some(anc_dom_id) = anc_node.dom_node_id else {
continue;
};
let node_state = &styled_nodes[anc_dom_id].styled_node_state;
let clips_x = get_overflow_x(styled_dom, anc_dom_id, node_state).is_clipped();
let clips_y = get_overflow_y(styled_dom, anc_dom_id, node_state).is_clipped();
if !clips_x && !clips_y {
continue;
}
let (Some(pos), Some(size)) = (positions.get(anc), anc_node.used_size) else {
continue;
};
let (ax0, ay0) = (pos.x + offset.x, pos.y + offset.y);
if clips_x {
min_x = min_x.max(ax0);
max_x = max_x.min(ax0 + size.width);
has_clip = true;
}
if clips_y {
min_y = min_y.max(ay0);
max_y = max_y.min(ay0 + size.height);
has_clip = true;
}
}
if !has_clip {
return None;
}
if !min_x.is_finite() {
min_x = -CLIP_UNBOUNDED;
}
if !min_y.is_finite() {
min_y = -CLIP_UNBOUNDED;
}
if !max_x.is_finite() {
max_x = CLIP_UNBOUNDED;
}
if !max_y.is_finite() {
max_y = CLIP_UNBOUNDED;
}
Some(LogicalRect {
origin: LogicalPosition { x: min_x, y: min_y },
size: LogicalSize {
width: (max_x - min_x).max(0.0),
height: (max_y - min_y).max(0.0),
},
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpu_hit_tester_empty() {
let tester = CpuHitTester::new();
let results = tester.hit_test(LogicalPosition { x: 100.0, y: 100.0 });
assert!(results.is_empty());
}
#[test]
fn test_point_in_rect() {
let rect = LogicalRect {
origin: LogicalPosition { x: 10.0, y: 10.0 },
size: LogicalSize {
width: 100.0,
height: 50.0,
},
};
assert!(point_in_rect(LogicalPosition { x: 50.0, y: 30.0 }, &rect));
assert!(point_in_rect(LogicalPosition { x: 10.0, y: 10.0 }, &rect));
assert!(!point_in_rect(LogicalPosition { x: 5.0, y: 5.0 }, &rect));
assert!(!point_in_rect(LogicalPosition { x: 200.0, y: 30.0 }, &rect));
}
}