Skip to main content

azul_core/
hit_test.rs

1use alloc::collections::BTreeMap;
2use core::{
3    fmt,
4    sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
5};
6
7use crate::{
8    dom::{DomId, DomNodeHash, DomNodeId, OptionDomNodeId, ScrollTagId, ScrollbarOrientation},
9    geom::{LogicalPosition, LogicalRect, LogicalSize},
10    hit_test_tag::CursorType,
11    id::NodeId,
12    resources::IdNamespace,
13    styled_dom::NodeHierarchyItemId,
14    window::MouseCursorType,
15    FastHashMap,
16};
17
18#[derive(Debug, Clone, PartialEq, PartialOrd)]
19pub struct HitTest {
20    pub regular_hit_test_nodes: BTreeMap<NodeId, HitTestItem>,
21    pub scroll_hit_test_nodes: BTreeMap<NodeId, ScrollHitTestItem>,
22    /// Hit test results for scrollbar components.
23    pub scrollbar_hit_test_nodes: BTreeMap<ScrollbarHitId, ScrollbarHitTestItem>,
24    /// Hit test results for cursor areas (text runs with cursor property).
25    /// Maps NodeId to (CursorType, hit_depth) - the cursor type and z-depth of the hit.
26    pub cursor_hit_test_nodes: BTreeMap<NodeId, CursorHitTestItem>,
27}
28
29/// Hit test item for cursor areas (determines which cursor icon to show).
30#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
31#[repr(C)]
32pub struct CursorHitTestItem {
33    pub cursor_type: CursorType,
34    pub hit_depth: u32,
35    pub point_in_viewport: LogicalPosition,
36}
37
38impl HitTest {
39    pub fn empty() -> Self {
40        Self {
41            regular_hit_test_nodes: BTreeMap::new(),
42            scroll_hit_test_nodes: BTreeMap::new(),
43            scrollbar_hit_test_nodes: BTreeMap::new(),
44            cursor_hit_test_nodes: BTreeMap::new(),
45        }
46    }
47    pub fn is_empty(&self) -> bool {
48        self.regular_hit_test_nodes.is_empty()
49            && self.scroll_hit_test_nodes.is_empty()
50            && self.scrollbar_hit_test_nodes.is_empty()
51            && self.cursor_hit_test_nodes.is_empty()
52    }
53}
54
55/// NEW: Unique identifier for a specific component of a scrollbar.
56#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57#[repr(C, u8)]
58pub enum ScrollbarHitId {
59    VerticalTrack(DomId, NodeId),
60    VerticalThumb(DomId, NodeId),
61    HorizontalTrack(DomId, NodeId),
62    HorizontalThumb(DomId, NodeId),
63}
64
65/// Hit test item specifically for scrollbar components.
66#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
67#[repr(C)]
68pub struct ScrollbarHitTestItem {
69    pub point_in_viewport: LogicalPosition,
70    pub point_relative_to_item: LogicalPosition,
71    pub orientation: ScrollbarOrientation,
72}
73
74#[derive(Copy, Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
75#[repr(C)]
76pub struct ExternalScrollId(pub u64, pub PipelineId);
77
78impl ::core::fmt::Display for ExternalScrollId {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "ExternalScrollId({})", self.0)
81    }
82}
83
84impl ::core::fmt::Debug for ExternalScrollId {
85    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
86        write!(f, "{}", self)
87    }
88}
89
90#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
91pub struct ScrolledNodes {
92    pub overflowing_nodes: BTreeMap<NodeHierarchyItemId, OverflowingScrollNode>,
93    /// Nodes that need to clip their direct children (i.e. nodes
94    /// with overflow-x and overflow-y set to "Hidden")
95    pub clip_nodes: BTreeMap<NodeId, LogicalSize>,
96    pub tags_to_node_ids: BTreeMap<ScrollTagId, NodeHierarchyItemId>,
97}
98
99#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
100pub struct OverflowingScrollNode {
101    pub parent_rect: LogicalRect,
102    pub child_rect: LogicalRect,
103    pub virtual_child_rect: LogicalRect,
104    pub parent_external_scroll_id: ExternalScrollId,
105    pub parent_dom_hash: DomNodeHash,
106    pub scroll_tag_id: ScrollTagId,
107}
108
109impl Default for OverflowingScrollNode {
110    fn default() -> Self {
111        use crate::dom::TagId;
112        Self {
113            parent_rect: LogicalRect::zero(),
114            child_rect: LogicalRect::zero(),
115            virtual_child_rect: LogicalRect::zero(),
116            parent_external_scroll_id: ExternalScrollId(0, PipelineId::DUMMY),
117            parent_dom_hash: DomNodeHash { inner: 0 },
118            scroll_tag_id: ScrollTagId {
119                inner: TagId { inner: 0 },
120            },
121        }
122    }
123}
124
125/// This type carries no valuable semantics for WR. However, it reflects the fact that
126/// clients (Servo) may generate pipelines by different semi-independent sources.
127///
128/// These pipelines still belong to the same `IdNamespace` and the same `DocumentId`.
129/// Having this extra Id field enables them to generate `PipelineId` without collision.
130pub type PipelineSourceId = u32;
131
132/// Information about a scroll frame, given to the user by the framework
133#[derive(Debug, Clone, PartialEq, PartialOrd)]
134pub struct ScrollPosition {
135    /// How big is the parent container
136    /// (so that things like "scroll to left edge" can be implemented)?
137    pub parent_rect: LogicalRect,
138    /// How big is the scroll rect (i.e. the union of all children)?
139    pub children_rect: LogicalRect,
140}
141
142#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
143pub struct DocumentId {
144    pub namespace_id: IdNamespace,
145    pub id: u32,
146}
147
148impl ::core::fmt::Display for DocumentId {
149    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150        write!(
151            f,
152            "DocumentId {{ ns: {}, id: {} }}",
153            self.namespace_id, self.id
154        )
155    }
156}
157
158impl ::core::fmt::Debug for DocumentId {
159    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160        write!(f, "{}", self)
161    }
162}
163
164#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
165pub struct PipelineId(pub PipelineSourceId, pub u32);
166
167impl ::core::fmt::Display for PipelineId {
168    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
169        write!(f, "PipelineId({}, {})", self.0, self.1)
170    }
171}
172
173impl ::core::fmt::Debug for PipelineId {
174    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
175        write!(f, "{}", self)
176    }
177}
178
179static LAST_PIPELINE_ID: AtomicUsize = AtomicUsize::new(0);
180
181impl PipelineId {
182    pub const DUMMY: PipelineId = PipelineId(0, 0);
183
184    pub fn new() -> Self {
185        PipelineId(
186            LAST_PIPELINE_ID.fetch_add(1, AtomicOrdering::SeqCst) as u32,
187            0,
188        )
189    }
190}
191
192#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
193pub struct HitTestItem {
194    /// The hit point in the coordinate space of the "viewport" of the display item.
195    /// The viewport is the scroll node formed by the root reference frame of the display item's
196    /// pipeline.
197    pub point_in_viewport: LogicalPosition,
198    /// The coordinates of the original hit test point relative to the origin of this item.
199    /// This is useful for calculating things like text offsets in the client.
200    pub point_relative_to_item: LogicalPosition,
201    /// Necessary to easily get the nearest IFrame node
202    pub is_focusable: bool,
203    /// If this hit is an IFrame node, stores the IFrames DomId + the origin of the IFrame
204    pub is_iframe_hit: Option<(DomId, LogicalPosition)>,
205    /// Z-order depth from WebRender hit test (0 = frontmost/topmost in z-order).
206    /// Lower values are closer to the user. This preserves the ordering from
207    /// WebRender's hit test results which returns items front-to-back.
208    pub hit_depth: u32,
209}
210
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 an IFrame node, stores the IFrames DomId + the origin of the IFrame
221    pub scroll_node: OverflowingScrollNode,
222}
223
224#[derive(Debug, Default)]
225pub struct ScrollStates(pub FastHashMap<ExternalScrollId, ScrollState>);
226
227impl ScrollStates {
228    /// Special rendering function that skips building a layout and only does
229    /// hit-testing and rendering - called on pure scroll events, since it's
230    /// significantly less CPU-intensive to just render the last display list instead of
231    /// re-layouting on every single scroll event.
232    #[must_use]
233    pub fn should_scroll_render(
234        &mut self,
235        (scroll_x, scroll_y): &(f32, f32),
236        hit_test: &FullHitTest,
237    ) -> bool {
238        let mut should_scroll_render = false;
239
240        for hit_test in hit_test.hovered_nodes.values() {
241            for scroll_hit_test_item in hit_test.scroll_hit_test_nodes.values() {
242                self.scroll_node(&scroll_hit_test_item.scroll_node, *scroll_x, *scroll_y);
243                should_scroll_render = true;
244                break; // only scroll first node that was hit
245            }
246        }
247
248        should_scroll_render
249    }
250
251    pub fn new() -> ScrollStates {
252        ScrollStates::default()
253    }
254
255    pub fn get_scroll_position(&self, scroll_id: &ExternalScrollId) -> Option<LogicalPosition> {
256        self.0.get(&scroll_id).map(|entry| entry.get())
257    }
258
259    /// Set the scroll amount - does not update the `entry.used_this_frame`,
260    /// since that is only relevant when we are actually querying the renderer.
261    pub fn set_scroll_position(
262        &mut self,
263        node: &OverflowingScrollNode,
264        scroll_position: LogicalPosition,
265    ) {
266        self.0
267            .entry(node.parent_external_scroll_id)
268            .or_insert_with(|| ScrollState::default())
269            .set(scroll_position.x, scroll_position.y, &node.child_rect);
270    }
271
272    /// Updating (add to) the existing scroll amount does not update the
273    /// `entry.used_this_frame`, since that is only relevant when we are
274    /// actually querying the renderer.
275    pub fn scroll_node(
276        &mut self,
277        node: &OverflowingScrollNode,
278        scroll_by_x: f32,
279        scroll_by_y: f32,
280    ) {
281        self.0
282            .entry(node.parent_external_scroll_id)
283            .or_insert_with(|| ScrollState::default())
284            .add(scroll_by_x, scroll_by_y, &node.child_rect);
285    }
286}
287
288#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
289#[repr(C)]
290pub struct ScrollState {
291    /// Amount in pixel that the current node is scrolled
292    pub scroll_position: LogicalPosition,
293}
294
295impl_option!(
296    ScrollState,
297    OptionScrollState,
298    [Debug, Copy, Clone, PartialEq, PartialOrd]
299);
300
301impl ScrollState {
302    /// Return the current position of the scroll state
303    pub fn get(&self) -> LogicalPosition {
304        self.scroll_position
305    }
306
307    /// Add a scroll X / Y onto the existing scroll state
308    pub fn add(&mut self, x: f32, y: f32, child_rect: &LogicalRect) {
309        self.scroll_position.x = (self.scroll_position.x + x)
310            .max(0.0)
311            .min(child_rect.size.width);
312        self.scroll_position.y = (self.scroll_position.y + y)
313            .max(0.0)
314            .min(child_rect.size.height);
315    }
316
317    /// Set the scroll state to a new position
318    pub fn set(&mut self, x: f32, y: f32, child_rect: &LogicalRect) {
319        self.scroll_position.x = x.max(0.0).min(child_rect.size.width);
320        self.scroll_position.y = y.max(0.0).min(child_rect.size.height);
321    }
322}
323
324impl Default for ScrollState {
325    fn default() -> Self {
326        ScrollState {
327            scroll_position: LogicalPosition::zero(),
328        }
329    }
330}
331
332#[derive(Debug, Clone, PartialEq)]
333pub struct FullHitTest {
334    pub hovered_nodes: BTreeMap<DomId, HitTest>,
335    pub focused_node: OptionDomNodeId,
336}
337
338pub struct FullHitTestHoveredNode {
339    pub dom_id: DomId,
340    pub hit_test: HitTest,
341}
342
343impl_option!(
344    FullHitTest,
345    OptionFullHitTest,
346    copy = false,
347    [Debug, Clone, PartialEq]
348);
349
350impl FullHitTest {
351    /// Create an empty hit-test result
352    pub fn empty(focused_node: Option<DomNodeId>) -> Self {
353        Self {
354            hovered_nodes: BTreeMap::new(),
355            focused_node: focused_node.into(),
356        }
357    }
358
359    /// Check if no nodes were hit
360    pub fn is_empty(&self) -> bool {
361        self.hovered_nodes.is_empty()
362    }
363}
364
365#[derive(Debug, Clone, Default, PartialEq)]
366pub struct CursorTypeHitTest {
367    /// closest-node is used for determining the cursor: property
368    /// The node is guaranteed to have a non-default cursor: property,
369    /// so that the cursor icon can be set accordingly
370    pub cursor_node: Option<(DomId, NodeId)>,
371    /// Mouse cursor type to set (if cursor_node is None, this is set to
372    /// `MouseCursorType::Default`)
373    pub cursor_icon: MouseCursorType,
374}