Skip to main content

azul_core/
hit_test.rs

1//! Hit-test result types for determining which DOM nodes are under the cursor,
2//! scroll state tracking, and pipeline/document identification. These types
3//! feed into the event dispatch system.
4
5use alloc::collections::BTreeMap;
6use core::{
7    fmt,
8    sync::atomic::{AtomicU32, Ordering as AtomicOrdering},
9};
10
11use crate::{
12    dom::{DomId, DomNodeHash, DomNodeId, OptionDomNodeId, ScrollTagId, ScrollbarOrientation},
13    geom::{LogicalPosition, LogicalRect},
14    hit_test_tag::CursorType,
15    id::NodeId,
16    resources::IdNamespace,
17    window::MouseCursorType,
18    OrderedMap,
19};
20
21/// Result of a hit test against a single DOM, containing all nodes hit
22/// by the cursor along with scroll, scrollbar, and cursor-type information.
23#[derive(Debug, Clone, PartialEq, PartialOrd)]
24pub struct HitTest {
25    pub regular_hit_test_nodes: BTreeMap<NodeId, HitTestItem>,
26    pub scroll_hit_test_nodes: BTreeMap<NodeId, ScrollHitTestItem>,
27    /// Hit test results for scrollbar components.
28    pub scrollbar_hit_test_nodes: BTreeMap<ScrollbarHitId, ScrollbarHitTestItem>,
29    /// Hit test results for cursor areas (text runs with cursor property).
30    /// Maps NodeId to (CursorType, hit_depth) - the cursor type and z-depth of the hit.
31    pub cursor_hit_test_nodes: BTreeMap<NodeId, CursorHitTestItem>,
32}
33
34/// Hit test item for cursor areas (determines which cursor icon to show).
35#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
36#[repr(C)]
37pub struct CursorHitTestItem {
38    pub cursor_type: CursorType,
39    pub hit_depth: u32,
40    pub point_in_viewport: LogicalPosition,
41}
42
43impl HitTest {
44    pub fn empty() -> Self {
45        Self {
46            regular_hit_test_nodes: BTreeMap::new(),
47            scroll_hit_test_nodes: BTreeMap::new(),
48            scrollbar_hit_test_nodes: BTreeMap::new(),
49            cursor_hit_test_nodes: BTreeMap::new(),
50        }
51    }
52    pub fn is_empty(&self) -> bool {
53        self.regular_hit_test_nodes.is_empty()
54            && self.scroll_hit_test_nodes.is_empty()
55            && self.scrollbar_hit_test_nodes.is_empty()
56            && self.cursor_hit_test_nodes.is_empty()
57    }
58}
59
60/// Unique identifier for a specific component of a scrollbar.
61#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
62#[repr(C, u8)]
63pub enum ScrollbarHitId {
64    VerticalTrack(DomId, NodeId),
65    VerticalThumb(DomId, NodeId),
66    HorizontalTrack(DomId, NodeId),
67    HorizontalThumb(DomId, NodeId),
68}
69
70/// Hit test item specifically for scrollbar components.
71#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
72#[repr(C)]
73pub struct ScrollbarHitTestItem {
74    pub point_in_viewport: LogicalPosition,
75    pub point_relative_to_item: LogicalPosition,
76    pub orientation: ScrollbarOrientation,
77}
78
79/// Scroll frame identifier combining a unique `u64` tag with its owning `PipelineId`.
80#[derive(Copy, Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
81#[repr(C)]
82pub struct ExternalScrollId(pub u64, pub PipelineId);
83
84impl ::core::fmt::Display for ExternalScrollId {
85    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
86        write!(f, "ExternalScrollId({})", self.0)
87    }
88}
89
90impl ::core::fmt::Debug for ExternalScrollId {
91    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92        write!(f, "{}", self)
93    }
94}
95
96/// A node whose content overflows its parent, requiring scroll handling.
97#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
98pub struct OverflowingScrollNode {
99    pub parent_rect: LogicalRect,
100    pub child_rect: LogicalRect,
101    pub virtual_child_rect: LogicalRect,
102    pub parent_external_scroll_id: ExternalScrollId,
103    pub parent_dom_hash: DomNodeHash,
104    pub scroll_tag_id: ScrollTagId,
105}
106
107impl Default for OverflowingScrollNode {
108    fn default() -> Self {
109        use crate::dom::TagId;
110        Self {
111            parent_rect: LogicalRect::zero(),
112            child_rect: LogicalRect::zero(),
113            virtual_child_rect: LogicalRect::zero(),
114            parent_external_scroll_id: ExternalScrollId(0, PipelineId::DUMMY),
115            parent_dom_hash: DomNodeHash { inner: 0 },
116            scroll_tag_id: ScrollTagId {
117                inner: TagId { inner: 0 },
118            },
119        }
120    }
121}
122
123/// Extra source identifier within a pipeline, allowing multiple independent
124/// subsystems to generate `PipelineId` values without collision.
125/// All pipelines still share the same `IdNamespace` and `DocumentId`.
126pub type PipelineSourceId = u32;
127
128/// Information about a scroll frame, given to the user by the framework
129#[derive(Debug, Clone, PartialEq, PartialOrd)]
130pub struct ScrollPosition {
131    /// How big is the parent container
132    /// (so that things like "scroll to left edge" can be implemented)?
133    pub parent_rect: LogicalRect,
134    /// How big is the scroll rect (i.e. the union of all children)?
135    pub children_rect: LogicalRect,
136}
137
138/// Identifies a document within a namespace, used for multi-document rendering.
139#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
140pub struct DocumentId {
141    pub namespace_id: IdNamespace,
142    pub id: u32,
143}
144
145impl ::core::fmt::Display for DocumentId {
146    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
147        write!(
148            f,
149            "DocumentId {{ ns: {}, id: {} }}",
150            self.namespace_id, self.id
151        )
152    }
153}
154
155impl ::core::fmt::Debug for DocumentId {
156    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157        write!(f, "{}", self)
158    }
159}
160
161/// Identifies a rendering pipeline by source and sequence number.
162#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
163pub struct PipelineId(pub PipelineSourceId, pub u32);
164
165impl ::core::fmt::Display for PipelineId {
166    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
167        write!(f, "PipelineId({}, {})", self.0, self.1)
168    }
169}
170
171impl ::core::fmt::Debug for PipelineId {
172    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173        write!(f, "{}", self)
174    }
175}
176
177static LAST_PIPELINE_ID: AtomicU32 = AtomicU32::new(0);
178
179impl PipelineId {
180    pub const DUMMY: PipelineId = PipelineId(0, 0);
181
182    pub fn new() -> Self {
183        PipelineId(
184            LAST_PIPELINE_ID.fetch_add(1, AtomicOrdering::SeqCst),
185            0,
186        )
187    }
188}
189
190/// A single hit-test result for a regular (non-scroll) DOM node.
191#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
192pub struct HitTestItem {
193    /// The hit point in the coordinate space of the "viewport" of the display item.
194    /// The viewport is the scroll node formed by the root reference frame of the display item's
195    /// pipeline.
196    pub point_in_viewport: LogicalPosition,
197    /// The coordinates of the original hit test point relative to the origin of this item.
198    /// This is useful for calculating things like text offsets in the client.
199    pub point_relative_to_item: LogicalPosition,
200    /// Necessary to easily get the nearest VirtualView node
201    pub is_focusable: bool,
202    /// If this hit is a VirtualView node, stores the VirtualViews DomId + the origin of the VirtualView
203    pub is_virtual_view_hit: Option<(DomId, LogicalPosition)>,
204    /// Z-order depth from WebRender hit test (0 = frontmost/topmost in z-order).
205    /// Lower values are closer to the user. This preserves the ordering from
206    /// WebRender's hit test results which returns items front-to-back.
207    pub hit_depth: u32,
208}
209
210/// A hit-test result for a scrollable DOM node.
211#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
212pub struct ScrollHitTestItem {
213    /// The hit point in the coordinate space of the "viewport" of the display item.
214    /// The viewport is the scroll node formed by the root reference frame of the display item's
215    /// pipeline.
216    pub point_in_viewport: LogicalPosition,
217    /// The coordinates of the original hit test point relative to the origin of this item.
218    /// This is useful for calculating things like text offsets in the client.
219    pub point_relative_to_item: LogicalPosition,
220    /// If this hit is a VirtualView node, stores the VirtualViews DomId + the origin of the VirtualView
221    pub scroll_node: OverflowingScrollNode,
222}
223
224/// Map of active scroll states, keyed by their external scroll ID.
225#[derive(Debug, Default)]
226pub struct ScrollStates(pub OrderedMap<ExternalScrollId, ScrollState>);
227
228impl ScrollStates {
229    pub fn new() -> ScrollStates {
230        ScrollStates::default()
231    }
232
233    pub fn get_scroll_position(&self, scroll_id: &ExternalScrollId) -> Option<LogicalPosition> {
234        self.0.get(&scroll_id).map(|entry| entry.get())
235    }
236
237    /// Set the scroll amount - does not update the `entry.used_this_frame`,
238    /// since that is only relevant when we are actually querying the renderer.
239    pub fn set_scroll_position(
240        &mut self,
241        node: &OverflowingScrollNode,
242        scroll_position: LogicalPosition,
243    ) {
244        self.0
245            .entry(node.parent_external_scroll_id)
246            .or_default()
247            .set(scroll_position.x, scroll_position.y, &node.child_rect);
248    }
249
250    /// Updating (add to) the existing scroll amount does not update the
251    /// `entry.used_this_frame`, since that is only relevant when we are
252    /// actually querying the renderer.
253    pub fn scroll_node(
254        &mut self,
255        node: &OverflowingScrollNode,
256        scroll_by_x: f32,
257        scroll_by_y: f32,
258    ) {
259        self.0
260            .entry(node.parent_external_scroll_id)
261            .or_default()
262            .add(scroll_by_x, scroll_by_y, &node.child_rect);
263    }
264}
265
266/// Current scroll position for a single scroll frame.
267#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
268#[repr(C)]
269pub struct ScrollState {
270    /// Amount in pixel that the current node is scrolled
271    pub scroll_position: LogicalPosition,
272}
273
274impl_option!(
275    ScrollState,
276    OptionScrollState,
277    [Debug, Copy, Clone, PartialEq, PartialOrd]
278);
279
280impl ScrollState {
281    /// Return the current position of the scroll state
282    pub fn get(&self) -> LogicalPosition {
283        self.scroll_position
284    }
285
286    /// Add a scroll X / Y onto the existing scroll state
287    pub fn add(&mut self, x: f32, y: f32, child_rect: &LogicalRect) {
288        self.scroll_position.x = (self.scroll_position.x + x)
289            .max(0.0)
290            .min(child_rect.size.width);
291        self.scroll_position.y = (self.scroll_position.y + y)
292            .max(0.0)
293            .min(child_rect.size.height);
294    }
295
296    /// Set the scroll state to a new position
297    pub fn set(&mut self, x: f32, y: f32, child_rect: &LogicalRect) {
298        self.scroll_position.x = x.max(0.0).min(child_rect.size.width);
299        self.scroll_position.y = y.max(0.0).min(child_rect.size.height);
300    }
301}
302
303impl Default for ScrollState {
304    fn default() -> Self {
305        ScrollState {
306            scroll_position: LogicalPosition::zero(),
307        }
308    }
309}
310
311/// Complete hit-test result across all DOMs, including the currently focused node.
312#[derive(Debug, Clone, PartialEq)]
313pub struct FullHitTest {
314    pub hovered_nodes: BTreeMap<DomId, HitTest>,
315    pub focused_node: OptionDomNodeId,
316}
317
318impl FullHitTest {
319    /// Create an empty hit-test result
320    pub fn empty(focused_node: Option<DomNodeId>) -> Self {
321        Self {
322            hovered_nodes: BTreeMap::new(),
323            focused_node: focused_node.into(),
324        }
325    }
326
327    /// Returns `true` if no nodes were hovered (ignores `focused_node`).
328    pub fn is_empty(&self) -> bool {
329        self.hovered_nodes.is_empty()
330    }
331}
332
333/// Result of determining which mouse cursor icon to display based on hit-test results.
334#[derive(Debug, Clone, Default, PartialEq)]
335pub struct CursorTypeHitTest {
336    /// closest-node is used for determining the cursor: property
337    /// The node is guaranteed to have a non-default cursor: property,
338    /// so that the cursor icon can be set accordingly
339    pub cursor_node: Option<(DomId, NodeId)>,
340    /// Mouse cursor type to set (if cursor_node is None, this is set to
341    /// `MouseCursorType::Default`)
342    pub cursor_icon: MouseCursorType,
343}