mod descend;
mod fragment;
mod nearest;
use rdom_core::{Dom, NodeId, Position};
use crate::ext::TuiExt;
use crate::render::Rect;
use crate::runtime::selection::user_select;
use descend::hit_stacking_context;
use nearest::inline_target_at;
pub(crate) use fragment::resolve_in_target;
pub(crate) use nearest::nearest_inline_target_in_subtree;
pub trait HitTestExt {
fn hit_test(&self, x: u16, y: u16) -> Option<NodeId>;
fn hit_test_path(&self, x: u16, y: u16) -> Vec<NodeId>;
fn position_at(&self, x: u16, y: u16) -> Option<Position>;
fn nearest_selectable_position(&self, x: u16, y: u16) -> Option<Position>;
}
impl HitTestExt for Dom<TuiExt> {
fn hit_test(&self, x: u16, y: u16) -> Option<NodeId> {
self.hit_test_path(x, y).last().copied()
}
fn hit_test_path(&self, x: u16, y: u16) -> Vec<NodeId> {
let mut path = Vec::new();
let unclipped = Rect::new(0, 0, u16::MAX, u16::MAX);
hit_stacking_context(self, self.root(), x, y, unclipped, unclipped, &mut path);
path
}
fn position_at(&self, x: u16, y: u16) -> Option<Position> {
let path = self.hit_test_path(x, y);
if let Some(target) = path
.iter()
.rev()
.find_map(|&id| inline_target_at(self, id, y))
{
if user_select::is_unselectable(self, *path.last()?) {
return None;
}
return resolve_in_target(self, target, x, y);
}
if let Some(&deepest) = path.last()
&& user_select::is_unselectable(self, deepest)
{
return None;
}
self.nearest_selectable_position(x, y)
}
fn nearest_selectable_position(&self, x: u16, y: u16) -> Option<Position> {
let path = self.hit_test_path(x, y);
let mut scope = path.last().copied();
loop {
let id = scope.unwrap_or_else(|| self.root());
if let Some(target) = nearest_inline_target_in_subtree(self, id, y) {
return resolve_in_target(self, target, x, y);
}
if id == self.root() {
return None;
}
scope = self.node(id).parent_node().map(|p| p.id());
}
}
}
#[cfg(test)]
mod tests;