use alloc::collections::BTreeMap;
use core::{
fmt,
sync::atomic::{AtomicU32, Ordering as AtomicOrdering},
};
use crate::{
dom::{
DomId, DomNodeHash, DomNodeId, OptionDomNodeId, ScrollTagId, ScrollbarOrientation, TagId,
},
geom::{LogicalPosition, LogicalRect, LogicalSize},
id::NodeId,
resources::IdNamespace,
window::MouseCursorType,
OrderedMap,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
pub struct HitTest {
pub regular_hit_test_nodes: BTreeMap<NodeId, HitTestItem>,
pub scroll_hit_test_nodes: BTreeMap<NodeId, ScrollHitTestItem>,
pub scrollbar_hit_test_nodes: BTreeMap<ScrollbarHitId, ScrollbarHitTestItem>,
pub cursor_hit_test_nodes: BTreeMap<NodeId, CursorHitTestItem>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct CursorHitTestItem {
pub cursor_type: CursorType,
pub hit_depth: u32,
pub point_in_viewport: LogicalPosition,
}
impl HitTest {
#[must_use]
pub const fn empty() -> Self {
Self {
regular_hit_test_nodes: BTreeMap::new(),
scroll_hit_test_nodes: BTreeMap::new(),
scrollbar_hit_test_nodes: BTreeMap::new(),
cursor_hit_test_nodes: BTreeMap::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.regular_hit_test_nodes.is_empty()
&& self.scroll_hit_test_nodes.is_empty()
&& self.scrollbar_hit_test_nodes.is_empty()
&& self.cursor_hit_test_nodes.is_empty()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum ScrollbarHitId {
VerticalTrack(DomId, NodeId),
VerticalThumb(DomId, NodeId),
HorizontalTrack(DomId, NodeId),
HorizontalThumb(DomId, NodeId),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct ScrollbarHitTestItem {
pub point_in_viewport: LogicalPosition,
pub point_relative_to_item: LogicalPosition,
pub orientation: ScrollbarOrientation,
}
#[derive(Copy, Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
#[repr(C)]
pub struct ExternalScrollId(pub u64, pub PipelineId);
impl ::core::fmt::Display for ExternalScrollId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ExternalScrollId({})", self.0)
}
}
impl ::core::fmt::Debug for ExternalScrollId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
pub struct OverflowingScrollNode {
pub parent_rect: LogicalRect,
pub child_rect: LogicalRect,
pub virtual_child_rect: LogicalRect,
pub parent_external_scroll_id: ExternalScrollId,
pub parent_dom_hash: DomNodeHash,
pub scroll_tag_id: ScrollTagId,
}
impl Default for OverflowingScrollNode {
fn default() -> Self {
use crate::dom::TagId;
Self {
parent_rect: LogicalRect::zero(),
child_rect: LogicalRect::zero(),
virtual_child_rect: LogicalRect::zero(),
parent_external_scroll_id: ExternalScrollId(0, PipelineId::DUMMY),
parent_dom_hash: DomNodeHash { inner: 0 },
scroll_tag_id: ScrollTagId {
inner: TagId { inner: 0 },
},
}
}
}
pub type PipelineSourceId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
pub struct ScrollPosition {
pub parent_rect: LogicalRect,
pub children_rect: LogicalRect,
}
#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct DocumentId {
pub namespace_id: IdNamespace,
pub id: u32,
}
impl ::core::fmt::Display for DocumentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"DocumentId {{ ns: {}, id: {} }}",
self.namespace_id, self.id
)
}
}
impl ::core::fmt::Debug for DocumentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct PipelineId(pub PipelineSourceId, pub u32);
impl ::core::fmt::Display for PipelineId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PipelineId({}, {})", self.0, self.1)
}
}
impl ::core::fmt::Debug for PipelineId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
static LAST_PIPELINE_ID: AtomicU32 = AtomicU32::new(0);
impl Default for PipelineId {
fn default() -> Self {
Self::new()
}
}
impl PipelineId {
pub const DUMMY: Self = Self(0, 0);
pub fn new() -> Self {
Self(LAST_PIPELINE_ID.fetch_add(1, AtomicOrdering::SeqCst), 0)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
pub struct HitTestItem {
pub point_in_viewport: LogicalPosition,
pub point_relative_to_item: crate::spaces::ContentBoxLocal,
pub is_focusable: bool,
pub is_virtual_view_hit: Option<(DomId, LogicalPosition)>,
pub hit_depth: u32,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
pub struct ScrollHitTestItem {
pub point_in_viewport: LogicalPosition,
pub point_relative_to_item: crate::spaces::BorderBoxLocal,
pub scroll_node: OverflowingScrollNode,
}
#[derive(Debug, Default)]
pub struct ScrollStates(pub OrderedMap<ExternalScrollId, ScrollState>);
impl ScrollStates {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get_scroll_position(&self, scroll_id: &ExternalScrollId) -> Option<LogicalPosition> {
self.0.get(scroll_id).map(ScrollState::get)
}
pub fn set_scroll_position(
&mut self,
node: &OverflowingScrollNode,
scroll_position: LogicalPosition,
) {
let max_scroll = max_scroll_rect(node);
self.0
.entry(node.parent_external_scroll_id)
.or_default()
.set(scroll_position.x, scroll_position.y, &max_scroll);
}
pub fn scroll_node(
&mut self,
node: &OverflowingScrollNode,
scroll_by_x: f32,
scroll_by_y: f32,
) {
let max_scroll = max_scroll_rect(node);
self.0
.entry(node.parent_external_scroll_id)
.or_default()
.add(scroll_by_x, scroll_by_y, &max_scroll);
}
}
fn max_scroll_rect(node: &OverflowingScrollNode) -> LogicalRect {
LogicalRect::new(
node.child_rect.origin,
LogicalSize::new(
(node.child_rect.size.width - node.parent_rect.size.width).max(0.0),
(node.child_rect.size.height - node.parent_rect.size.height).max(0.0),
),
)
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct ScrollState {
pub scroll_position: LogicalPosition,
}
impl_option!(
ScrollState,
OptionScrollState,
[Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
);
impl ScrollState {
#[must_use]
pub const fn get(&self) -> LogicalPosition {
self.scroll_position
}
pub fn add(&mut self, x: f32, y: f32, max_scroll_rect: &LogicalRect) {
self.scroll_position.x = (self.scroll_position.x + x)
.max(0.0)
.min(max_scroll_rect.size.width.max(0.0));
self.scroll_position.y = (self.scroll_position.y + y)
.max(0.0)
.min(max_scroll_rect.size.height.max(0.0));
}
pub const fn set(&mut self, x: f32, y: f32, max_scroll_rect: &LogicalRect) {
self.scroll_position.x = x.max(0.0).min(max_scroll_rect.size.width.max(0.0));
self.scroll_position.y = y.max(0.0).min(max_scroll_rect.size.height.max(0.0));
}
}
impl Default for ScrollState {
fn default() -> Self {
Self {
scroll_position: LogicalPosition::zero(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FullHitTest {
pub hovered_nodes: BTreeMap<DomId, HitTest>,
pub focused_node: OptionDomNodeId,
}
impl FullHitTest {
#[must_use]
pub fn hovered_node_ids(&self) -> Vec<DomNodeId> {
let mut with_depth: Vec<(u32, DomNodeId)> = Vec::new();
for (dom_id, hit) in &self.hovered_nodes {
for (node_id, item) in &hit.regular_hit_test_nodes {
with_depth.push((
item.hit_depth,
DomNodeId {
dom: *dom_id,
node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(
*node_id,
)),
},
));
}
}
with_depth.sort_by_key(|(depth, _)| *depth);
with_depth.into_iter().map(|(_, id)| id).collect()
}
#[must_use]
pub fn topmost_node(&self) -> Option<DomNodeId> {
let mut best: Option<(u32, DomNodeId)> = None;
for (dom_id, hit) in &self.hovered_nodes {
for (node_id, item) in &hit.regular_hit_test_nodes {
let candidate = DomNodeId {
dom: *dom_id,
node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(
*node_id,
)),
};
if best.is_none_or(|(d, _)| item.hit_depth < d) {
best = Some((item.hit_depth, candidate));
}
}
}
best.map(|(_, id)| id)
}
}
impl FullHitTest {
#[must_use]
pub fn empty(focused_node: Option<DomNodeId>) -> Self {
Self {
hovered_nodes: BTreeMap::new(),
focused_node: focused_node.into(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.hovered_nodes.is_empty()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CursorTypeHitTest {
pub cursor_node: Option<(DomId, NodeId)>,
pub cursor_icon: MouseCursorType,
}
pub const TAG_TYPE_DOM_NODE: u16 = 0x0100;
pub const TAG_TYPE_SCROLLBAR: u16 = 0x0200;
pub const TAG_TYPE_SELECTION: u16 = 0x0300;
pub const TAG_TYPE_CURSOR: u16 = 0x0400;
pub const TAG_TYPE_SCROLL_CONTAINER: u16 = 0x0500;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum ScrollbarComponent {
VerticalTrack = 0,
VerticalThumb = 1,
HorizontalTrack = 2,
HorizontalThumb = 3,
}
impl ScrollbarComponent {
#[must_use]
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::VerticalTrack),
1 => Some(Self::VerticalThumb),
2 => Some(Self::HorizontalTrack),
3 => Some(Self::HorizontalThumb),
_ => None,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum HitTestTag {
DomNode {
tag_id: TagId,
},
Scrollbar {
dom_id: DomId,
node_id: NodeId,
component: ScrollbarComponent,
},
Cursor {
dom_id: DomId,
node_id: NodeId,
cursor_type: CursorType,
},
Selection {
dom_id: DomId,
container_node_id: NodeId,
text_run_index: u16,
},
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(u8)]
pub enum CursorType {
#[default]
Default = 0,
Pointer = 1,
Text = 2,
Crosshair = 3,
Move = 4,
NotAllowed = 5,
Grab = 6,
Grabbing = 7,
EResize = 8,
WResize = 9,
NResize = 10,
SResize = 11,
EwResize = 12,
NsResize = 13,
NeswResize = 14,
NwseResize = 15,
ColResize = 16,
RowResize = 17,
Wait = 18,
Help = 19,
Progress = 20,
}
impl CursorType {
#[allow(clippy::match_same_arms)]
#[must_use]
pub const fn from_u8(value: u8) -> Self {
match value {
0 => Self::Default,
1 => Self::Pointer,
2 => Self::Text,
3 => Self::Crosshair,
4 => Self::Move,
5 => Self::NotAllowed,
6 => Self::Grab,
7 => Self::Grabbing,
8 => Self::EResize,
9 => Self::WResize,
10 => Self::NResize,
11 => Self::SResize,
12 => Self::EwResize,
13 => Self::NsResize,
14 => Self::NeswResize,
15 => Self::NwseResize,
16 => Self::ColResize,
17 => Self::RowResize,
18 => Self::Wait,
19 => Self::Help,
20 => Self::Progress,
_ => Self::Default,
}
}
}
impl HitTestTag {
#[must_use]
pub fn to_item_tag(&self) -> (u64, u16) {
match self {
Self::DomNode { tag_id } => {
(tag_id.inner, TAG_TYPE_DOM_NODE)
}
Self::Scrollbar {
dom_id,
node_id,
component,
} => {
let tag_value = ((dom_id.inner as u64) << 32) | (node_id.index() as u64);
let tag_type = TAG_TYPE_SCROLLBAR | (*component as u16);
(tag_value, tag_type)
}
Self::Cursor {
dom_id,
node_id,
cursor_type,
} => {
let tag_value = ((dom_id.inner as u64) << 32) | (node_id.index() as u64);
let tag_type = TAG_TYPE_CURSOR | (*cursor_type as u16);
(tag_value, tag_type)
}
Self::Selection {
dom_id,
container_node_id,
text_run_index,
} => {
let dom_bits = (dom_id.inner as u64) & 0xFFFF;
let node_bits = (container_node_id.index() as u64) & 0xFFFF_FFFF;
let tag_value = (dom_bits << 48) | (node_bits << 16) | u64::from(*text_run_index);
(tag_value, TAG_TYPE_SELECTION)
}
}
}
#[must_use]
pub fn from_item_tag(tag: (u64, u16)) -> Option<Self> {
let (tag_value, tag_type) = tag;
let type_marker = tag_type & 0xFF00;
match type_marker {
TAG_TYPE_DOM_NODE => {
Some(Self::DomNode {
tag_id: TagId { inner: tag_value },
})
}
TAG_TYPE_SCROLLBAR => {
let dom_id = DomId {
inner: ((tag_value >> 32) & 0xFFFF_FFFF) as usize,
};
let node_id = NodeId::new((tag_value & 0xFFFF_FFFF) as usize);
let component_value = (tag_type & 0x00FF) as u8;
let component = ScrollbarComponent::from_u8(component_value)?;
Some(Self::Scrollbar {
dom_id,
node_id,
component,
})
}
TAG_TYPE_CURSOR => {
let dom_id = DomId {
inner: ((tag_value >> 32) & 0xFFFF_FFFF) as usize,
};
let node_id = NodeId::new((tag_value & 0xFFFF_FFFF) as usize);
let cursor_value = (tag_type & 0x00FF) as u8;
let cursor_type = CursorType::from_u8(cursor_value);
Some(Self::Cursor {
dom_id,
node_id,
cursor_type,
})
}
TAG_TYPE_SELECTION => {
let dom_id = DomId {
inner: ((tag_value >> 48) & 0xFFFF) as usize,
};
let container_node_id = NodeId::new(((tag_value >> 16) & 0xFFFF_FFFF) as usize);
let text_run_index = (tag_value & 0xFFFF) as u16;
Some(Self::Selection {
dom_id,
container_node_id,
text_run_index,
})
}
_ => {
if tag_type == 0 {
Some(Self::DomNode {
tag_id: TagId { inner: tag_value },
})
} else {
None
}
}
}
}
#[must_use]
pub const fn is_dom_node(&self) -> bool {
matches!(self, Self::DomNode { .. })
}
#[must_use]
pub const fn is_scrollbar(&self) -> bool {
matches!(self, Self::Scrollbar { .. })
}
#[must_use]
pub const fn is_cursor(&self) -> bool {
matches!(self, Self::Cursor { .. })
}
#[must_use]
pub const fn is_selection(&self) -> bool {
matches!(self, Self::Selection { .. })
}
#[must_use]
pub const fn as_dom_node(&self) -> Option<TagId> {
match self {
Self::DomNode { tag_id } => Some(*tag_id),
_ => None,
}
}
#[must_use]
pub const fn as_cursor(&self) -> Option<(DomId, NodeId, CursorType)> {
match self {
Self::Cursor {
dom_id,
node_id,
cursor_type,
} => Some((*dom_id, *node_id, *cursor_type)),
_ => None,
}
}
#[must_use]
pub const fn as_selection(&self) -> Option<(DomId, NodeId, u16)> {
match self {
Self::Selection {
dom_id,
container_node_id,
text_run_index,
} => Some((*dom_id, *container_node_id, *text_run_index)),
_ => None,
}
}
#[must_use]
pub const fn as_scrollbar(&self) -> Option<(DomId, NodeId, ScrollbarComponent)> {
match self {
Self::Scrollbar {
dom_id,
node_id,
component,
} => Some((*dom_id, *node_id, *component)),
_ => None,
}
}
}
impl fmt::Display for HitTestTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DomNode { tag_id } => {
write!(f, "DomNode(tag:{})", tag_id.inner)
}
Self::Scrollbar {
dom_id,
node_id,
component,
} => {
write!(
f,
"Scrollbar(dom:{}, node:{}, {:?})",
dom_id.inner,
node_id.index(),
component
)
}
Self::Cursor {
dom_id,
node_id,
cursor_type,
} => {
write!(
f,
"Cursor(dom:{}, node:{}, {:?})",
dom_id.inner,
node_id.index(),
cursor_type
)
}
Self::Selection {
dom_id,
container_node_id,
text_run_index,
} => {
write!(
f,
"Selection(dom:{}, container:{}, run:{})",
dom_id.inner,
container_node_id.index(),
text_run_index
)
}
}
}
}
#[cfg(test)]
#[path = "hit_test_test.rs"]
mod tests;