use rdom_core::NodeRef;
use crate::runtime::hit_test::HitTestExt;
use crate::{TuiDom, TuiExt};
pub trait TuiDocAccessors {
fn element_from_point(&self, x: i32, y: i32) -> Option<NodeRef<'_, TuiExt>>;
fn elements_from_point(&self, x: i32, y: i32) -> Vec<NodeRef<'_, TuiExt>>;
fn caret_position_from_point(&self, x: i32, y: i32) -> Option<rdom_core::Position>;
}
impl TuiDocAccessors for TuiDom {
fn element_from_point(&self, x: i32, y: i32) -> Option<NodeRef<'_, TuiExt>> {
let (x, y) = clamp_cell_coords(x, y)?;
let id = self.hit_test(x, y)?;
Some(self.node(id))
}
fn elements_from_point(&self, x: i32, y: i32) -> Vec<NodeRef<'_, TuiExt>> {
let Some((x, y)) = clamp_cell_coords(x, y) else {
return Vec::new();
};
self.hit_test_path(x, y)
.into_iter()
.map(|id| self.node(id))
.collect()
}
fn caret_position_from_point(&self, x: i32, y: i32) -> Option<rdom_core::Position> {
let (x, y) = clamp_cell_coords(x, y)?;
self.position_at(x, y)
}
}
fn clamp_cell_coords(x: i32, y: i32) -> Option<(u16, u16)> {
let x = u16::try_from(x).ok()?;
let y = u16::try_from(y).ok()?;
Some((x, y))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Size;
use crate::style::Stylesheet;
fn dom_with_div_at_origin() -> (TuiDom, rdom_core::NodeId) {
use crate::LayoutExt;
use crate::render::Rect;
use crate::style::{CascadeExt, TuiStyle};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5)),
);
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
(dom, div)
}
#[test]
fn element_from_point_returns_hit_node_ref() {
let (dom, div) = dom_with_div_at_origin();
let el = dom.element_from_point(2, 2).expect("hit");
assert_eq!(el.id(), div);
}
#[test]
fn element_from_point_returns_none_for_off_screen() {
let (dom, _div) = dom_with_div_at_origin();
assert!(dom.element_from_point(200, 200).is_none());
}
#[test]
fn element_from_point_returns_none_for_negative_coords() {
let (dom, _div) = dom_with_div_at_origin();
assert!(dom.element_from_point(-1, 5).is_none());
assert!(dom.element_from_point(5, -1).is_none());
}
#[test]
fn elements_from_point_root_first_deepest_last() {
let (dom, div) = dom_with_div_at_origin();
let path: Vec<rdom_core::NodeId> = dom
.elements_from_point(2, 2)
.iter()
.map(|n| n.id())
.collect();
assert!(!path.is_empty());
assert_eq!(*path.last().unwrap(), div);
}
#[test]
fn elements_from_point_empty_for_off_screen() {
let (dom, _div) = dom_with_div_at_origin();
assert!(dom.elements_from_point(500, 500).is_empty());
}
#[test]
fn elements_from_point_empty_for_negative_coords() {
let (dom, _div) = dom_with_div_at_origin();
assert!(dom.elements_from_point(-1, 0).is_empty());
assert!(dom.elements_from_point(0, -1).is_empty());
}
#[test]
fn caret_position_from_point_none_outside_text() {
let (dom, _div) = dom_with_div_at_origin();
assert!(dom.caret_position_from_point(2, 2).is_none());
}
#[test]
fn caret_position_from_point_none_for_negative_coords() {
let (dom, _div) = dom_with_div_at_origin();
assert!(dom.caret_position_from_point(-1, 0).is_none());
}
}