use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::LayoutRect;
use crate::node::TuiNodeExt;
use crate::render::Rect;
use crate::render::inline::has_inline_layout;
use crate::render::stacking::{
LayerEntry, children_clip, collect_layers, creates_stacking_context, is_positioned,
};
use crate::style::ComputedStyle;
pub(super) fn hit_stacking_context(
dom: &Dom<TuiExt>,
root: NodeId,
x: u16,
y: u16,
clip: Rect,
viewport: Rect,
path: &mut Vec<NodeId>,
) -> bool {
if !clip.contains(x, y) {
return false;
}
let root_box = element_box(dom, root);
if dom.node(root).node_type() == NodeType::Element && root_box.is_none() {
return false; }
let content_clip = root_box
.as_ref()
.map_or(clip, |(c, _)| children_clip(dom, root, c, clip));
let layers = collect_layers(dom, root, content_clip, viewport);
let mark = path.len();
let transparent = root_box
.as_ref()
.is_some_and(|(c, _)| c.pointer_events == crate::layout::PointerEvents::None);
let root_in_path = |path: &mut Vec<NodeId>| {
if root_box.is_some() && !transparent {
path.insert(mark, root);
}
};
if hit_layers(dom, root, &layers.positive, x, y, viewport, path)
|| hit_layers(dom, root, &layers.zero_auto, x, y, viewport, path)
{
root_in_path(path);
return true;
}
let Some((_, outer)) = root_box else {
return descend_children_reverse(dom, root, x, y, content_clip, viewport, path)
|| hit_layers(dom, root, &layers.negative, x, y, viewport, path);
};
let contains = rect_contains(outer, x, y);
if contains && content_clip.contains(x, y) {
if !transparent {
path.push(root);
}
if hit_content(dom, root, x, y, content_clip, viewport, path) {
return true;
}
path.truncate(mark);
}
if hit_layers(dom, root, &layers.negative, x, y, viewport, path) {
root_in_path(path);
return true;
}
if contains && !transparent {
path.push(root);
return true;
}
false
}
fn hit_layers(
dom: &Dom<TuiExt>,
root: NodeId,
entries: &[LayerEntry],
x: u16,
y: u16,
viewport: Rect,
path: &mut Vec<NodeId>,
) -> bool {
for e in entries.iter().rev() {
let mark = path.len();
let hit = if e.context {
hit_stacking_context(dom, e.id, x, y, e.clip, viewport, path)
} else {
descend_plain(dom, e.id, x, y, e.clip, viewport, path)
};
if hit {
let mut chain = Vec::new();
let mut cur = dom.node(e.id).parent_node().map(|p| p.id());
while let Some(id) = cur {
if id == root {
break;
}
if dom.node(id).node_type() == NodeType::Element {
chain.push(id);
}
cur = dom.node(id).parent_node().map(|p| p.id());
}
chain.reverse();
path.splice(mark..mark, chain);
return true;
}
}
false
}
fn element_box(dom: &Dom<TuiExt>, id: NodeId) -> Option<(std::rc::Rc<ComputedStyle>, LayoutRect)> {
let node = dom.node(id);
if node.node_type() != NodeType::Element {
return None;
}
let computed = node
.computed_rc()
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
if matches!(computed.display, crate::layout::Display::None) {
return None;
}
let outer = node.layout_rect()?;
Some((computed, outer))
}
fn descend_plain(
dom: &Dom<TuiExt>,
id: NodeId,
x: u16,
y: u16,
clip: Rect,
viewport: Rect,
path: &mut Vec<NodeId>,
) -> bool {
if !clip.contains(x, y) {
return false;
}
let Some((computed, outer)) = element_box(dom, id) else {
return false;
};
if !rect_contains(outer, x, y) {
return false;
}
let content_clip = children_clip(dom, id, &computed, clip);
if computed.pointer_events == crate::layout::PointerEvents::None {
return content_clip.contains(x, y)
&& hit_content(dom, id, x, y, content_clip, viewport, path);
}
path.push(id);
if content_clip.contains(x, y) {
hit_content(dom, id, x, y, content_clip, viewport, path);
}
true
}
fn hit_content(
dom: &Dom<TuiExt>,
id: NodeId,
x: u16,
y: u16,
content_clip: Rect,
viewport: Rect,
path: &mut Vec<NodeId>,
) -> bool {
if has_inline_layout(dom, id) {
let outer = dom.node(id).layout_rect().unwrap_or_default();
let inner = crate::render::inline::scrolled_content_rect(dom, id).unwrap_or(outer);
let Some(owner) = hit_fragment(dom, id, inner, x, y) else {
return false;
};
let mut target = owner;
while is_pointer_transparent(dom, target) {
if target == id {
return false;
}
target = match dom.node(target).parent_node() {
Some(p) => p.id(),
None => return false,
};
}
if target == id {
return false;
}
append_inline_ancestors(dom, id, target, path);
return true;
}
descend_children_reverse(dom, id, x, y, content_clip, viewport, path)
}
fn descend_children_reverse(
dom: &Dom<TuiExt>,
id: NodeId,
x: u16,
y: u16,
clip: Rect,
viewport: Rect,
path: &mut Vec<NodeId>,
) -> bool {
let child_ids: Vec<NodeId> = dom.node(id).child_nodes().map(|n| n.id()).collect();
for &child in child_ids.iter().rev() {
let node = dom.node(child);
let hit = match node.node_type() {
NodeType::Fragment => descend_children_reverse(dom, child, x, y, clip, viewport, path),
NodeType::Element => match node.ext().and_then(|e| e.computed.as_ref()) {
Some(c) if is_positioned(c) => false,
Some(c) if creates_stacking_context(c) => {
hit_stacking_context(dom, child, x, y, clip, viewport, path)
}
_ => descend_plain(dom, child, x, y, clip, viewport, path),
},
_ => false,
};
if hit {
return true;
}
}
false
}
fn hit_fragment(
dom: &Dom<TuiExt>,
ifc_block: NodeId,
content: LayoutRect,
x: u16,
y: u16,
) -> Option<NodeId> {
let ext = dom.node(ifc_block).ext()?;
let layout = ext.inline_layout.as_ref()?;
let line_index = y as i32 - content.y;
if line_index < 0 || line_index as usize >= layout.lines.len() {
return None;
}
let line = &layout.lines[line_index as usize];
let x_local_i = x as i32 - content.x;
if x_local_i < 0 {
return None;
}
let x_local = x_local_i as u16;
for fragment in &line.fragments {
if x_local >= fragment.x && x_local < fragment.x + fragment.width {
return Some(fragment.node);
}
}
line.generated
.iter()
.find(|g| x_local >= g.x && x_local < g.x + g.width)
.filter(|g| is_descendant(dom, g.host, ifc_block))
.filter(|g| {
let node = dom.node(g.host);
let pseudo = match g.slot {
crate::ext::PseudoSlot::Before => node.computed_before(),
crate::ext::PseudoSlot::After => node.computed_after(),
};
pseudo.is_none_or(|c| c.pointer_events != crate::layout::PointerEvents::None)
})
.map(|g| g.host)
}
fn is_descendant(dom: &Dom<TuiExt>, id: NodeId, ancestor: NodeId) -> bool {
let mut cur = dom.node(id).parent_node().map(|p| p.id());
while let Some(n) = cur {
if n == ancestor {
return true;
}
cur = dom.node(n).parent_node().map(|p| p.id());
}
false
}
fn is_pointer_transparent(dom: &Dom<TuiExt>, id: NodeId) -> bool {
dom.node(id)
.computed()
.is_some_and(|c| c.pointer_events == crate::layout::PointerEvents::None)
}
fn append_inline_ancestors(
dom: &Dom<TuiExt>,
ifc_block: NodeId,
owner: NodeId,
path: &mut Vec<NodeId>,
) {
let mut chain = Vec::new();
let mut cur = owner;
while cur != ifc_block {
if !is_pointer_transparent(dom, cur) {
chain.push(cur);
}
match dom.node(cur).parent_node() {
Some(parent) => cur = parent.id(),
None => break, }
}
chain.reverse();
path.extend(chain);
}
#[inline]
fn rect_contains(r: LayoutRect, x: u16, y: u16) -> bool {
let x = x as i32;
let y = y as i32;
x >= r.x && x < r.x + r.width as i32 && y >= r.y && y < r.y + r.height as i32
}