1use 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, TagId},
13 geom::{LogicalPosition, LogicalRect, LogicalSize},
14 id::NodeId,
15 resources::IdNamespace,
16 window::MouseCursorType,
17 OrderedMap,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
23pub struct HitTest {
24 pub regular_hit_test_nodes: BTreeMap<NodeId, HitTestItem>,
25 pub scroll_hit_test_nodes: BTreeMap<NodeId, ScrollHitTestItem>,
26 pub scrollbar_hit_test_nodes: BTreeMap<ScrollbarHitId, ScrollbarHitTestItem>,
28 pub cursor_hit_test_nodes: BTreeMap<NodeId, CursorHitTestItem>,
31}
32
33#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
35#[repr(C)]
36pub struct CursorHitTestItem {
37 pub cursor_type: CursorType,
38 pub hit_depth: u32,
39 pub point_in_viewport: LogicalPosition,
40}
41
42impl HitTest {
43 #[must_use] pub const fn empty() -> Self {
44 Self {
45 regular_hit_test_nodes: BTreeMap::new(),
46 scroll_hit_test_nodes: BTreeMap::new(),
47 scrollbar_hit_test_nodes: BTreeMap::new(),
48 cursor_hit_test_nodes: BTreeMap::new(),
49 }
50 }
51 #[must_use] pub fn is_empty(&self) -> bool {
52 self.regular_hit_test_nodes.is_empty()
53 && self.scroll_hit_test_nodes.is_empty()
54 && self.scrollbar_hit_test_nodes.is_empty()
55 && self.cursor_hit_test_nodes.is_empty()
56 }
57}
58
59#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C, u8)]
62pub enum ScrollbarHitId {
63 VerticalTrack(DomId, NodeId),
64 VerticalThumb(DomId, NodeId),
65 HorizontalTrack(DomId, NodeId),
66 HorizontalThumb(DomId, NodeId),
67}
68
69#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
71#[repr(C)]
72pub struct ScrollbarHitTestItem {
73 pub point_in_viewport: LogicalPosition,
74 pub point_relative_to_item: LogicalPosition,
75 pub orientation: ScrollbarOrientation,
76}
77
78#[derive(Copy, Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
80#[repr(C)]
81pub struct ExternalScrollId(pub u64, pub PipelineId);
82
83impl ::core::fmt::Display for ExternalScrollId {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "ExternalScrollId({})", self.0)
86 }
87}
88
89impl ::core::fmt::Debug for ExternalScrollId {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "{self}")
92 }
93}
94
95#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
97pub struct OverflowingScrollNode {
98 pub parent_rect: LogicalRect,
99 pub child_rect: LogicalRect,
100 pub virtual_child_rect: LogicalRect,
101 pub parent_external_scroll_id: ExternalScrollId,
102 pub parent_dom_hash: DomNodeHash,
103 pub scroll_tag_id: ScrollTagId,
104}
105
106impl Default for OverflowingScrollNode {
107 fn default() -> Self {
108 use crate::dom::TagId;
109 Self {
110 parent_rect: LogicalRect::zero(),
111 child_rect: LogicalRect::zero(),
112 virtual_child_rect: LogicalRect::zero(),
113 parent_external_scroll_id: ExternalScrollId(0, PipelineId::DUMMY),
114 parent_dom_hash: DomNodeHash { inner: 0 },
115 scroll_tag_id: ScrollTagId {
116 inner: TagId { inner: 0 },
117 },
118 }
119 }
120}
121
122pub type PipelineSourceId = u32;
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
130pub struct ScrollPosition {
131 pub parent_rect: LogicalRect,
134 pub children_rect: LogicalRect,
136}
137
138#[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#[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 Default for PipelineId {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185impl PipelineId {
186 pub const DUMMY: Self = Self(0, 0);
187
188 pub fn new() -> Self {
189 Self(
190 LAST_PIPELINE_ID.fetch_add(1, AtomicOrdering::SeqCst),
191 0,
192 )
193 }
194}
195
196#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
198pub struct HitTestItem {
199 pub point_in_viewport: LogicalPosition,
203 pub point_relative_to_item: LogicalPosition,
206 pub is_focusable: bool,
208 pub is_virtual_view_hit: Option<(DomId, LogicalPosition)>,
210 pub hit_depth: u32,
214}
215
216#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
218pub struct ScrollHitTestItem {
219 pub point_in_viewport: LogicalPosition,
223 pub point_relative_to_item: LogicalPosition,
226 pub scroll_node: OverflowingScrollNode,
228}
229
230#[derive(Debug, Default)]
232pub struct ScrollStates(pub OrderedMap<ExternalScrollId, ScrollState>);
233
234impl ScrollStates {
235 #[must_use] pub fn new() -> Self {
236 Self::default()
237 }
238
239 #[must_use] pub fn get_scroll_position(&self, scroll_id: &ExternalScrollId) -> Option<LogicalPosition> {
240 self.0.get(scroll_id).map(ScrollState::get)
241 }
242
243 pub fn set_scroll_position(
246 &mut self,
247 node: &OverflowingScrollNode,
248 scroll_position: LogicalPosition,
249 ) {
250 let max_scroll = max_scroll_rect(node);
251 self.0
252 .entry(node.parent_external_scroll_id)
253 .or_default()
254 .set(scroll_position.x, scroll_position.y, &max_scroll);
255 }
256
257 pub fn scroll_node(
261 &mut self,
262 node: &OverflowingScrollNode,
263 scroll_by_x: f32,
264 scroll_by_y: f32,
265 ) {
266 let max_scroll = max_scroll_rect(node);
267 self.0
268 .entry(node.parent_external_scroll_id)
269 .or_default()
270 .add(scroll_by_x, scroll_by_y, &max_scroll);
271 }
272}
273
274fn max_scroll_rect(node: &OverflowingScrollNode) -> LogicalRect {
281 LogicalRect::new(
282 node.child_rect.origin,
283 LogicalSize::new(
284 (node.child_rect.size.width - node.parent_rect.size.width).max(0.0),
285 (node.child_rect.size.height - node.parent_rect.size.height).max(0.0),
286 ),
287 )
288}
289
290#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
292#[repr(C)]
293pub struct ScrollState {
294 pub scroll_position: LogicalPosition,
296}
297
298impl_option!(
299 ScrollState,
300 OptionScrollState,
301 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
302);
303
304impl ScrollState {
305 #[must_use] pub const fn get(&self) -> LogicalPosition {
307 self.scroll_position
308 }
309
310 pub fn add(&mut self, x: f32, y: f32, max_scroll_rect: &LogicalRect) {
318 self.scroll_position.x = (self.scroll_position.x + x)
319 .max(0.0)
320 .min(max_scroll_rect.size.width.max(0.0));
321 self.scroll_position.y = (self.scroll_position.y + y)
322 .max(0.0)
323 .min(max_scroll_rect.size.height.max(0.0));
324 }
325
326 pub const fn set(&mut self, x: f32, y: f32, max_scroll_rect: &LogicalRect) {
330 self.scroll_position.x = x.max(0.0).min(max_scroll_rect.size.width.max(0.0));
331 self.scroll_position.y = y.max(0.0).min(max_scroll_rect.size.height.max(0.0));
332 }
333}
334
335impl Default for ScrollState {
336 fn default() -> Self {
337 Self {
338 scroll_position: LogicalPosition::zero(),
339 }
340 }
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct FullHitTest {
346 pub hovered_nodes: BTreeMap<DomId, HitTest>,
347 pub focused_node: OptionDomNodeId,
348}
349
350impl FullHitTest {
351 #[must_use] 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 #[must_use] pub fn is_empty(&self) -> bool {
361 self.hovered_nodes.is_empty()
362 }
363}
364
365#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
367pub struct CursorTypeHitTest {
368 pub cursor_node: Option<(DomId, NodeId)>,
372 pub cursor_icon: MouseCursorType,
375}
376
377pub const TAG_TYPE_DOM_NODE: u16 = 0x0100;
390
391pub const TAG_TYPE_SCROLLBAR: u16 = 0x0200;
393
394pub const TAG_TYPE_SELECTION: u16 = 0x0300;
403
404pub const TAG_TYPE_CURSOR: u16 = 0x0400;
409
410pub const TAG_TYPE_SCROLL_CONTAINER: u16 = 0x0500;
415
416
417#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
430#[repr(u8)]
431pub enum ScrollbarComponent {
432 VerticalTrack = 0,
434 VerticalThumb = 1,
436 HorizontalTrack = 2,
438 HorizontalThumb = 3,
440 }
446
447impl ScrollbarComponent {
448 #[must_use] pub const fn from_u8(value: u8) -> Option<Self> {
450 match value {
451 0 => Some(Self::VerticalTrack),
452 1 => Some(Self::VerticalThumb),
453 2 => Some(Self::HorizontalTrack),
454 3 => Some(Self::HorizontalThumb),
455 _ => None,
456 }
457 }
458
459}
460
461#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
484pub enum HitTestTag {
485 DomNode {
490 tag_id: TagId,
492 },
493
494 Scrollbar {
499 dom_id: DomId,
501 node_id: NodeId,
503 component: ScrollbarComponent,
505 },
506
507 Cursor {
512 dom_id: DomId,
514 node_id: NodeId,
516 cursor_type: CursorType,
518 },
519
520 Selection {
525 dom_id: DomId,
527 container_node_id: NodeId,
529 text_run_index: u16,
531 },
532}
533
534#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
537#[repr(u8)]
538pub enum CursorType {
539 #[default]
540 Default = 0,
541 Pointer = 1,
542 Text = 2,
543 Crosshair = 3,
544 Move = 4,
545 NotAllowed = 5,
546 Grab = 6,
547 Grabbing = 7,
548 EResize = 8,
549 WResize = 9,
550 NResize = 10,
551 SResize = 11,
552 EwResize = 12,
553 NsResize = 13,
554 NeswResize = 14,
555 NwseResize = 15,
556 ColResize = 16,
557 RowResize = 17,
558 Wait = 18,
559 Help = 19,
560 Progress = 20,
561 }
563
564impl CursorType {
565 #[allow(clippy::match_same_arms)]
569 #[must_use] pub const fn from_u8(value: u8) -> Self {
570 match value {
571 0 => Self::Default,
572 1 => Self::Pointer,
573 2 => Self::Text,
574 3 => Self::Crosshair,
575 4 => Self::Move,
576 5 => Self::NotAllowed,
577 6 => Self::Grab,
578 7 => Self::Grabbing,
579 8 => Self::EResize,
580 9 => Self::WResize,
581 10 => Self::NResize,
582 11 => Self::SResize,
583 12 => Self::EwResize,
584 13 => Self::NsResize,
585 14 => Self::NeswResize,
586 15 => Self::NwseResize,
587 16 => Self::ColResize,
588 17 => Self::RowResize,
589 18 => Self::Wait,
590 19 => Self::Help,
591 20 => Self::Progress,
592 _ => Self::Default,
593 }
594 }
595}
596
597impl HitTestTag {
598 #[must_use] pub fn to_item_tag(&self) -> (u64, u16) {
602 match self {
603 Self::DomNode { tag_id } => {
604 (tag_id.inner, TAG_TYPE_DOM_NODE)
607 }
608 Self::Scrollbar {
609 dom_id,
610 node_id,
611 component,
612 } => {
613 let tag_value = ((dom_id.inner as u64) << 32) | (node_id.index() as u64);
615 let tag_type = TAG_TYPE_SCROLLBAR | (*component as u16);
617 (tag_value, tag_type)
618 }
619 Self::Cursor {
620 dom_id,
621 node_id,
622 cursor_type,
623 } => {
624 let tag_value = ((dom_id.inner as u64) << 32) | (node_id.index() as u64);
626 let tag_type = TAG_TYPE_CURSOR | (*cursor_type as u16);
628 (tag_value, tag_type)
629 }
630 Self::Selection {
631 dom_id,
632 container_node_id,
633 text_run_index,
634 } => {
635 let dom_bits = (dom_id.inner as u64) & 0xFFFF;
641 let node_bits = (container_node_id.index() as u64) & 0xFFFF_FFFF;
642 let tag_value = (dom_bits << 48)
643 | (node_bits << 16)
644 | u64::from(*text_run_index);
645 (tag_value, TAG_TYPE_SELECTION)
646 }
647 }
648 }
649
650 #[must_use] pub fn from_item_tag(tag: (u64, u16)) -> Option<Self> {
654 let (tag_value, tag_type) = tag;
655
656 let type_marker = tag_type & 0xFF00;
658
659 match type_marker {
660 TAG_TYPE_DOM_NODE => {
661 Some(Self::DomNode {
663 tag_id: TagId { inner: tag_value },
664 })
665 }
666 TAG_TYPE_SCROLLBAR => {
667 let dom_id = DomId {
669 inner: ((tag_value >> 32) & 0xFFFF_FFFF) as usize,
670 };
671 let node_id = NodeId::new((tag_value & 0xFFFF_FFFF) as usize);
672 let component_value = (tag_type & 0x00FF) as u8;
673 let component = ScrollbarComponent::from_u8(component_value)?;
674
675 Some(Self::Scrollbar {
676 dom_id,
677 node_id,
678 component,
679 })
680 }
681 TAG_TYPE_CURSOR => {
682 let dom_id = DomId {
684 inner: ((tag_value >> 32) & 0xFFFF_FFFF) as usize,
685 };
686 let node_id = NodeId::new((tag_value & 0xFFFF_FFFF) as usize);
687 let cursor_value = (tag_type & 0x00FF) as u8;
688 let cursor_type = CursorType::from_u8(cursor_value);
689
690 Some(Self::Cursor {
691 dom_id,
692 node_id,
693 cursor_type,
694 })
695 }
696 TAG_TYPE_SELECTION => {
697 let dom_id = DomId {
699 inner: ((tag_value >> 48) & 0xFFFF) as usize,
700 };
701 let container_node_id = NodeId::new(((tag_value >> 16) & 0xFFFF_FFFF) as usize);
702 let text_run_index = (tag_value & 0xFFFF) as u16;
703
704 Some(Self::Selection {
705 dom_id,
706 container_node_id,
707 text_run_index,
708 })
709 }
710 _ => {
711 if tag_type == 0 {
715 Some(Self::DomNode {
716 tag_id: TagId { inner: tag_value },
717 })
718 } else {
719 None
720 }
721 }
722 }
723 }
724
725 #[must_use] pub const fn is_dom_node(&self) -> bool {
727 matches!(self, Self::DomNode { .. })
728 }
729
730 #[must_use] pub const fn is_scrollbar(&self) -> bool {
732 matches!(self, Self::Scrollbar { .. })
733 }
734
735 #[must_use] pub const fn is_cursor(&self) -> bool {
737 matches!(self, Self::Cursor { .. })
738 }
739
740 #[must_use] pub const fn is_selection(&self) -> bool {
742 matches!(self, Self::Selection { .. })
743 }
744
745 #[must_use] pub const fn as_dom_node(&self) -> Option<TagId> {
747 match self {
748 Self::DomNode { tag_id } => Some(*tag_id),
749 _ => None,
750 }
751 }
752
753 #[must_use] pub const fn as_cursor(&self) -> Option<(DomId, NodeId, CursorType)> {
755 match self {
756 Self::Cursor {
757 dom_id,
758 node_id,
759 cursor_type,
760 } => Some((*dom_id, *node_id, *cursor_type)),
761 _ => None,
762 }
763 }
764
765 #[must_use] pub const fn as_selection(&self) -> Option<(DomId, NodeId, u16)> {
767 match self {
768 Self::Selection {
769 dom_id,
770 container_node_id,
771 text_run_index,
772 } => Some((*dom_id, *container_node_id, *text_run_index)),
773 _ => None,
774 }
775 }
776
777 #[must_use] pub const fn as_scrollbar(&self) -> Option<(DomId, NodeId, ScrollbarComponent)> {
779 match self {
780 Self::Scrollbar {
781 dom_id,
782 node_id,
783 component,
784 } => Some((*dom_id, *node_id, *component)),
785 _ => None,
786 }
787 }
788}
789
790impl fmt::Display for HitTestTag {
791 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792 match self {
793 Self::DomNode { tag_id } => {
794 write!(f, "DomNode(tag:{})", tag_id.inner)
795 }
796 Self::Scrollbar {
797 dom_id,
798 node_id,
799 component,
800 } => {
801 write!(
802 f,
803 "Scrollbar(dom:{}, node:{}, {:?})",
804 dom_id.inner,
805 node_id.index(),
806 component
807 )
808 }
809 Self::Cursor {
810 dom_id,
811 node_id,
812 cursor_type,
813 } => {
814 write!(
815 f,
816 "Cursor(dom:{}, node:{}, {:?})",
817 dom_id.inner,
818 node_id.index(),
819 cursor_type
820 )
821 }
822 Self::Selection {
823 dom_id,
824 container_node_id,
825 text_run_index,
826 } => {
827 write!(
828 f,
829 "Selection(dom:{}, container:{}, run:{})",
830 dom_id.inner,
831 container_node_id.index(),
832 text_run_index
833 )
834 }
835 }
836 }
837}
838
839#[cfg(test)]
840#[allow(clippy::float_cmp)] mod tests {
842 use super::*;
843
844 #[test]
845 fn test_dom_node_tag_roundtrip() {
846 let tag = HitTestTag::DomNode {
847 tag_id: TagId { inner: 42 },
848 };
849 let item_tag = tag.to_item_tag();
850 let decoded = HitTestTag::from_item_tag(item_tag).unwrap();
851 assert_eq!(tag, decoded);
852 }
853
854 #[test]
855 fn test_scrollbar_tag_roundtrip() {
856 let tag = HitTestTag::Scrollbar {
857 dom_id: DomId { inner: 1 },
858 node_id: NodeId::new(123),
859 component: ScrollbarComponent::VerticalThumb,
860 };
861 let item_tag = tag.to_item_tag();
862 let decoded = HitTestTag::from_item_tag(item_tag).unwrap();
863 assert_eq!(tag, decoded);
864 }
865
866 #[test]
867 fn test_dom_node_tag_not_confused_with_scrollbar() {
868 let dom_tag = HitTestTag::DomNode {
870 tag_id: TagId { inner: 673 },
871 };
872 let item_tag = dom_tag.to_item_tag();
873
874 assert_eq!(item_tag.1, TAG_TYPE_DOM_NODE);
876
877 let decoded = HitTestTag::from_item_tag(item_tag).unwrap();
879 assert!(decoded.is_dom_node());
880 assert!(!decoded.is_scrollbar());
881 }
882
883 #[test]
884 fn test_legacy_tag_compatibility() {
885 let legacy_tag = (42u64, 0u16);
888 let decoded = HitTestTag::from_item_tag(legacy_tag).unwrap();
889 assert!(decoded.is_dom_node());
890 assert_eq!(decoded.as_dom_node().unwrap().inner, 42);
891 }
892
893 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
894 LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
895 }
896
897 #[test]
898 fn scroll_state_clamps_to_content_minus_viewport() {
899 let max = max_scroll_rect(&OverflowingScrollNode {
901 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
902 child_rect: rect(0.0, 0.0, 100.0, 300.0),
903 ..Default::default()
904 });
905 assert_eq!(max.size.height, 200.0);
906
907 let mut st = ScrollState::default();
908 st.set(0.0, 999.0, &max);
909 assert_eq!(st.scroll_position.y, 200.0); }
911
912 #[test]
913 fn scroll_state_no_scroll_when_content_fits() {
914 let max = max_scroll_rect(&OverflowingScrollNode {
916 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
917 child_rect: rect(0.0, 0.0, 100.0, 100.0),
918 ..Default::default()
919 });
920 let mut st = ScrollState::default();
921 st.add(50.0, 50.0, &max);
922 assert_eq!(st.scroll_position, LogicalPosition::zero());
923 }
924
925 #[test]
926 fn scroll_state_nan_delta_does_not_poison() {
927 let max = rect(0.0, 0.0, 100.0, 200.0);
928 let mut st = ScrollState::default();
929 st.add(f32::NAN, f32::NAN, &max);
930 assert_eq!(st.scroll_position, LogicalPosition::zero());
931 }
932
933 #[test]
934 fn selection_tag_out_of_range_domid_is_clamped_not_corrupting() {
935 let tag = HitTestTag::Selection {
938 dom_id: DomId { inner: 0x1_0007 }, container_node_id: NodeId::new(5),
940 text_run_index: 9,
941 };
942 let (value, ty) = tag.to_item_tag();
943 assert_eq!(ty, TAG_TYPE_SELECTION);
944 assert_eq!((value >> 16) & 0xFFFF_FFFF, 5);
946 assert_eq!(value >> 48, 0x0007);
948 }
949}
950
951#[cfg(test)]
952#[allow(
953 clippy::float_cmp,
954 clippy::cast_lossless,
955 clippy::cast_possible_truncation,
956 clippy::unreadable_literal
957)]
958mod autotest_generated {
959 use super::*;
960
961 fn r(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
966 LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
967 }
968
969 fn scroll_node_of(parent: LogicalRect, child: LogicalRect) -> OverflowingScrollNode {
972 OverflowingScrollNode {
973 parent_rect: parent,
974 child_rect: child,
975 ..Default::default()
976 }
977 }
978
979 fn ext_id(raw: u64) -> ExternalScrollId {
980 ExternalScrollId(raw, PipelineId::DUMMY)
981 }
982
983 const ALL_CURSORS: [CursorType; 21] = [
984 CursorType::Default,
985 CursorType::Pointer,
986 CursorType::Text,
987 CursorType::Crosshair,
988 CursorType::Move,
989 CursorType::NotAllowed,
990 CursorType::Grab,
991 CursorType::Grabbing,
992 CursorType::EResize,
993 CursorType::WResize,
994 CursorType::NResize,
995 CursorType::SResize,
996 CursorType::EwResize,
997 CursorType::NsResize,
998 CursorType::NeswResize,
999 CursorType::NwseResize,
1000 CursorType::ColResize,
1001 CursorType::RowResize,
1002 CursorType::Wait,
1003 CursorType::Help,
1004 CursorType::Progress,
1005 ];
1006
1007 const ALL_SCROLLBAR_COMPONENTS: [ScrollbarComponent; 4] = [
1008 ScrollbarComponent::VerticalTrack,
1009 ScrollbarComponent::VerticalThumb,
1010 ScrollbarComponent::HorizontalTrack,
1011 ScrollbarComponent::HorizontalThumb,
1012 ];
1013
1014 #[test]
1019 fn hit_test_empty_is_neutral_element() {
1020 let h = HitTest::empty();
1021 assert!(h.is_empty());
1022 assert_eq!(h.regular_hit_test_nodes.len(), 0);
1023 assert_eq!(h.scroll_hit_test_nodes.len(), 0);
1024 assert_eq!(h.scrollbar_hit_test_nodes.len(), 0);
1025 assert_eq!(h.cursor_hit_test_nodes.len(), 0);
1026 assert_eq!(HitTest::empty(), HitTest::empty());
1028 }
1029
1030 #[test]
1031 fn hit_test_is_empty_false_if_any_single_map_is_populated() {
1032 let mut a = HitTest::empty();
1035 a.regular_hit_test_nodes.insert(
1036 NodeId::ZERO,
1037 HitTestItem {
1038 point_in_viewport: LogicalPosition::zero(),
1039 point_relative_to_item: LogicalPosition::zero(),
1040 is_focusable: false,
1041 is_virtual_view_hit: None,
1042 hit_depth: 0,
1043 },
1044 );
1045 assert!(!a.is_empty());
1046
1047 let mut b = HitTest::empty();
1048 b.scroll_hit_test_nodes.insert(
1049 NodeId::new(usize::MAX),
1050 ScrollHitTestItem {
1051 point_in_viewport: LogicalPosition::zero(),
1052 point_relative_to_item: LogicalPosition::zero(),
1053 scroll_node: OverflowingScrollNode::default(),
1054 },
1055 );
1056 assert!(!b.is_empty());
1057
1058 let mut c = HitTest::empty();
1059 c.scrollbar_hit_test_nodes.insert(
1060 ScrollbarHitId::VerticalTrack(DomId::ROOT_ID, NodeId::ZERO),
1061 ScrollbarHitTestItem {
1062 point_in_viewport: LogicalPosition::zero(),
1063 point_relative_to_item: LogicalPosition::zero(),
1064 orientation: ScrollbarOrientation::Vertical,
1065 },
1066 );
1067 assert!(!c.is_empty());
1068
1069 let mut d = HitTest::empty();
1070 d.cursor_hit_test_nodes.insert(
1071 NodeId::ZERO,
1072 CursorHitTestItem {
1073 cursor_type: CursorType::Default,
1074 hit_depth: u32::MAX,
1075 point_in_viewport: LogicalPosition::new(f32::NAN, f32::INFINITY),
1076 },
1077 );
1078 assert!(!d.is_empty());
1079 }
1080
1081 #[test]
1082 fn full_hit_test_empty_is_empty_regardless_of_focused_node() {
1083 let none = FullHitTest::empty(None);
1084 assert!(none.is_empty());
1085 assert!(none.focused_node.is_none());
1086
1087 let focused = FullHitTest::empty(Some(DomNodeId::ROOT));
1090 assert!(focused.is_empty());
1091 assert!(focused.focused_node.is_some());
1092 assert_eq!(focused.hovered_nodes.len(), 0);
1093 }
1094
1095 #[test]
1096 fn full_hit_test_is_empty_false_once_a_dom_is_hovered() {
1097 let mut f = FullHitTest::empty(None);
1098 f.hovered_nodes.insert(DomId::ROOT_ID, HitTest::empty());
1099 assert!(!f.is_empty());
1102 }
1103
1104 #[test]
1109 fn pipeline_id_new_is_monotonic_and_second_field_is_zero() {
1110 let a = PipelineId::new();
1111 let b = PipelineId::new();
1112 let c = PipelineId::default();
1113 assert!(b.0 > a.0);
1116 assert!(c.0 > b.0);
1117 assert_eq!(a.1, 0);
1118 assert_eq!(b.1, 0);
1119 assert_eq!(PipelineId::DUMMY, PipelineId(0, 0));
1120 }
1121
1122 #[test]
1123 fn pipeline_id_display_and_debug_agree_on_edge_values() {
1124 assert_eq!(alloc::format!("{}", PipelineId::DUMMY), "PipelineId(0, 0)");
1125 let maxed = PipelineId(u32::MAX, u32::MAX);
1126 let shown = alloc::format!("{maxed}");
1127 assert_eq!(shown, "PipelineId(4294967295, 4294967295)");
1128 assert_eq!(alloc::format!("{maxed:?}"), shown);
1129 }
1130
1131 #[test]
1132 fn document_id_display_handles_min_and_max() {
1133 let zero = DocumentId {
1134 namespace_id: IdNamespace(0),
1135 id: 0,
1136 };
1137 let maxed = DocumentId {
1138 namespace_id: IdNamespace(u32::MAX),
1139 id: u32::MAX,
1140 };
1141 for d in [zero, maxed] {
1142 let shown = alloc::format!("{d}");
1143 assert!(!shown.is_empty());
1144 assert!(shown.starts_with("DocumentId {"));
1145 assert_eq!(alloc::format!("{d:?}"), shown);
1147 }
1148 assert!(alloc::format!("{maxed}").contains("4294967295"));
1149 }
1150
1151 #[test]
1152 fn external_scroll_id_display_omits_the_pipeline_but_keys_stay_distinct() {
1153 let a = ExternalScrollId(7, PipelineId(1, 0));
1154 let b = ExternalScrollId(7, PipelineId(2, 0));
1155
1156 assert_eq!(alloc::format!("{a}"), "ExternalScrollId(7)");
1160 assert_eq!(alloc::format!("{a:?}"), alloc::format!("{b:?}"));
1161 assert_ne!(a, b);
1162
1163 let mut states = ScrollStates::new();
1164 states.0.insert(a, ScrollState::default());
1165 states.0.insert(b, ScrollState::default());
1166 assert_eq!(states.0.len(), 2);
1167 }
1168
1169 #[test]
1170 fn external_scroll_id_display_no_panic_at_u64_max() {
1171 let shown = alloc::format!("{}", ExternalScrollId(u64::MAX, PipelineId::DUMMY));
1172 assert_eq!(shown, "ExternalScrollId(18446744073709551615)");
1173 }
1174
1175 #[test]
1180 fn max_scroll_rect_is_content_minus_viewport_and_keeps_origin() {
1181 let max = max_scroll_rect(&scroll_node_of(
1182 r(0.0, 0.0, 100.0, 100.0),
1183 r(-12.5, 7.25, 400.0, 300.0),
1184 ));
1185 assert_eq!(max.size.width, 300.0);
1186 assert_eq!(max.size.height, 200.0);
1187 assert_eq!(max.origin.x, -12.5);
1189 assert_eq!(max.origin.y, 7.25);
1190 }
1191
1192 #[test]
1193 fn max_scroll_rect_clamps_negative_range_to_zero() {
1194 let max = max_scroll_rect(&scroll_node_of(
1196 r(0.0, 0.0, 500.0, 500.0),
1197 r(0.0, 0.0, 100.0, 100.0),
1198 ));
1199 assert_eq!(max.size.width, 0.0);
1200 assert_eq!(max.size.height, 0.0);
1201 }
1202
1203 #[test]
1204 fn max_scroll_rect_nan_and_infinite_sizes_do_not_produce_nan() {
1205 let nan = max_scroll_rect(&scroll_node_of(
1207 r(0.0, 0.0, 10.0, 10.0),
1208 r(0.0, 0.0, f32::NAN, f32::NAN),
1209 ));
1210 assert!(!nan.size.width.is_nan());
1211 assert!(!nan.size.height.is_nan());
1212 assert_eq!(nan.size.width, 0.0);
1213 assert_eq!(nan.size.height, 0.0);
1214
1215 let both_inf = max_scroll_rect(&scroll_node_of(
1217 r(0.0, 0.0, f32::INFINITY, f32::INFINITY),
1218 r(0.0, 0.0, f32::INFINITY, f32::INFINITY),
1219 ));
1220 assert_eq!(both_inf.size.width, 0.0);
1221 assert_eq!(both_inf.size.height, 0.0);
1222
1223 let inf = max_scroll_rect(&scroll_node_of(
1225 r(0.0, 0.0, 10.0, 10.0),
1226 r(0.0, 0.0, f32::INFINITY, f32::INFINITY),
1227 ));
1228 assert!(inf.size.width.is_infinite() && inf.size.width.is_sign_positive());
1229 assert!(inf.size.height.is_infinite() && inf.size.height.is_sign_positive());
1230 }
1231
1232 #[test]
1233 fn max_scroll_rect_at_float_extremes_does_not_panic() {
1234 let max = max_scroll_rect(&scroll_node_of(
1235 r(f32::MIN, f32::MIN, f32::MIN_POSITIVE, f32::MAX),
1236 r(f32::MAX, f32::MAX, f32::MAX, f32::MIN_POSITIVE),
1237 ));
1238 assert!(max.size.width >= 0.0);
1239 assert!(max.size.height >= 0.0);
1240 assert!(!max.size.width.is_nan());
1241 assert!(!max.size.height.is_nan());
1242 }
1243
1244 #[test]
1249 fn scroll_state_default_and_get_round_trip() {
1250 let st = ScrollState::default();
1251 assert_eq!(st.get(), LogicalPosition::zero());
1252
1253 let st = ScrollState {
1254 scroll_position: LogicalPosition::new(3.5, -4.25),
1255 };
1256 assert_eq!(st.get().x, 3.5);
1258 assert_eq!(st.get().y, -4.25);
1259 }
1260
1261 #[test]
1262 fn scroll_state_set_zero_is_identity_within_range() {
1263 let max = r(0.0, 0.0, 100.0, 200.0);
1264 let mut st = ScrollState::default();
1265 st.set(0.0, 0.0, &max);
1266 assert_eq!(st.get(), LogicalPosition::zero());
1267
1268 st.set(50.0, 150.0, &max);
1269 assert_eq!(st.get().x, 50.0);
1270 assert_eq!(st.get().y, 150.0);
1271 }
1272
1273 #[test]
1274 fn scroll_state_set_clamps_negative_to_zero_and_overshoot_to_max() {
1275 let max = r(0.0, 0.0, 100.0, 200.0);
1276 let mut st = ScrollState::default();
1277
1278 st.set(-1.0, -f32::MAX, &max);
1279 assert_eq!(st.get().x, 0.0);
1280 assert_eq!(st.get().y, 0.0);
1281
1282 st.set(f32::MAX, f32::MAX, &max);
1283 assert_eq!(st.get().x, 100.0);
1284 assert_eq!(st.get().y, 200.0);
1285
1286 st.set(f32::INFINITY, f32::INFINITY, &max);
1287 assert_eq!(st.get().x, 100.0);
1288 assert_eq!(st.get().y, 200.0);
1289
1290 st.set(f32::NEG_INFINITY, f32::NEG_INFINITY, &max);
1291 assert_eq!(st.get().x, 0.0);
1292 assert_eq!(st.get().y, 0.0);
1293 }
1294
1295 #[test]
1296 fn scroll_state_set_nan_position_collapses_to_zero() {
1297 let max = r(0.0, 0.0, 100.0, 200.0);
1298 let mut st = ScrollState {
1299 scroll_position: LogicalPosition::new(40.0, 40.0),
1300 };
1301 st.set(f32::NAN, f32::NAN, &max);
1302 assert!(!st.get().x.is_nan());
1303 assert!(!st.get().y.is_nan());
1304 assert_eq!(st.get(), LogicalPosition::zero());
1305 }
1306
1307 #[test]
1308 fn scroll_state_set_nan_max_range_collapses_to_zero() {
1309 let max = r(0.0, 0.0, f32::NAN, f32::NAN);
1312 let mut st = ScrollState::default();
1313 st.set(75.0, 75.0, &max);
1314 assert_eq!(st.get(), LogicalPosition::zero());
1315 }
1316
1317 #[test]
1318 fn scroll_state_set_negative_max_range_collapses_to_zero() {
1319 let max = r(0.0, 0.0, -50.0, -50.0);
1320 let mut st = ScrollState::default();
1321 st.set(10.0, 10.0, &max);
1322 assert_eq!(st.get(), LogicalPosition::zero());
1323 }
1324
1325 #[test]
1326 fn scroll_state_add_accumulates_then_saturates_at_the_range() {
1327 let max = r(0.0, 0.0, 100.0, 200.0);
1328 let mut st = ScrollState::default();
1329 st.add(30.0, 30.0, &max);
1330 st.add(30.0, 30.0, &max);
1331 assert_eq!(st.get().x, 60.0);
1332 assert_eq!(st.get().y, 60.0);
1333
1334 st.add(f32::MAX, f32::MAX, &max);
1337 st.add(f32::MAX, f32::MAX, &max);
1338 assert_eq!(st.get().x, 100.0);
1339 assert_eq!(st.get().y, 200.0);
1340 assert!(st.get().x.is_finite() && st.get().y.is_finite());
1341 }
1342
1343 #[test]
1344 fn scroll_state_add_negative_underflow_clamps_to_zero() {
1345 let max = r(0.0, 0.0, 100.0, 200.0);
1346 let mut st = ScrollState {
1347 scroll_position: LogicalPosition::new(10.0, 10.0),
1348 };
1349 st.add(f32::MIN, f32::MIN, &max);
1350 assert_eq!(st.get(), LogicalPosition::zero());
1351
1352 st.add(f32::NEG_INFINITY, f32::NEG_INFINITY, &max);
1353 assert_eq!(st.get(), LogicalPosition::zero());
1354 assert!(st.get().x.is_finite() && st.get().y.is_finite());
1355 }
1356
1357 #[test]
1358 fn scroll_state_add_inf_minus_inf_does_not_poison_position() {
1359 let unbounded = r(0.0, 0.0, f32::INFINITY, f32::INFINITY);
1363 let mut st = ScrollState::default();
1364 st.add(f32::INFINITY, f32::INFINITY, &unbounded);
1365 assert!(st.get().x.is_infinite());
1366
1367 st.add(f32::NEG_INFINITY, f32::NEG_INFINITY, &unbounded);
1368 assert!(!st.get().x.is_nan());
1369 assert!(!st.get().y.is_nan());
1370 assert_eq!(st.get(), LogicalPosition::zero());
1371 }
1372
1373 #[test]
1374 fn scroll_state_add_nan_delta_from_nonzero_position_resets_to_zero() {
1375 let max = r(0.0, 0.0, 100.0, 200.0);
1376 let mut st = ScrollState {
1377 scroll_position: LogicalPosition::new(50.0, 50.0),
1378 };
1379 st.add(f32::NAN, 0.0, &max);
1380 assert_eq!(st.get().x, 0.0);
1383 assert_eq!(st.get().y, 50.0);
1384 }
1385
1386 #[test]
1391 fn scroll_states_new_is_empty_and_lookup_misses_return_none() {
1392 let states = ScrollStates::new();
1393 assert_eq!(states.0.len(), 0);
1394 assert!(states.get_scroll_position(&ext_id(0)).is_none());
1395 assert!(states.get_scroll_position(&ext_id(u64::MAX)).is_none());
1396 }
1397
1398 #[test]
1399 fn scroll_states_set_scroll_position_creates_entry_and_clamps() {
1400 let node = scroll_node_of(r(0.0, 0.0, 100.0, 100.0), r(0.0, 0.0, 100.0, 300.0));
1401 let mut states = ScrollStates::new();
1402
1403 states.set_scroll_position(&node, LogicalPosition::new(999.0, 999.0));
1404 let pos = states
1405 .get_scroll_position(&node.parent_external_scroll_id)
1406 .expect("entry must exist after set_scroll_position");
1407 assert_eq!(states.0.len(), 1);
1408 assert_eq!(pos.x, 0.0); assert_eq!(pos.y, 200.0); states.set_scroll_position(&node, LogicalPosition::new(-5.0, -5.0));
1413 assert_eq!(states.0.len(), 1);
1414 let pos = states
1415 .get_scroll_position(&node.parent_external_scroll_id)
1416 .unwrap();
1417 assert_eq!(pos, LogicalPosition::zero());
1418 }
1419
1420 #[test]
1421 fn scroll_states_set_scroll_position_with_nan_stays_defined() {
1422 let node = scroll_node_of(r(0.0, 0.0, 100.0, 100.0), r(0.0, 0.0, 100.0, 300.0));
1423 let mut states = ScrollStates::new();
1424 states.set_scroll_position(&node, LogicalPosition::new(f32::NAN, f32::NAN));
1425 let pos = states
1426 .get_scroll_position(&node.parent_external_scroll_id)
1427 .unwrap();
1428 assert!(!pos.x.is_nan() && !pos.y.is_nan());
1429 assert_eq!(pos, LogicalPosition::zero());
1430 }
1431
1432 #[test]
1433 fn scroll_states_scroll_node_accumulates_and_saturates() {
1434 let node = scroll_node_of(r(0.0, 0.0, 100.0, 100.0), r(0.0, 0.0, 400.0, 300.0));
1435 let mut states = ScrollStates::new();
1436
1437 states.scroll_node(&node, 0.0, 0.0);
1438 assert_eq!(
1439 states
1440 .get_scroll_position(&node.parent_external_scroll_id)
1441 .unwrap(),
1442 LogicalPosition::zero()
1443 );
1444
1445 states.scroll_node(&node, 10.0, 10.0);
1446 states.scroll_node(&node, 10.0, 10.0);
1447 let pos = states
1448 .get_scroll_position(&node.parent_external_scroll_id)
1449 .unwrap();
1450 assert_eq!(pos.x, 20.0);
1451 assert_eq!(pos.y, 20.0);
1452
1453 states.scroll_node(&node, f32::MAX, f32::INFINITY);
1455 let pos = states
1456 .get_scroll_position(&node.parent_external_scroll_id)
1457 .unwrap();
1458 assert_eq!(pos.x, 300.0);
1459 assert_eq!(pos.y, 200.0);
1460
1461 states.scroll_node(&node, f32::NAN, f32::NAN);
1463 let pos = states
1464 .get_scroll_position(&node.parent_external_scroll_id)
1465 .unwrap();
1466 assert!(!pos.x.is_nan() && !pos.y.is_nan());
1467 assert_eq!(states.0.len(), 1);
1468 }
1469
1470 #[test]
1471 fn scroll_states_keys_are_pipeline_qualified() {
1472 let a = OverflowingScrollNode {
1474 parent_rect: r(0.0, 0.0, 10.0, 10.0),
1475 child_rect: r(0.0, 0.0, 10.0, 100.0),
1476 parent_external_scroll_id: ExternalScrollId(1, PipelineId(1, 0)),
1477 ..Default::default()
1478 };
1479 let b = OverflowingScrollNode {
1480 parent_external_scroll_id: ExternalScrollId(1, PipelineId(2, 0)),
1481 ..a
1482 };
1483
1484 let mut states = ScrollStates::new();
1485 states.scroll_node(&a, 0.0, 25.0);
1486 states.scroll_node(&b, 0.0, 50.0);
1487 assert_eq!(states.0.len(), 2);
1488 assert_eq!(
1489 states
1490 .get_scroll_position(&a.parent_external_scroll_id)
1491 .unwrap()
1492 .y,
1493 25.0
1494 );
1495 assert_eq!(
1496 states
1497 .get_scroll_position(&b.parent_external_scroll_id)
1498 .unwrap()
1499 .y,
1500 50.0
1501 );
1502 }
1503
1504 #[test]
1509 fn scrollbar_component_from_u8_covers_all_256_values() {
1510 for v in 0u8..=255 {
1511 match ScrollbarComponent::from_u8(v) {
1512 Some(c) => {
1513 assert!(v < 4, "value {v} unexpectedly decoded to {c:?}");
1514 assert_eq!(c as u8, v);
1516 }
1517 None => assert!(v >= 4, "value {v} should have decoded"),
1518 }
1519 }
1520 for c in ALL_SCROLLBAR_COMPONENTS {
1521 assert_eq!(ScrollbarComponent::from_u8(c as u8), Some(c));
1522 }
1523 }
1524
1525 #[test]
1526 fn cursor_type_from_u8_is_total_and_unknown_falls_back_to_default() {
1527 for v in 0u8..=255 {
1528 let c = CursorType::from_u8(v);
1529 if v <= 20 {
1530 assert_eq!(c as u8, v, "known discriminant {v} must round-trip");
1531 } else {
1532 assert_eq!(c, CursorType::Default, "unknown byte {v} must be Default");
1534 }
1535 }
1536 assert_eq!(CursorType::default(), CursorType::Default);
1537 for c in ALL_CURSORS {
1538 assert_eq!(CursorType::from_u8(c as u8), c);
1539 }
1540 }
1541
1542 #[test]
1547 fn dom_node_tag_round_trips_at_u64_boundaries() {
1548 for inner in [0u64, 1, 673, u64::from(u32::MAX), u64::MAX] {
1549 let tag = HitTestTag::DomNode {
1550 tag_id: TagId { inner },
1551 };
1552 let item = tag.to_item_tag();
1553 assert_eq!(item, (inner, TAG_TYPE_DOM_NODE));
1554 assert_eq!(HitTestTag::from_item_tag(item), Some(tag));
1555 assert_eq!(tag.as_dom_node().unwrap().inner, inner);
1556 }
1557 }
1558
1559 #[test]
1560 fn scrollbar_tag_round_trips_for_every_component_at_field_boundaries() {
1561 let ids = [
1562 (0usize, 0usize),
1563 (0, u32::MAX as usize),
1564 (u32::MAX as usize, 0),
1565 (u32::MAX as usize, u32::MAX as usize),
1566 ];
1567 for component in ALL_SCROLLBAR_COMPONENTS {
1568 for (dom, node) in ids {
1569 let tag = HitTestTag::Scrollbar {
1570 dom_id: DomId { inner: dom },
1571 node_id: NodeId::new(node),
1572 component,
1573 };
1574 let item = tag.to_item_tag();
1575 assert_eq!(item.1 & 0xFF00, TAG_TYPE_SCROLLBAR);
1576 assert_eq!(
1577 HitTestTag::from_item_tag(item),
1578 Some(tag),
1579 "scrollbar round-trip failed for dom={dom} node={node} {component:?}"
1580 );
1581 }
1582 }
1583 }
1584
1585 #[test]
1586 fn cursor_tag_round_trips_for_every_cursor_type() {
1587 for cursor_type in ALL_CURSORS {
1588 let tag = HitTestTag::Cursor {
1589 dom_id: DomId { inner: u32::MAX as usize },
1590 node_id: NodeId::new(u32::MAX as usize),
1591 cursor_type,
1592 };
1593 let item = tag.to_item_tag();
1594 assert_eq!(item.1 & 0xFF00, TAG_TYPE_CURSOR);
1595 assert_eq!(
1596 HitTestTag::from_item_tag(item),
1597 Some(tag),
1598 "cursor round-trip failed for {cursor_type:?}"
1599 );
1600 }
1601 }
1602
1603 #[test]
1604 fn selection_tag_round_trips_at_every_field_boundary() {
1605 let cases = [
1606 (0usize, 0usize, 0u16),
1607 (0xFFFF, u32::MAX as usize, u16::MAX),
1608 (1, 1, 1),
1609 (0xFFFF, 0, u16::MAX),
1610 ];
1611 for (dom, node, run) in cases {
1612 let tag = HitTestTag::Selection {
1613 dom_id: DomId { inner: dom },
1614 container_node_id: NodeId::new(node),
1615 text_run_index: run,
1616 };
1617 let item = tag.to_item_tag();
1618 assert_eq!(item.1, TAG_TYPE_SELECTION);
1619 assert_eq!(
1620 HitTestTag::from_item_tag(item),
1621 Some(tag),
1622 "selection round-trip failed for dom={dom} node={node} run={run}"
1623 );
1624 }
1625 }
1626
1627 #[test]
1628 fn selection_tag_oversized_node_id_is_masked_not_bled_into_dom_id() {
1629 let tag = HitTestTag::Selection {
1633 dom_id: DomId { inner: 0x00AB },
1634 container_node_id: NodeId::new(u32::MAX as usize),
1635 text_run_index: 0xBEEF,
1636 };
1637 let (value, _) = tag.to_item_tag();
1638 assert_eq!(value >> 48, 0x00AB);
1639 assert_eq!((value >> 16) & 0xFFFF_FFFF, u64::from(u32::MAX));
1640 assert_eq!(value & 0xFFFF, 0xBEEF);
1641 }
1642
1643 #[test]
1644 fn tag_namespaces_do_not_collide_on_identical_payloads() {
1645 let payload = 0x0000_0001_0000_0002u64;
1648 let decoded: [HitTestTag; 4] = [
1649 HitTestTag::from_item_tag((payload, TAG_TYPE_DOM_NODE)).unwrap(),
1650 HitTestTag::from_item_tag((payload, TAG_TYPE_SCROLLBAR)).unwrap(),
1651 HitTestTag::from_item_tag((payload, TAG_TYPE_SELECTION)).unwrap(),
1652 HitTestTag::from_item_tag((payload, TAG_TYPE_CURSOR)).unwrap(),
1653 ];
1654 assert!(decoded[0].is_dom_node());
1655 assert!(decoded[1].is_scrollbar());
1656 assert!(decoded[2].is_selection());
1657 assert!(decoded[3].is_cursor());
1658 for (i, a) in decoded.iter().enumerate() {
1659 for b in decoded.iter().skip(i + 1) {
1660 assert_ne!(a, b);
1661 }
1662 }
1663 }
1664
1665 #[test]
1670 fn from_item_tag_sweeps_every_type_marker_without_panicking() {
1671 for hi in 0u16..=0xFF {
1674 for lo in [0u16, 1, 3, 4, 20, 21, 0xFF] {
1675 let tag_type = (hi << 8) | lo;
1676 let decoded = HitTestTag::from_item_tag((0xDEAD_BEEF_CAFE_BABE, tag_type));
1677 let expected_some = match hi {
1678 0x00 => tag_type == 0, 0x01 | 0x03 | 0x04 => true, 0x02 => lo < 4, _ => false, };
1683 assert_eq!(
1684 decoded.is_some(),
1685 expected_some,
1686 "tag_type {tag_type:#06x} decoded to {decoded:?}"
1687 );
1688 }
1689 }
1690 }
1691
1692 #[test]
1693 fn from_item_tag_rejects_invalid_scrollbar_components() {
1694 for lo in 4u16..=0xFF {
1695 let item = (0u64, TAG_TYPE_SCROLLBAR | lo);
1696 assert!(
1697 HitTestTag::from_item_tag(item).is_none(),
1698 "scrollbar component byte {lo} should be rejected"
1699 );
1700 }
1701 }
1702
1703 #[test]
1704 fn from_item_tag_unknown_cursor_byte_degrades_to_default() {
1705 for lo in [21u16, 100, 0xFF] {
1708 let decoded = HitTestTag::from_item_tag((0, TAG_TYPE_CURSOR | lo)).unwrap();
1709 assert_eq!(
1710 decoded.as_cursor().unwrap().2,
1711 CursorType::Default,
1712 "cursor byte {lo} should fall back to Default"
1713 );
1714 }
1715 }
1716
1717 #[test]
1718 fn from_item_tag_dom_node_ignores_the_lower_byte() {
1719 for lo in [0u16, 1, 0x7F, 0xFF] {
1722 let decoded = HitTestTag::from_item_tag((99, TAG_TYPE_DOM_NODE | lo)).unwrap();
1723 assert!(decoded.is_dom_node());
1724 assert_eq!(decoded.as_dom_node().unwrap().inner, 99);
1725 }
1726 }
1727
1728 #[test]
1729 fn from_item_tag_scroll_container_marker_is_not_decodable() {
1730 assert!(HitTestTag::from_item_tag((0, TAG_TYPE_SCROLL_CONTAINER)).is_none());
1733 assert!(HitTestTag::from_item_tag((u64::MAX, TAG_TYPE_SCROLL_CONTAINER)).is_none());
1734 }
1735
1736 #[test]
1737 fn from_item_tag_legacy_zero_type_only_matches_exact_zero() {
1738 let legacy = HitTestTag::from_item_tag((u64::MAX, 0)).unwrap();
1740 assert_eq!(legacy.as_dom_node().unwrap().inner, u64::MAX);
1741 for lo in 1u16..=0xFF {
1744 assert!(
1745 HitTestTag::from_item_tag((0, lo)).is_none(),
1746 "tag_type {lo:#06x} must not be treated as a legacy DOM tag"
1747 );
1748 }
1749 }
1750
1751 #[cfg(target_pointer_width = "64")]
1752 #[test]
1753 fn scrollbar_encode_keeps_decoded_fields_inside_their_bit_windows() {
1754 let tag = HitTestTag::Scrollbar {
1759 dom_id: DomId { inner: 0 },
1760 node_id: NodeId::new(1usize << 32),
1761 component: ScrollbarComponent::VerticalTrack,
1762 };
1763 let (value, ty) = tag.to_item_tag();
1764 assert_eq!(ty & 0xFF00, TAG_TYPE_SCROLLBAR);
1765
1766 let decoded = HitTestTag::from_item_tag((value, ty)).expect("must still decode");
1767 let (dom, node, component) = decoded.as_scrollbar().unwrap();
1768 assert!(dom.inner <= u32::MAX as usize);
1769 assert!(node.index() <= u32::MAX as usize);
1770 assert_eq!(component, ScrollbarComponent::VerticalTrack);
1771 }
1772
1773 fn sample_tags() -> [HitTestTag; 4] {
1778 [
1779 HitTestTag::DomNode {
1780 tag_id: TagId { inner: 7 },
1781 },
1782 HitTestTag::Scrollbar {
1783 dom_id: DomId { inner: 1 },
1784 node_id: NodeId::new(2),
1785 component: ScrollbarComponent::HorizontalThumb,
1786 },
1787 HitTestTag::Cursor {
1788 dom_id: DomId { inner: 3 },
1789 node_id: NodeId::new(4),
1790 cursor_type: CursorType::Grabbing,
1791 },
1792 HitTestTag::Selection {
1793 dom_id: DomId { inner: 5 },
1794 container_node_id: NodeId::new(6),
1795 text_run_index: 8,
1796 },
1797 ]
1798 }
1799
1800 #[test]
1801 fn exactly_one_predicate_is_true_per_variant() {
1802 for tag in sample_tags() {
1803 let flags = [
1804 tag.is_dom_node(),
1805 tag.is_scrollbar(),
1806 tag.is_cursor(),
1807 tag.is_selection(),
1808 ];
1809 assert_eq!(
1810 flags.iter().filter(|b| **b).count(),
1811 1,
1812 "predicates not mutually exclusive for {tag:?}"
1813 );
1814 }
1815 }
1816
1817 #[test]
1818 fn accessors_agree_with_predicates_and_return_none_otherwise() {
1819 for tag in sample_tags() {
1820 assert_eq!(tag.as_dom_node().is_some(), tag.is_dom_node());
1821 assert_eq!(tag.as_scrollbar().is_some(), tag.is_scrollbar());
1822 assert_eq!(tag.as_cursor().is_some(), tag.is_cursor());
1823 assert_eq!(tag.as_selection().is_some(), tag.is_selection());
1824
1825 let some_count = usize::from(tag.as_dom_node().is_some())
1827 + usize::from(tag.as_scrollbar().is_some())
1828 + usize::from(tag.as_cursor().is_some())
1829 + usize::from(tag.as_selection().is_some());
1830 assert_eq!(some_count, 1, "accessor overlap for {tag:?}");
1831 }
1832 }
1833
1834 #[test]
1835 fn accessors_return_the_constructed_payload() {
1836 let [dom, scrollbar, cursor, selection] = sample_tags();
1837
1838 assert_eq!(dom.as_dom_node().unwrap().inner, 7);
1839
1840 let (d, n, c) = scrollbar.as_scrollbar().unwrap();
1841 assert_eq!((d.inner, n.index()), (1, 2));
1842 assert_eq!(c, ScrollbarComponent::HorizontalThumb);
1843
1844 let (d, n, c) = cursor.as_cursor().unwrap();
1845 assert_eq!((d.inner, n.index()), (3, 4));
1846 assert_eq!(c, CursorType::Grabbing);
1847
1848 let (d, n, run) = selection.as_selection().unwrap();
1849 assert_eq!((d.inner, n.index()), (5, 6));
1850 assert_eq!(run, 8);
1851 }
1852
1853 #[test]
1858 fn hit_test_tag_display_is_non_empty_and_variant_tagged() {
1859 let [dom, scrollbar, cursor, selection] = sample_tags();
1860 for (tag, prefix) in [
1861 (dom, "DomNode("),
1862 (scrollbar, "Scrollbar("),
1863 (cursor, "Cursor("),
1864 (selection, "Selection("),
1865 ] {
1866 let shown = alloc::format!("{tag}");
1867 assert!(!shown.is_empty());
1868 assert!(
1869 shown.starts_with(prefix),
1870 "expected {shown:?} to start with {prefix:?}"
1871 );
1872 }
1873 }
1874
1875 #[test]
1876 fn hit_test_tag_display_handles_extreme_ids() {
1877 let tags = [
1878 HitTestTag::DomNode {
1879 tag_id: TagId { inner: u64::MAX },
1880 },
1881 HitTestTag::Scrollbar {
1882 dom_id: DomId { inner: usize::MAX },
1883 node_id: NodeId::new(usize::MAX),
1884 component: ScrollbarComponent::VerticalThumb,
1885 },
1886 HitTestTag::Cursor {
1887 dom_id: DomId { inner: usize::MAX },
1888 node_id: NodeId::new(usize::MAX),
1889 cursor_type: CursorType::Progress,
1890 },
1891 HitTestTag::Selection {
1892 dom_id: DomId { inner: usize::MAX },
1893 container_node_id: NodeId::new(usize::MAX),
1894 text_run_index: u16::MAX,
1895 },
1896 ];
1897 for tag in tags {
1898 assert!(!alloc::format!("{tag}").is_empty());
1899 assert!(!alloc::format!("{tag:?}").is_empty());
1900 let _ = tag.to_item_tag();
1902 }
1903 }
1904}