use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};
use crate::{
layout::{Rect, ScrollbarLayout, layoutbox::LayoutBox},
tdom::node::{NodeArena, NodeId},
};
use super::{ElementRef, Geometry};
#[derive(Debug, Clone)]
pub struct HitRegion {
pub node_id: NodeId,
pub rect: Rect,
pub client: Rect,
pub path: Arc<[ElementRef]>,
}
impl HitRegion {
pub fn id(&self) -> Option<&str> {
self.path.first().and_then(|e| e.id.as_deref())
}
}
pub type SharedHitMap = Arc<Mutex<Arc<HitMap>>>;
#[derive(Default)]
pub struct HitMap {
regions: Vec<HitRegion>,
unselectable: HashSet<NodeId>,
pub scrollbars: HashMap<NodeId, ScrollbarRegion>,
rows: Vec<Vec<u32>>,
}
impl HitMap {
pub fn new() -> HitMap {
HitMap::default()
}
pub fn len(&self) -> usize {
self.regions.len()
}
pub fn is_empty(&self) -> bool {
self.regions.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &HitRegion> {
self.regions.iter()
}
pub fn build(geometry: &Geometry) -> HitMap {
let mut regions = Vec::new();
let mut scrollbars = HashMap::new();
let mut unselectable = HashSet::new();
for (node_id, _) in geometry.arena.iter() {
if crate::layout::user_select_is_none(&geometry.arena, node_id) {
unselectable.insert(node_id);
}
}
let mut paths: HashMap<NodeId, Arc<[ElementRef]>> = HashMap::new();
let mut path_of = |node_id: NodeId| -> Arc<[ElementRef]> {
paths
.entry(node_id)
.or_insert_with(|| ancestor_path(&geometry.arena, node_id).into())
.clone()
};
collect_regions_roots(&geometry.boxes, &mut path_of, &mut regions, &mut scrollbars);
for (node_id, rect) in crate::layout::label_regions(&geometry.arena, &geometry.boxes) {
regions.push(HitRegion {
node_id,
rect,
client: rect,
path: path_of(node_id),
});
}
let mut rows: Vec<Vec<u32>> = Vec::new();
for (idx, region) in regions.iter().enumerate() {
let idx = idx as u32;
let row_end = region.rect.y.saturating_add(region.rect.height);
if rows.len() < row_end {
rows.resize_with(row_end, Vec::new);
}
for bucket in &mut rows[region.rect.y..row_end] {
bucket.push(idx);
}
}
HitMap {
regions,
unselectable,
scrollbars,
rows,
}
}
pub fn is_unselectable(&self, node_id: NodeId) -> bool {
self.unselectable.contains(&node_id)
}
pub fn hit_test(&self, x: u32, y: u32) -> Option<&HitRegion> {
let mut best: Option<&HitRegion> = None;
for &idx in self.rows.get(y as usize)? {
let region = &self.regions[idx as usize];
if region.rect.contains(x, y) {
best = Some(region);
}
}
best
}
pub fn resolve_by_id(&self, id: &str) -> Option<&HitRegion> {
self.regions.iter().find(|r| r.id() == Some(id))
}
pub fn region_for(&self, node_id: NodeId) -> Option<&HitRegion> {
self.regions.iter().find(|r| r.node_id == node_id)
}
pub fn scroll_container(
&self,
start: Option<NodeId>,
arena: &NodeArena,
) -> Option<&ScrollbarRegion> {
let mut current = start;
while let Some(node_id) = current {
if let Some(scrollbar) = self.scrollbars.get(&node_id) {
return Some(scrollbar);
}
current = arena.get(node_id).and_then(|node| node.parent);
}
None
}
}
fn collect_regions_roots(
roots: &[LayoutBox],
path_of: &mut impl FnMut(NodeId) -> Arc<[ElementRef]>,
out: &mut Vec<HitRegion>,
scrollbars: &mut HashMap<NodeId, ScrollbarRegion>,
) {
for root in roots {
collect_regions(root, path_of, out, scrollbars);
}
}
fn collect_regions(
box_tree: &LayoutBox,
path_of: &mut impl FnMut(NodeId) -> Arc<[ElementRef]>,
out: &mut Vec<HitRegion>,
scrollbars: &mut HashMap<NodeId, ScrollbarRegion>,
) {
let rect = box_tree.rect;
let border = &box_tree.paint_style.box_edges.border;
let client = Rect {
x: rect.x + border.left.width,
y: rect.y + border.top.width,
width: rect
.width
.saturating_sub(border.left.width + border.right.width),
height: rect
.height
.saturating_sub(border.top.width + border.bottom.width),
};
out.push(HitRegion {
node_id: box_tree.node_id,
rect,
client,
path: path_of(box_tree.node_id),
});
if let Some(scroll) = box_tree.paint_style.scrollbar {
scrollbars.insert(
box_tree.node_id,
ScrollbarRegion::new(box_tree.node_id, scroll),
);
}
let mut children: Vec<&LayoutBox> = box_tree.children.iter().collect();
children.sort_by_key(|c| crate::layout::paint::stacking_key(c));
for child in children {
collect_regions(child, path_of, out, scrollbars);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollbarRegion {
pub node_id: NodeId,
pub track: Rect,
pub thumb: Rect,
pub scroll_y: u32,
pub max_scroll: u32,
pub content_height: u32,
pub viewport_height: u32,
pub scroll_x: u32,
pub max_scroll_x: u32,
pub content_width: u32,
pub viewport_width: u32,
pub h_track: Rect,
pub h_thumb: Rect,
}
impl ScrollbarRegion {
fn new(node_id: NodeId, scroll: ScrollbarLayout) -> Self {
Self {
node_id,
track: scroll.track,
thumb: scroll.thumb,
scroll_y: scroll.scroll_y as u32,
max_scroll: scroll.max_scroll as u32,
content_height: scroll.content_height as u32,
viewport_height: scroll.viewport_height as u32,
scroll_x: scroll.scroll_x as u32,
max_scroll_x: scroll.max_scroll_x as u32,
content_width: scroll.content_width as u32,
viewport_width: scroll.viewport_width as u32,
h_track: scroll.h_track,
h_thumb: scroll.h_thumb,
}
}
pub fn offset_at_x(&self, x: u32, grab_offset: u32) -> u32 {
if self.h_track.width <= 1 || self.max_scroll_x == 0 {
return 0;
}
let x = x as usize;
let target = x
.saturating_sub(grab_offset as usize)
.saturating_sub(self.h_track.x)
.min(self.h_track.width.saturating_sub(self.h_thumb.width.max(1)));
let travel = self
.h_track
.width
.saturating_sub(self.h_thumb.width.max(1))
.max(1);
((target as u64 * self.max_scroll_x as u64) / travel as u64) as u32
}
pub fn offset_at(&self, y: u32, grab_offset: u32) -> u32 {
if self.track.height <= 1 || self.max_scroll == 0 {
return 0;
}
let y = y as usize;
let target = y
.saturating_sub(grab_offset as usize)
.saturating_sub(self.track.y)
.min(self.track.height.saturating_sub(self.thumb.height.max(1)));
let travel = self
.track
.height
.saturating_sub(self.thumb.height.max(1))
.max(1);
((target as u64 * self.max_scroll as u64) / travel as u64) as u32
}
}
fn ancestor_path(arena: &NodeArena, start: NodeId) -> Vec<ElementRef> {
let mut path = Vec::new();
let mut current = Some(start);
while let Some(id) = current {
let node = &arena[id];
path.push(ElementRef {
id: node.get_id().cloned(),
node_id: id,
});
current = node.parent;
}
path
}