1use alloc::vec::Vec;
14
15use crate::dom::{DomId, DomNodeId, NodeId, OptionDomNodeId};
16use crate::geom::LogicalPosition;
17use crate::selection::TextCursor;
18use crate::window::WindowPosition;
19
20use azul_css::{AzString, StringVec, U8Vec};
21
22#[derive(Debug, Clone, PartialEq)]
27#[repr(C, u8)]
28pub enum ActiveDragType {
29 TextSelection(TextSelectionDrag),
31 ScrollbarThumb(ScrollbarThumbDrag),
33 Node(NodeDrag),
35 WindowMove(WindowMoveDrag),
37 WindowResize(WindowResizeDrag),
39 FileDrop(FileDropDrag),
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[repr(C)]
48pub struct TextSelectionDrag {
49 pub dom_id: DomId,
51 pub anchor_ifc_node: NodeId,
53 pub anchor_cursor: Option<TextCursor>,
55 pub start_mouse_position: LogicalPosition,
57 pub current_mouse_position: LogicalPosition,
59 pub auto_scroll_direction: AutoScrollDirection,
61 pub auto_scroll_container: Option<NodeId>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
69#[repr(C)]
70pub struct ScrollbarThumbDrag {
71 pub dom_id: DomId,
75 pub scroll_container_node: NodeId,
77 pub axis: ScrollbarAxis,
79 pub start_mouse_position: LogicalPosition,
81 pub start_scroll_offset: f32,
83 pub current_mouse_position: LogicalPosition,
85 pub track_length_px: f32,
87 pub content_length_px: f32,
89 pub viewport_length_px: f32,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95#[repr(C)]
96pub enum ScrollbarAxis {
97 Vertical,
98 Horizontal,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
105#[repr(C)]
106pub struct NodeDrag {
107 pub dom_id: DomId,
109 pub node_id: NodeId,
111 pub start_position: LogicalPosition,
113 pub current_position: LogicalPosition,
115 pub drag_offset: LogicalPosition,
117 pub current_drop_target: OptionDomNodeId,
119 pub previous_drop_target: OptionDomNodeId,
121 pub drag_data: DragData,
123 pub drop_accepted: bool,
125 pub drop_effect: DropEffect,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133#[repr(C)]
134pub struct WindowMoveDrag {
135 pub start_position: LogicalPosition,
137 pub current_position: LogicalPosition,
139 pub initial_window_position: WindowPosition,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[repr(C)]
148pub struct WindowResizeDrag {
149 pub edge: WindowResizeEdge,
151 pub start_position: LogicalPosition,
153 pub current_position: LogicalPosition,
155 pub initial_width: u32,
157 pub initial_height: u32,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[repr(C)]
164pub enum WindowResizeEdge {
165 Top,
166 Bottom,
167 Left,
168 Right,
169 TopLeft,
170 TopRight,
171 BottomLeft,
172 BottomRight,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
179#[repr(C)]
180pub struct FileDropDrag {
181 pub files: StringVec,
183 pub position: LogicalPosition,
185 pub drop_target: OptionDomNodeId,
187 pub drop_effect: DropEffect,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
193#[repr(C)]
194pub enum AutoScrollDirection {
195 #[default]
196 None,
197 Up,
198 Down,
199 Left,
200 Right,
201 UpLeft,
202 UpRight,
203 DownLeft,
204 DownRight,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
214#[repr(C)]
215pub enum DropEffect {
216 #[default]
218 None,
219 Copy,
221 Link,
223 Move,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
235#[repr(C)]
236pub enum DragEffect {
237 #[default]
240 Uninitialized,
241 None,
243 Copy,
245 CopyLink,
247 CopyMove,
249 Link,
251 LinkMove,
253 Move,
255 All,
257}
258
259#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
262#[repr(C)]
263pub struct MimeTypeData {
264 pub mime_type: AzString,
265 pub data: U8Vec,
266}
267
268impl_option!(
269 MimeTypeData,
270 OptionMimeTypeData,
271 copy = false,
272 [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
273);
274
275impl_vec!(
276 MimeTypeData,
277 MimeTypeDataVec,
278 MimeTypeDataVecDestructor,
279 MimeTypeDataVecDestructorType,
280 MimeTypeDataVecSlice,
281 OptionMimeTypeData
282);
283impl_vec_mut!(MimeTypeData, MimeTypeDataVec);
284impl_vec_debug!(MimeTypeData, MimeTypeDataVec);
285impl_vec_partialord!(MimeTypeData, MimeTypeDataVec);
286impl_vec_ord!(MimeTypeData, MimeTypeDataVec);
287impl_vec_clone!(MimeTypeData, MimeTypeDataVec, MimeTypeDataVecDestructor);
288impl_vec_partialeq!(MimeTypeData, MimeTypeDataVec);
289impl_vec_eq!(MimeTypeData, MimeTypeDataVec);
290impl_vec_hash!(MimeTypeData, MimeTypeDataVec);
291
292#[derive(Debug, Default, Clone, PartialEq, Eq)]
297#[repr(C)]
298pub struct DragData {
299 pub data: MimeTypeDataVec,
303 pub effect_allowed: DragEffect,
305}
306
307impl DragData {
308 #[must_use] pub const fn new() -> Self {
310 Self {
311 data: MimeTypeDataVec::new(),
312 effect_allowed: DragEffect::Uninitialized,
313 }
314 }
315
316 pub fn set_data(&mut self, mime_type: impl Into<AzString>, data: Vec<u8>) {
319 let mime_type = mime_type.into();
320 let value: U8Vec = data.into();
321 if let Some(entry) = self
322 .data
323 .as_mut()
324 .iter_mut()
325 .find(|e| e.mime_type == mime_type)
326 {
327 entry.data = value;
328 } else {
329 self.data.push(MimeTypeData {
330 mime_type,
331 data: value,
332 });
333 }
334 }
335
336 #[must_use] pub fn get_data(&self, mime_type: &str) -> Option<&[u8]> {
338 self.data
339 .as_ref()
340 .iter()
341 .find(|e| e.mime_type.as_str() == mime_type)
342 .map(|e| e.data.as_ref())
343 }
344
345 pub fn set_text(&mut self, text: impl Into<AzString>) {
347 let text_str = text.into();
348 self.set_data("text/plain", text_str.as_str().as_bytes().to_vec());
349 }
350
351 #[must_use] pub fn get_text(&self) -> Option<AzString> {
353 self.get_data("text/plain")
354 .map(|bytes| AzString::from(core::str::from_utf8(bytes).unwrap_or("")))
355 }
356}
357
358#[derive(Debug, Clone, PartialEq)]
365pub struct DragContext {
366 pub drag_type: ActiveDragType,
368 pub session_id: u64,
370 pub cancelled: bool,
372}
373
374impl DragContext {
375 #[must_use] pub const fn new(drag_type: ActiveDragType, session_id: u64) -> Self {
377 Self {
378 drag_type,
379 session_id,
380 cancelled: false,
381 }
382 }
383
384 #[must_use] pub const fn text_selection(
386 dom_id: DomId,
387 anchor_ifc_node: NodeId,
388 start_mouse_position: LogicalPosition,
389 session_id: u64,
390 ) -> Self {
391 Self::new(
392 ActiveDragType::TextSelection(TextSelectionDrag {
393 dom_id,
394 anchor_ifc_node,
395 anchor_cursor: None,
396 start_mouse_position,
397 current_mouse_position: start_mouse_position,
398 auto_scroll_direction: AutoScrollDirection::None,
399 auto_scroll_container: None,
400 }),
401 session_id,
402 )
403 }
404
405 #[must_use] pub const fn scrollbar_thumb(
407 dom_id: DomId,
408 scroll_container_node: NodeId,
409 axis: ScrollbarAxis,
410 start_mouse_position: LogicalPosition,
411 start_scroll_offset: f32,
412 track_length_px: f32,
413 content_length_px: f32,
414 viewport_length_px: f32,
415 session_id: u64,
416 ) -> Self {
417 Self::new(
418 ActiveDragType::ScrollbarThumb(ScrollbarThumbDrag {
419 dom_id,
420 scroll_container_node,
421 axis,
422 start_mouse_position,
423 start_scroll_offset,
424 current_mouse_position: start_mouse_position,
425 track_length_px,
426 content_length_px,
427 viewport_length_px,
428 }),
429 session_id,
430 )
431 }
432
433 #[must_use] pub const fn node_drag(
435 dom_id: DomId,
436 node_id: NodeId,
437 start_position: LogicalPosition,
438 drag_data: DragData,
439 session_id: u64,
440 ) -> Self {
441 Self::new(
442 ActiveDragType::Node(NodeDrag {
443 dom_id,
444 node_id,
445 start_position,
446 current_position: start_position,
447 drag_offset: LogicalPosition::zero(),
448 current_drop_target: OptionDomNodeId::None,
449 previous_drop_target: OptionDomNodeId::None,
450 drag_data,
451 drop_accepted: false,
452 drop_effect: DropEffect::None,
453 }),
454 session_id,
455 )
456 }
457
458 #[must_use] pub const fn window_move(
460 start_position: LogicalPosition,
461 initial_window_position: WindowPosition,
462 session_id: u64,
463 ) -> Self {
464 Self::new(
465 ActiveDragType::WindowMove(WindowMoveDrag {
466 start_position,
467 current_position: start_position,
468 initial_window_position,
469 }),
470 session_id,
471 )
472 }
473
474 #[must_use] pub fn file_drop(files: Vec<AzString>, position: LogicalPosition, session_id: u64) -> Self {
476 Self::new(
477 ActiveDragType::FileDrop(FileDropDrag {
478 files: files.into(),
479 position,
480 drop_target: OptionDomNodeId::None,
481 drop_effect: DropEffect::Copy,
482 }),
483 session_id,
484 )
485 }
486
487 pub const fn update_position(&mut self, position: LogicalPosition) {
489 match &mut self.drag_type {
490 ActiveDragType::TextSelection(ref mut drag) => {
491 drag.current_mouse_position = position;
492 }
493 ActiveDragType::ScrollbarThumb(ref mut drag) => {
494 drag.current_mouse_position = position;
495 }
496 ActiveDragType::Node(ref mut drag) => {
497 drag.current_position = position;
498 }
499 ActiveDragType::WindowMove(ref mut drag) => {
500 drag.current_position = position;
501 }
502 ActiveDragType::WindowResize(ref mut drag) => {
503 drag.current_position = position;
504 }
505 ActiveDragType::FileDrop(ref mut drag) => {
506 drag.position = position;
507 }
508 }
509 }
510
511 #[must_use] pub const fn current_position(&self) -> LogicalPosition {
513 match &self.drag_type {
514 ActiveDragType::TextSelection(drag) => drag.current_mouse_position,
515 ActiveDragType::ScrollbarThumb(drag) => drag.current_mouse_position,
516 ActiveDragType::Node(drag) => drag.current_position,
517 ActiveDragType::WindowMove(drag) => drag.current_position,
518 ActiveDragType::WindowResize(drag) => drag.current_position,
519 ActiveDragType::FileDrop(drag) => drag.position,
520 }
521 }
522
523 #[must_use] pub const fn start_position(&self) -> LogicalPosition {
525 match &self.drag_type {
526 ActiveDragType::TextSelection(drag) => drag.start_mouse_position,
527 ActiveDragType::ScrollbarThumb(drag) => drag.start_mouse_position,
528 ActiveDragType::Node(drag) => drag.start_position,
529 ActiveDragType::WindowMove(drag) => drag.start_position,
530 ActiveDragType::WindowResize(drag) => drag.start_position,
531 ActiveDragType::FileDrop(drag) => drag.position, }
533 }
534
535 #[must_use] pub const fn is_text_selection(&self) -> bool {
537 matches!(self.drag_type, ActiveDragType::TextSelection(_))
538 }
539
540 #[must_use] pub const fn is_scrollbar_thumb(&self) -> bool {
542 matches!(self.drag_type, ActiveDragType::ScrollbarThumb(_))
543 }
544
545 #[must_use] pub const fn is_node_drag(&self) -> bool {
547 matches!(self.drag_type, ActiveDragType::Node(_))
548 }
549
550 #[must_use] pub const fn is_window_move(&self) -> bool {
552 matches!(self.drag_type, ActiveDragType::WindowMove(_))
553 }
554
555 #[must_use] pub const fn is_file_drop(&self) -> bool {
557 matches!(self.drag_type, ActiveDragType::FileDrop(_))
558 }
559
560 #[must_use] pub const fn as_text_selection(&self) -> Option<&TextSelectionDrag> {
562 match &self.drag_type {
563 ActiveDragType::TextSelection(drag) => Some(drag),
564 _ => None,
565 }
566 }
567
568 pub const fn as_text_selection_mut(&mut self) -> Option<&mut TextSelectionDrag> {
570 match &mut self.drag_type {
571 ActiveDragType::TextSelection(drag) => Some(drag),
572 _ => None,
573 }
574 }
575
576 #[must_use] pub const fn as_scrollbar_thumb(&self) -> Option<&ScrollbarThumbDrag> {
578 match &self.drag_type {
579 ActiveDragType::ScrollbarThumb(drag) => Some(drag),
580 _ => None,
581 }
582 }
583
584 pub const fn as_scrollbar_thumb_mut(&mut self) -> Option<&mut ScrollbarThumbDrag> {
586 match &mut self.drag_type {
587 ActiveDragType::ScrollbarThumb(drag) => Some(drag),
588 _ => None,
589 }
590 }
591
592 #[must_use] pub const fn as_node_drag(&self) -> Option<&NodeDrag> {
594 match &self.drag_type {
595 ActiveDragType::Node(drag) => Some(drag),
596 _ => None,
597 }
598 }
599
600 pub const fn as_node_drag_mut(&mut self) -> Option<&mut NodeDrag> {
602 match &mut self.drag_type {
603 ActiveDragType::Node(drag) => Some(drag),
604 _ => None,
605 }
606 }
607
608 #[must_use] pub const fn as_window_move(&self) -> Option<&WindowMoveDrag> {
610 match &self.drag_type {
611 ActiveDragType::WindowMove(drag) => Some(drag),
612 _ => None,
613 }
614 }
615
616 #[must_use] pub const fn as_file_drop(&self) -> Option<&FileDropDrag> {
618 match &self.drag_type {
619 ActiveDragType::FileDrop(drag) => Some(drag),
620 _ => None,
621 }
622 }
623
624 pub const fn as_file_drop_mut(&mut self) -> Option<&mut FileDropDrag> {
626 match &mut self.drag_type {
627 ActiveDragType::FileDrop(drag) => Some(drag),
628 _ => None,
629 }
630 }
631
632 #[must_use] pub fn calculate_scrollbar_scroll_offset(&self) -> Option<f32> {
636 let drag = self.as_scrollbar_thumb()?;
637
638 let mouse_delta = match drag.axis {
640 ScrollbarAxis::Vertical => {
641 drag.current_mouse_position.y - drag.start_mouse_position.y
642 }
643 ScrollbarAxis::Horizontal => {
644 drag.current_mouse_position.x - drag.start_mouse_position.x
645 }
646 };
647
648 let scrollable_range = drag.content_length_px - drag.viewport_length_px;
650 if scrollable_range <= 0.0 || scrollable_range.is_nan() || drag.track_length_px <= 0.0 {
655 return Some(drag.start_scroll_offset);
656 }
657
658 let thumb_length = (drag.viewport_length_px / drag.content_length_px) * drag.track_length_px;
660 let scrollable_track = drag.track_length_px - thumb_length;
661
662 if scrollable_track <= 0.0 {
663 return Some(drag.start_scroll_offset);
664 }
665
666 let scroll_ratio = mouse_delta / scrollable_track;
668 let scroll_delta = scroll_ratio * scrollable_range;
669
670 let new_offset = drag.start_scroll_offset + scroll_delta;
672
673 Some(new_offset.clamp(0.0, scrollable_range))
675 }
676
677 fn remap_drop_target(
680 target: &mut OptionDomNodeId,
681 dom_id: DomId,
682 node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
683 ) {
684 let dt = match target.into_option() {
685 Some(dt) if dt.dom == dom_id => dt,
686 _ => return,
687 };
688 let Some(old_nid) = dt.node.into_crate_internal() else {
689 return;
690 };
691 if let Some(&new_nid) = node_id_map.get(&old_nid) {
692 *target = Some(DomNodeId {
693 dom: dom_id,
694 node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_nid)),
695 }).into();
696 } else {
697 *target = OptionDomNodeId::None;
698 }
699 }
700
701 pub fn remap_node_ids(
707 &mut self,
708 dom_id: DomId,
709 node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
710 ) -> bool {
711 match &mut self.drag_type {
712 ActiveDragType::TextSelection(ref mut drag) => {
713 if drag.dom_id != dom_id {
714 return true;
715 }
716 if let Some(&new_id) = node_id_map.get(&drag.anchor_ifc_node) {
717 drag.anchor_ifc_node = new_id;
718 } else {
719 return false; }
721 if let Some(ref mut container) = drag.auto_scroll_container {
722 if let Some(&new_id) = node_id_map.get(container) {
723 *container = new_id;
724 } else {
725 drag.auto_scroll_container = None;
726 }
727 }
728 true
729 }
730 ActiveDragType::ScrollbarThumb(ref mut drag) => {
731 if drag.dom_id != dom_id {
734 return true;
735 }
736 if let Some(&new_id) = node_id_map.get(&drag.scroll_container_node) {
737 drag.scroll_container_node = new_id;
738 true
739 } else {
740 false }
742 }
743 ActiveDragType::Node(ref mut drag) => {
744 if drag.dom_id != dom_id {
745 return true;
746 }
747 if let Some(&new_id) = node_id_map.get(&drag.node_id) {
748 drag.node_id = new_id;
749 } else {
750 return false; }
752 Self::remap_drop_target(&mut drag.current_drop_target, dom_id, node_id_map);
757 Self::remap_drop_target(&mut drag.previous_drop_target, dom_id, node_id_map);
758 true
759 }
760 ActiveDragType::WindowMove(_) | ActiveDragType::WindowResize(_) => true,
762 ActiveDragType::FileDrop(ref mut drag) => {
763 Self::remap_drop_target(&mut drag.drop_target, dom_id, node_id_map);
764 true
765 }
766 }
767 }
768}
769
770azul_css::impl_option!(
771 DragContext,
772 OptionDragContext,
773 copy = false,
774 [Debug, Clone, PartialEq]
775);
776
777
778#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
781#[repr(C)]
782pub struct DragDelta {
783 pub dx: f32,
784 pub dy: f32,
785}
786
787impl DragDelta {
788 #[inline]
789 #[must_use] pub const fn new(dx: f32, dy: f32) -> Self {
790 Self { dx, dy }
791 }
792 #[inline]
793 #[must_use] pub const fn zero() -> Self {
794 Self::new(0.0, 0.0)
795 }
796}
797
798impl_option!(
799 DragDelta,
800 OptionDragDelta,
801 [Debug, Copy, Clone, PartialEq, PartialOrd]
802);
803
804#[cfg(test)]
805mod audit_tests {
806 use super::*;
807 use crate::styled_dom::NodeHierarchyItemId;
808
809 fn node_map(from: usize, to: usize) -> alloc::collections::BTreeMap<NodeId, NodeId> {
810 let mut m = alloc::collections::BTreeMap::new();
811 m.insert(NodeId::new(from), NodeId::new(to));
812 m
813 }
814
815 fn dnid(dom: usize, node: usize) -> DomNodeId {
816 DomNodeId {
817 dom: DomId { inner: dom },
818 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
819 }
820 }
821
822 #[test]
823 fn scrollbar_remap_scoped_to_dom() {
824 let mut ctx = DragContext::scrollbar_thumb(
825 DomId { inner: 0 },
826 NodeId::new(3),
827 ScrollbarAxis::Vertical,
828 LogicalPosition::zero(),
829 0.0, 100.0, 300.0, 100.0,
830 1,
831 );
832 let ok = ctx.remap_node_ids(DomId { inner: 1 }, &node_map(3, 99));
834 assert!(ok);
835 assert_eq!(ctx.as_scrollbar_thumb().unwrap().scroll_container_node, NodeId::new(3));
836
837 let ok2 = ctx.remap_node_ids(DomId { inner: 0 }, &node_map(3, 99));
839 assert!(ok2);
840 assert_eq!(ctx.as_scrollbar_thumb().unwrap().scroll_container_node, NodeId::new(99));
841 }
842
843 #[test]
844 fn node_drag_remaps_previous_drop_target() {
845 let mut ctx = DragContext::node_drag(
846 DomId { inner: 0 },
847 NodeId::new(1),
848 LogicalPosition::zero(),
849 DragData::new(),
850 2,
851 );
852 {
853 let nd = ctx.as_node_drag_mut().unwrap();
854 nd.current_drop_target = Some(dnid(0, 5)).into();
855 nd.previous_drop_target = Some(dnid(0, 6)).into();
856 }
857 let mut m = alloc::collections::BTreeMap::new();
859 m.insert(NodeId::new(1), NodeId::new(1));
860 m.insert(NodeId::new(5), NodeId::new(50));
861 m.insert(NodeId::new(6), NodeId::new(60));
862 assert!(ctx.remap_node_ids(DomId { inner: 0 }, &m));
863
864 let nd = ctx.as_node_drag().unwrap();
865 let cur = nd.current_drop_target.into_option().unwrap().node.into_crate_internal().unwrap();
866 let prev = nd.previous_drop_target.into_option().unwrap().node.into_crate_internal().unwrap();
867 assert_eq!(cur, NodeId::new(50));
868 assert_eq!(prev, NodeId::new(60)); }
870}
871
872#[cfg(test)]
873mod autotest_generated {
874 use alloc::collections::BTreeMap;
875 use alloc::string::{String, ToString};
876
877 use super::*;
878 use crate::geom::PhysicalPosition;
879 use crate::styled_dom::NodeHierarchyItemId;
880
881 fn dom(i: usize) -> DomId {
884 DomId { inner: i }
885 }
886
887 fn dnid(dom_idx: usize, node: usize) -> DomNodeId {
888 DomNodeId {
889 dom: dom(dom_idx),
890 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
891 }
892 }
893
894 fn dnid_no_node(dom_idx: usize) -> DomNodeId {
897 DomNodeId {
898 dom: dom(dom_idx),
899 node: NodeHierarchyItemId::from_crate_internal(None),
900 }
901 }
902
903 fn nid_map(pairs: &[(usize, usize)]) -> BTreeMap<NodeId, NodeId> {
904 let mut m = BTreeMap::new();
905 for (from, to) in pairs {
906 m.insert(NodeId::new(*from), NodeId::new(*to));
907 }
908 m
909 }
910
911 fn vscroll(
913 start_scroll_offset: f32,
914 track_length_px: f32,
915 content_length_px: f32,
916 viewport_length_px: f32,
917 ) -> DragContext {
918 DragContext::scrollbar_thumb(
919 dom(0),
920 NodeId::new(1),
921 ScrollbarAxis::Vertical,
922 LogicalPosition::zero(),
923 start_scroll_offset,
924 track_length_px,
925 content_length_px,
926 viewport_length_px,
927 7,
928 )
929 }
930
931 fn resize_ctx() -> DragContext {
932 DragContext::new(
933 ActiveDragType::WindowResize(WindowResizeDrag {
934 edge: WindowResizeEdge::BottomRight,
935 start_position: LogicalPosition::new(1.0, 2.0),
936 current_position: LogicalPosition::new(1.0, 2.0),
937 initial_width: u32::MAX,
938 initial_height: 0,
939 }),
940 u64::MAX,
941 )
942 }
943
944 #[test]
948 fn dragdata_new_is_empty_and_matches_default() {
949 let d = DragData::new();
950 assert_eq!(d.data.len(), 0);
951 assert!(d.data.is_empty());
952 assert_eq!(d.effect_allowed, DragEffect::Uninitialized);
953 assert_eq!(d, DragData::default());
954 assert!(d.get_data("text/plain").is_none());
955 assert!(d.get_text().is_none());
956 }
957
958 #[test]
959 fn get_data_valid_minimal_positive_control() {
960 let mut d = DragData::new();
961 d.set_data("text/plain", b"hi".to_vec());
962 assert_eq!(d.get_data("text/plain"), Some(&b"hi"[..]));
963 }
964
965 #[test]
966 fn get_data_empty_key_on_empty_and_populated_returns_none() {
967 let empty = DragData::new();
968 assert!(empty.get_data("").is_none());
969
970 let mut d = DragData::new();
971 d.set_data("text/plain", b"x".to_vec());
972 assert!(d.get_data("").is_none());
973 }
974
975 #[test]
976 fn get_data_empty_key_is_a_real_key_when_stored() {
977 let mut d = DragData::new();
979 d.set_data("", b"empty-key".to_vec());
980 assert_eq!(d.get_data(""), Some(&b"empty-key"[..]));
981 assert!(d.get_data("text/plain").is_none());
982 }
983
984 #[test]
985 fn get_data_whitespace_only_keys_return_none() {
986 let mut d = DragData::new();
987 d.set_data("text/plain", b"x".to_vec());
988 for k in [" ", "\t\n", "\r\n", "\u{a0}", "\u{2028}"] {
989 assert!(d.get_data(k).is_none(), "whitespace key {k:?} matched");
990 }
991 }
992
993 #[test]
994 fn get_data_garbage_bytes_return_none_without_panicking() {
995 let mut d = DragData::new();
996 d.set_data("text/plain", b"x".to_vec());
997 for k in [
998 "\u{0}",
999 "\u{0}\u{1}\u{2}\u{7f}",
1000 "\u{feff}",
1001 "%%%;;;///",
1002 "text/plain\u{0}",
1003 "\u{0}text/plain",
1004 ] {
1005 assert!(d.get_data(k).is_none(), "garbage key {k:?} matched");
1006 }
1007 }
1008
1009 #[test]
1010 fn get_data_leading_trailing_junk_is_not_trimmed() {
1011 let mut d = DragData::new();
1012 d.set_data("text/plain", b"x".to_vec());
1013 assert!(d.get_data(" text/plain ").is_none());
1015 assert!(d.get_data("text/plain;garbage").is_none());
1016 assert!(d.get_data("text/plain ").is_none());
1017 assert!(d.get_data(" text/plain").is_none());
1018 assert_eq!(d.get_data("text/plain"), Some(&b"x"[..]));
1019 }
1020
1021 #[test]
1022 fn get_data_is_case_sensitive() {
1023 let mut d = DragData::new();
1025 d.set_data("text/plain", b"x".to_vec());
1026 assert!(d.get_data("TEXT/PLAIN").is_none());
1027 assert!(d.get_data("Text/Plain").is_none());
1028 }
1029
1030 #[test]
1031 fn get_data_boundary_number_strings_return_none() {
1032 let mut d = DragData::new();
1033 d.set_data("text/plain", b"x".to_vec());
1034 for k in [
1035 "0",
1036 "-0",
1037 "9223372036854775807", "-9223372036854775808", "18446744073709551615", "1e400",
1041 "NaN",
1042 "inf",
1043 "-inf",
1044 "1.7976931348623157e308",
1045 "5e-324",
1046 ] {
1047 assert!(d.get_data(k).is_none(), "numeric key {k:?} matched");
1048 }
1049 }
1050
1051 #[test]
1052 fn get_data_unicode_keys_round_trip() {
1053 let mut d = DragData::new();
1054 let emoji = "application/x-\u{1F600};charset=utf-8";
1055 let combining = "text/e\u{0301}"; d.set_data(emoji, b"grin".to_vec());
1057 d.set_data(combining, b"acute".to_vec());
1058
1059 assert_eq!(d.get_data(emoji), Some(&b"grin"[..]));
1060 assert_eq!(d.get_data(combining), Some(&b"acute"[..]));
1061 assert!(d.get_data("text/\u{e9}").is_none());
1063 assert_eq!(d.data.len(), 2);
1064 }
1065
1066 #[test]
1067 fn get_data_extremely_long_key_does_not_panic_or_hang() {
1068 let huge: String = "a".repeat(1_000_000);
1069 let mut d = DragData::new();
1070 d.set_data("text/plain", b"x".to_vec());
1071 assert!(d.get_data(&huge).is_none());
1073 d.set_data(huge.as_str(), b"huge-key".to_vec());
1075 assert_eq!(d.get_data(&huge), Some(&b"huge-key"[..]));
1076 let mut nearly = huge.clone();
1078 let _ = nearly.pop();
1079 nearly.push('b');
1080 assert!(d.get_data(&nearly).is_none());
1081 }
1082
1083 #[test]
1084 fn get_data_deeply_nested_brackets_do_not_stack_overflow() {
1085 let nested: String = "[".repeat(10_000);
1088 let mut d = DragData::new();
1089 assert!(d.get_data(&nested).is_none());
1090 d.set_data(nested.as_str(), b"nested".to_vec());
1091 assert_eq!(d.get_data(&nested), Some(&b"nested"[..]));
1092 }
1093
1094 #[test]
1095 fn set_data_replaces_existing_entry_for_same_mime() {
1096 let mut d = DragData::new();
1097 d.set_data("text/plain", b"first".to_vec());
1098 d.set_data("text/plain", b"second".to_vec());
1099 assert_eq!(d.data.len(), 1, "duplicate MIME key was appended");
1100 assert_eq!(d.get_data("text/plain"), Some(&b"second"[..]));
1101 }
1102
1103 #[test]
1104 fn set_data_empty_payload_is_some_empty_not_none() {
1105 let mut d = DragData::new();
1106 d.set_data("application/octet-stream", Vec::new());
1107 assert_eq!(d.get_data("application/octet-stream"), Some(&b""[..]));
1109 assert!(d.get_data("application/octet-stream").is_some());
1110 }
1111
1112 #[test]
1113 fn set_data_huge_payload_round_trips() {
1114 let mut d = DragData::new();
1115 let payload = alloc::vec![0xABu8; 1 << 20]; d.set_data("application/octet-stream", payload);
1117 let got = d.get_data("application/octet-stream").unwrap();
1118 assert_eq!(got.len(), 1 << 20);
1119 assert!(got.iter().all(|b| *b == 0xAB));
1120 }
1121
1122 #[test]
1123 fn set_data_binary_payload_is_not_utf8_validated() {
1124 let mut d = DragData::new();
1125 d.set_data("application/octet-stream", alloc::vec![0xFF, 0x00, 0xFE]);
1126 assert_eq!(d.get_data("application/octet-stream"), Some(&[0xFFu8, 0x00, 0xFE][..]));
1127 }
1128
1129 #[test]
1130 fn set_data_many_distinct_mime_types_all_retrievable() {
1131 let mut d = DragData::new();
1132 for i in 0..1_000usize {
1133 let mut key = String::from("application/x-");
1134 key.push_str(&i.to_string());
1135 d.set_data(key.as_str(), alloc::vec![(i % 251) as u8]);
1136 }
1137 assert_eq!(d.data.len(), 1_000);
1138 assert_eq!(d.get_data("application/x-0"), Some(&[0u8][..]));
1139 assert_eq!(d.get_data("application/x-999"), Some(&[(999 % 251) as u8][..]));
1140 assert!(d.get_data("application/x-1000").is_none());
1141 }
1142
1143 #[test]
1144 fn set_text_get_text_round_trip_unicode() {
1145 for s in [
1146 "hello",
1147 "",
1148 "\u{1F600}\u{1F4A9}",
1149 "e\u{0301}\u{0300}\u{0308}", "\u{200B}zero-width",
1151 "line1\nline2\r\n\ttab",
1152 "\u{0}interior nul\u{0}",
1153 ] {
1154 let mut d = DragData::new();
1155 d.set_text(s);
1156 let got = d.get_text().expect("text/plain must be present");
1157 assert_eq!(got.as_str(), s, "round-trip failed for {s:?}");
1158 }
1159 }
1160
1161 #[test]
1162 fn set_text_empty_is_some_not_none() {
1163 let mut d = DragData::new();
1164 d.set_text("");
1165 assert!(d.get_text().is_some());
1166 assert_eq!(d.get_text().unwrap().as_str(), "");
1167 assert_eq!(d.get_data("text/plain"), Some(&b""[..]));
1168 }
1169
1170 #[test]
1171 fn set_text_huge_string_round_trips() {
1172 let huge: String = "\u{1F600}".repeat(100_000); let mut d = DragData::new();
1174 d.set_text(huge.as_str());
1175 let got = d.get_text().unwrap();
1176 assert_eq!(got.as_str().len(), 400_000);
1177 assert_eq!(got.as_str(), huge.as_str());
1178 assert_eq!(d.data.len(), 1);
1179 }
1180
1181 #[test]
1182 fn set_text_twice_replaces_and_does_not_grow() {
1183 let mut d = DragData::new();
1184 d.set_text("one");
1185 d.set_text("two");
1186 assert_eq!(d.data.len(), 1);
1187 assert_eq!(d.get_text().unwrap().as_str(), "two");
1188 }
1189
1190 #[test]
1191 fn set_text_then_set_data_on_same_mime_wins() {
1192 let mut d = DragData::new();
1193 d.set_text("text");
1194 d.set_data("text/plain", b"raw".to_vec());
1195 assert_eq!(d.data.len(), 1);
1196 assert_eq!(d.get_text().unwrap().as_str(), "raw");
1197 }
1198
1199 #[test]
1200 fn get_text_on_invalid_utf8_yields_empty_string_not_panic() {
1201 let mut d = DragData::new();
1205 d.set_data("text/plain", alloc::vec![0xFF, 0xFE, 0x80]);
1206 let got = d.get_text().expect("entry exists, so Some");
1207 assert_eq!(got.as_str(), "");
1208 assert_eq!(d.get_data("text/plain"), Some(&[0xFFu8, 0xFE, 0x80][..]));
1209 }
1210
1211 #[test]
1212 fn get_text_truncated_utf8_yields_empty_string() {
1213 let mut d = DragData::new();
1214 d.set_data("text/plain", alloc::vec![0xF0, 0x9F, 0x98]);
1216 assert_eq!(d.get_text().unwrap().as_str(), "");
1217 }
1218
1219 #[test]
1220 fn get_text_is_none_when_only_other_mimes_present() {
1221 let mut d = DragData::new();
1222 d.set_data("text/html", b"<b>x</b>".to_vec());
1223 assert!(d.get_text().is_none());
1224 }
1225
1226 #[test]
1229 fn drag_context_new_preserves_session_id_and_is_not_cancelled() {
1230 for sid in [0u64, 1, u64::MAX] {
1231 let ctx = DragContext::new(
1232 ActiveDragType::WindowMove(WindowMoveDrag {
1233 start_position: LogicalPosition::zero(),
1234 current_position: LogicalPosition::zero(),
1235 initial_window_position: WindowPosition::Uninitialized,
1236 }),
1237 sid,
1238 );
1239 assert_eq!(ctx.session_id, sid);
1240 assert!(!ctx.cancelled);
1241 }
1242 }
1243
1244 #[test]
1245 fn text_selection_invariants_at_extremes() {
1246 let pos = LogicalPosition::new(f32::MIN, f32::MAX);
1247 let ctx = DragContext::text_selection(
1248 dom(usize::MAX),
1249 NodeId::new(usize::MAX),
1250 pos,
1251 u64::MAX,
1252 );
1253 let ts = ctx.as_text_selection().expect("must be a text selection");
1254 assert_eq!(ts.dom_id, dom(usize::MAX));
1255 assert_eq!(ts.anchor_ifc_node, NodeId::new(usize::MAX));
1256 assert!(ts.anchor_cursor.is_none());
1257 assert!(ts.auto_scroll_container.is_none());
1258 assert_eq!(ts.auto_scroll_direction, AutoScrollDirection::None);
1259 assert_eq!(ts.start_mouse_position.x.to_bits(), f32::MIN.to_bits());
1262 assert_eq!(ts.start_mouse_position.y.to_bits(), f32::MAX.to_bits());
1263 assert_eq!(
1264 ts.current_mouse_position.x.to_bits(),
1265 ts.start_mouse_position.x.to_bits()
1266 );
1267 assert_eq!(ctx.session_id, u64::MAX);
1268 }
1269
1270 #[test]
1271 fn text_selection_with_nan_position_does_not_panic() {
1272 let ctx = DragContext::text_selection(
1273 dom(0),
1274 NodeId::ZERO,
1275 LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
1276 0,
1277 );
1278 let ts = ctx.as_text_selection().unwrap();
1279 assert!(ts.start_mouse_position.x.is_nan());
1280 assert!(ts.current_mouse_position.x.is_nan());
1281 assert_eq!(ts.start_mouse_position.y, f32::NEG_INFINITY);
1282 }
1283
1284 #[test]
1285 fn scrollbar_thumb_stores_all_float_metrics_verbatim_incl_nan_inf() {
1286 let ctx = DragContext::scrollbar_thumb(
1287 dom(3),
1288 NodeId::new(9),
1289 ScrollbarAxis::Horizontal,
1290 LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
1291 f32::NAN,
1292 f32::INFINITY,
1293 -0.0,
1294 f32::MAX,
1295 0,
1296 );
1297 let sb = ctx.as_scrollbar_thumb().unwrap();
1298 assert_eq!(sb.dom_id, dom(3));
1299 assert_eq!(sb.scroll_container_node, NodeId::new(9));
1300 assert_eq!(sb.axis, ScrollbarAxis::Horizontal);
1301 assert!(sb.start_scroll_offset.is_nan(), "NaN was mangled at construction");
1302 assert_eq!(sb.track_length_px, f32::INFINITY);
1303 assert!(sb.content_length_px.is_sign_negative(), "-0.0 lost its sign");
1304 assert_eq!(sb.content_length_px, 0.0);
1305 assert_eq!(sb.viewport_length_px, f32::MAX);
1306 assert_eq!(sb.current_mouse_position.x, f32::INFINITY);
1307 }
1308
1309 #[test]
1310 fn scrollbar_thumb_all_zero_is_constructible() {
1311 let ctx = vscroll(0.0, 0.0, 0.0, 0.0);
1312 assert!(ctx.is_scrollbar_thumb());
1313 let sb = ctx.as_scrollbar_thumb().unwrap();
1314 assert_eq!(sb.start_scroll_offset, 0.0);
1315 assert_eq!(sb.track_length_px, 0.0);
1316 }
1317
1318 #[test]
1319 fn node_drag_invariants_hold_after_construction() {
1320 let mut data = DragData::new();
1321 data.set_text("payload");
1322 let ctx = DragContext::node_drag(
1323 dom(2),
1324 NodeId::new(usize::MAX),
1325 LogicalPosition::new(-1.5, 2.5),
1326 data,
1327 u64::MAX,
1328 );
1329 let nd = ctx.as_node_drag().unwrap();
1330 assert_eq!(nd.dom_id, dom(2));
1331 assert_eq!(nd.node_id, NodeId::new(usize::MAX));
1332 assert_eq!(nd.start_position, nd.current_position);
1333 assert_eq!(nd.drag_offset, LogicalPosition::zero());
1334 assert!(nd.current_drop_target.into_option().is_none());
1335 assert!(nd.previous_drop_target.into_option().is_none());
1336 assert!(!nd.drop_accepted);
1337 assert_eq!(nd.drop_effect, DropEffect::None);
1338 assert_eq!(nd.drag_data.get_text().unwrap().as_str(), "payload");
1339 }
1340
1341 #[test]
1342 fn node_drag_with_empty_drag_data_is_fine() {
1343 let ctx = DragContext::node_drag(
1344 dom(0),
1345 NodeId::ZERO,
1346 LogicalPosition::new(f32::NAN, f32::NAN),
1347 DragData::new(),
1348 0,
1349 );
1350 let nd = ctx.as_node_drag().unwrap();
1351 assert!(nd.drag_data.data.is_empty());
1352 assert!(nd.start_position.x.is_nan());
1353 assert!(nd.current_position.x.is_nan());
1354 }
1355
1356 #[test]
1357 fn window_move_preserves_initial_window_position_extremes() {
1358 for wp in [
1359 WindowPosition::Uninitialized,
1360 WindowPosition::Initialized(PhysicalPosition {
1361 x: i32::MIN,
1362 y: i32::MAX,
1363 }),
1364 WindowPosition::Initialized(PhysicalPosition { x: 0, y: 0 }),
1365 ] {
1366 let ctx = DragContext::window_move(
1367 LogicalPosition::new(f32::MAX, f32::MIN),
1368 wp,
1369 u64::MAX,
1370 );
1371 let wm = ctx.as_window_move().unwrap();
1372 assert_eq!(wm.initial_window_position, wp);
1373 assert_eq!(wm.start_position.x.to_bits(), f32::MAX.to_bits());
1374 assert_eq!(
1375 wm.current_position.y.to_bits(),
1376 wm.start_position.y.to_bits()
1377 );
1378 }
1379 }
1380
1381 #[test]
1382 fn file_drop_empty_file_list_is_allowed() {
1383 let ctx = DragContext::file_drop(Vec::new(), LogicalPosition::zero(), 0);
1384 let fd = ctx.as_file_drop().unwrap();
1385 assert_eq!(fd.files.len(), 0);
1386 assert!(fd.drop_target.into_option().is_none());
1387 assert_eq!(fd.drop_effect, DropEffect::Copy);
1388 }
1389
1390 #[test]
1391 fn file_drop_unicode_and_pathological_filenames_round_trip() {
1392 let files = alloc::vec![
1393 AzString::from(""),
1394 AzString::from("/tmp/\u{1F4C1}/f\u{0301}ile.txt"),
1395 AzString::from("C:\\Windows\\..\\..\\etc\\passwd"),
1396 AzString::from("a".repeat(4096)),
1397 AzString::from("with\nnewline\tand\u{0}nul"),
1398 ];
1399 let ctx = DragContext::file_drop(files, LogicalPosition::new(1.0, 2.0), 42);
1400 let fd = ctx.as_file_drop().unwrap();
1401 assert_eq!(fd.files.len(), 5);
1402 assert_eq!(fd.files.as_slice()[0].as_str(), "");
1403 assert_eq!(fd.files.as_slice()[3].as_str().len(), 4096);
1404 assert_eq!(fd.files.as_slice()[4].as_str(), "with\nnewline\tand\u{0}nul");
1405 assert_eq!(ctx.session_id, 42);
1406 }
1407
1408 #[test]
1409 fn file_drop_ten_thousand_files_does_not_hang() {
1410 let mut files = Vec::with_capacity(10_000);
1411 for i in 0..10_000usize {
1412 files.push(AzString::from(i.to_string()));
1413 }
1414 let ctx = DragContext::file_drop(files, LogicalPosition::zero(), 1);
1415 assert_eq!(ctx.as_file_drop().unwrap().files.len(), 10_000);
1416 }
1417
1418 fn one_of_each() -> [DragContext; 6] {
1421 [
1422 DragContext::text_selection(dom(0), NodeId::ZERO, LogicalPosition::zero(), 0),
1423 vscroll(0.0, 100.0, 200.0, 100.0),
1424 DragContext::node_drag(
1425 dom(0),
1426 NodeId::ZERO,
1427 LogicalPosition::zero(),
1428 DragData::new(),
1429 0,
1430 ),
1431 DragContext::window_move(
1432 LogicalPosition::zero(),
1433 WindowPosition::Uninitialized,
1434 0,
1435 ),
1436 resize_ctx(),
1437 DragContext::file_drop(Vec::new(), LogicalPosition::zero(), 0),
1438 ]
1439 }
1440
1441 #[test]
1442 fn predicates_are_mutually_exclusive_across_every_variant() {
1443 for (i, ctx) in one_of_each().iter().enumerate() {
1444 let flags = [
1445 ctx.is_text_selection(),
1446 ctx.is_scrollbar_thumb(),
1447 ctx.is_node_drag(),
1448 ctx.is_window_move(),
1449 ctx.is_file_drop(),
1450 ];
1451 let set = flags.iter().filter(|f| **f).count();
1452 if i == 4 {
1453 assert_eq!(set, 0, "WindowResize matched a predicate");
1455 } else {
1456 assert_eq!(set, 1, "variant {i} matched {set} predicates, expected 1");
1457 assert!(flags[if i == 5 { 4 } else { i }], "wrong predicate for {i}");
1458 }
1459 }
1460 }
1461
1462 #[test]
1463 fn as_accessors_return_none_for_every_non_matching_variant() {
1464 for (i, ctx) in one_of_each().iter().enumerate() {
1465 assert_eq!(ctx.as_text_selection().is_some(), i == 0);
1466 assert_eq!(ctx.as_scrollbar_thumb().is_some(), i == 1);
1467 assert_eq!(ctx.as_node_drag().is_some(), i == 2);
1468 assert_eq!(ctx.as_window_move().is_some(), i == 3);
1469 assert_eq!(ctx.as_file_drop().is_some(), i == 5);
1470 }
1471 }
1472
1473 #[test]
1474 fn as_mut_accessors_return_none_for_every_non_matching_variant() {
1475 for (i, ctx) in one_of_each().iter_mut().enumerate() {
1476 assert_eq!(ctx.as_text_selection_mut().is_some(), i == 0);
1477 assert_eq!(ctx.as_scrollbar_thumb_mut().is_some(), i == 1);
1478 assert_eq!(ctx.as_node_drag_mut().is_some(), i == 2);
1479 assert_eq!(ctx.as_file_drop_mut().is_some(), i == 5);
1480 }
1481 }
1482
1483 #[test]
1484 fn as_text_selection_mut_writes_are_visible_through_shared_getter() {
1485 let mut ctx =
1486 DragContext::text_selection(dom(0), NodeId::new(1), LogicalPosition::zero(), 0);
1487 {
1488 let ts = ctx.as_text_selection_mut().unwrap();
1489 ts.auto_scroll_direction = AutoScrollDirection::DownRight;
1490 ts.auto_scroll_container = Some(NodeId::new(77));
1491 }
1492 let ts = ctx.as_text_selection().unwrap();
1493 assert_eq!(ts.auto_scroll_direction, AutoScrollDirection::DownRight);
1494 assert_eq!(ts.auto_scroll_container, Some(NodeId::new(77)));
1495 }
1496
1497 #[test]
1498 fn as_scrollbar_thumb_mut_writes_are_visible_through_shared_getter() {
1499 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1500 ctx.as_scrollbar_thumb_mut().unwrap().start_scroll_offset = f32::NAN;
1501 assert!(ctx.as_scrollbar_thumb().unwrap().start_scroll_offset.is_nan());
1502 }
1503
1504 #[test]
1505 fn as_node_drag_mut_writes_are_visible_through_shared_getter() {
1506 let mut ctx = DragContext::node_drag(
1507 dom(0),
1508 NodeId::ZERO,
1509 LogicalPosition::zero(),
1510 DragData::new(),
1511 0,
1512 );
1513 {
1514 let nd = ctx.as_node_drag_mut().unwrap();
1515 nd.drop_accepted = true;
1516 nd.drop_effect = DropEffect::Move;
1517 nd.current_drop_target = Some(dnid(0, 4)).into();
1518 }
1519 let nd = ctx.as_node_drag().unwrap();
1520 assert!(nd.drop_accepted);
1521 assert_eq!(nd.drop_effect, DropEffect::Move);
1522 assert_eq!(
1523 nd.current_drop_target
1524 .into_option()
1525 .unwrap()
1526 .node
1527 .into_crate_internal(),
1528 Some(NodeId::new(4))
1529 );
1530 }
1531
1532 #[test]
1533 fn as_file_drop_mut_writes_are_visible_through_shared_getter() {
1534 let mut ctx = DragContext::file_drop(Vec::new(), LogicalPosition::zero(), 0);
1535 ctx.as_file_drop_mut().unwrap().drop_effect = DropEffect::Link;
1536 assert_eq!(ctx.as_file_drop().unwrap().drop_effect, DropEffect::Link);
1537 }
1538
1539 #[test]
1542 fn update_position_moves_current_for_every_variant_and_leaves_start_alone() {
1543 let new_pos = LogicalPosition::new(123.5, -456.25);
1544 for (i, mut ctx) in one_of_each().into_iter().enumerate() {
1545 let start_before = ctx.start_position();
1546 ctx.update_position(new_pos);
1547 assert_eq!(ctx.current_position(), new_pos, "variant {i} did not move");
1548 if i == 5 {
1549 assert_eq!(ctx.start_position(), new_pos);
1552 } else {
1553 assert_eq!(ctx.start_position(), start_before, "variant {i} start moved");
1554 }
1555 }
1556 }
1557
1558 #[test]
1559 fn update_position_with_nan_and_inf_is_stored_verbatim() {
1560 for mut ctx in one_of_each() {
1561 ctx.update_position(LogicalPosition::new(f32::NAN, f32::INFINITY));
1562 let cur = ctx.current_position();
1563 assert!(cur.x.is_nan());
1564 assert_eq!(cur.y, f32::INFINITY);
1565
1566 ctx.update_position(LogicalPosition::new(f32::MIN, f32::NEG_INFINITY));
1567 let cur = ctx.current_position();
1568 assert_eq!(cur.x.to_bits(), f32::MIN.to_bits());
1569 assert_eq!(cur.y, f32::NEG_INFINITY);
1570 }
1571 }
1572
1573 #[test]
1574 fn update_position_is_idempotent_and_last_write_wins() {
1575 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1576 for i in 0..1000u32 {
1577 ctx.update_position(LogicalPosition::new(i as f32, -(i as f32)));
1578 }
1579 assert_eq!(ctx.current_position(), LogicalPosition::new(999.0, -999.0));
1580 assert_eq!(ctx.start_position(), LogicalPosition::zero());
1581 }
1582
1583 #[test]
1584 fn window_resize_position_getters_work_without_a_predicate() {
1585 let mut ctx = resize_ctx();
1586 assert_eq!(ctx.start_position(), LogicalPosition::new(1.0, 2.0));
1587 assert_eq!(ctx.current_position(), LogicalPosition::new(1.0, 2.0));
1588 ctx.update_position(LogicalPosition::new(-3.0, -4.0));
1589 assert_eq!(ctx.current_position(), LogicalPosition::new(-3.0, -4.0));
1590 assert_eq!(ctx.start_position(), LogicalPosition::new(1.0, 2.0));
1591 assert!(ctx.as_text_selection().is_none());
1593 assert!(ctx.as_window_move().is_none());
1594 }
1595
1596 #[test]
1599 fn scroll_offset_is_none_for_non_scrollbar_drags() {
1600 for (i, ctx) in one_of_each().iter().enumerate() {
1601 if i == 1 {
1602 continue;
1603 }
1604 assert!(
1605 ctx.calculate_scrollbar_scroll_offset().is_none(),
1606 "variant {i} returned Some"
1607 );
1608 }
1609 }
1610
1611 #[test]
1612 fn scroll_offset_basic_vertical_half_track() {
1613 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1617 ctx.update_position(LogicalPosition::new(0.0, 25.0));
1618 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(50.0));
1619 }
1620
1621 #[test]
1622 fn scroll_offset_horizontal_uses_x_and_ignores_y() {
1623 let mut ctx = DragContext::scrollbar_thumb(
1624 dom(0),
1625 NodeId::new(1),
1626 ScrollbarAxis::Horizontal,
1627 LogicalPosition::zero(),
1628 0.0,
1629 100.0,
1630 200.0,
1631 100.0,
1632 0,
1633 );
1634 ctx.update_position(LogicalPosition::new(0.0, 9999.0));
1636 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1637 ctx.update_position(LogicalPosition::new(25.0, 9999.0));
1638 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(50.0));
1639 }
1640
1641 #[test]
1642 fn scroll_offset_clamps_to_range_on_huge_and_infinite_drags() {
1643 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0); for (y, expect) in [
1645 (1.0e30f32, 100.0f32),
1646 (f32::MAX, 100.0),
1647 (f32::INFINITY, 100.0),
1648 (-1.0e30, 0.0),
1649 (f32::MIN, 0.0),
1650 (f32::NEG_INFINITY, 0.0),
1651 ] {
1652 ctx.update_position(LogicalPosition::new(0.0, y));
1653 assert_eq!(
1654 ctx.calculate_scrollbar_scroll_offset(),
1655 Some(expect),
1656 "y = {y}"
1657 );
1658 }
1659 }
1660
1661 #[test]
1662 fn scroll_offset_result_always_within_range_for_finite_inputs() {
1663 let mut ctx = vscroll(30.0, 80.0, 500.0, 120.0);
1664 let range = 500.0 - 120.0;
1665 for y in [-1e9f32, -1.0, 0.0, 0.5, 1.0, 37.0, 1e9] {
1666 ctx.update_position(LogicalPosition::new(0.0, y));
1667 let off = ctx.calculate_scrollbar_scroll_offset().unwrap();
1668 assert!(
1669 (0.0..=range).contains(&off),
1670 "offset {off} escaped [0, {range}] for y = {y}"
1671 );
1672 }
1673 }
1674
1675 #[test]
1676 fn scroll_offset_out_of_range_start_offset_is_clamped_back_in() {
1677 let ctx = vscroll(9999.0, 100.0, 200.0, 100.0);
1679 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(100.0));
1680 let ctx = vscroll(-9999.0, 100.0, 200.0, 100.0);
1681 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1682 }
1683
1684 #[test]
1685 fn scroll_offset_non_scrollable_content_returns_start_offset_unchanged() {
1686 let mut ctx = vscroll(7.0, 100.0, 50.0, 100.0);
1689 ctx.update_position(LogicalPosition::new(0.0, 1000.0));
1690 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(7.0));
1691
1692 let ctx = vscroll(7.0, 100.0, 100.0, 100.0); assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(7.0));
1694
1695 let ctx = vscroll(7.0, 100.0, 0.0, 0.0); assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(7.0));
1697 }
1698
1699 #[test]
1700 fn scroll_offset_zero_or_negative_track_returns_start_offset() {
1701 for track in [0.0f32, -1.0, -1e30, f32::NEG_INFINITY] {
1702 let mut ctx = vscroll(7.0, track, 200.0, 100.0);
1703 ctx.update_position(LogicalPosition::new(0.0, 50.0));
1704 assert_eq!(
1705 ctx.calculate_scrollbar_scroll_offset(),
1706 Some(7.0),
1707 "track = {track}"
1708 );
1709 }
1710 }
1711
1712 #[test]
1713 fn scroll_offset_negative_content_and_viewport_return_start_offset() {
1714 let mut ctx = vscroll(3.0, 100.0, -200.0, -100.0);
1715 ctx.update_position(LogicalPosition::new(0.0, 50.0));
1716 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(3.0));
1718 }
1719
1720 #[test]
1721 fn scroll_offset_nan_start_offset_propagates_nan_without_panicking() {
1722 let mut ctx = vscroll(f32::NAN, 100.0, 200.0, 100.0);
1723 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1724 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1725 assert!(off.is_nan());
1727 }
1728
1729 #[test]
1730 fn scroll_offset_nan_track_length_propagates_nan_without_panicking() {
1731 let mut ctx = vscroll(0.0, f32::NAN, 200.0, 100.0);
1732 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1733 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1734 assert!(off.is_nan());
1738 }
1739
1740 #[test]
1741 fn scroll_offset_nan_mouse_position_propagates_nan_without_panicking() {
1742 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1743 ctx.update_position(LogicalPosition::new(0.0, f32::NAN));
1744 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1745 assert!(off.is_nan());
1746 }
1747
1748 #[test]
1749 fn scroll_offset_infinite_content_yields_nan_on_zero_mouse_delta() {
1750 let ctx = vscroll(0.0, 100.0, f32::INFINITY, 100.0);
1754 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1755 assert!(off.is_nan(), "expected the 0 * inf NaN, got {off}");
1756 }
1757
1758 #[test]
1764 fn scroll_offset_nan_content_length_falls_back_without_panicking() {
1765 let mut ctx = vscroll(0.0, 100.0, f32::NAN, 100.0);
1766 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1767 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1768 }
1769
1770 #[test]
1771 fn scroll_offset_nan_viewport_length_falls_back_without_panicking() {
1772 let mut ctx = vscroll(0.0, 100.0, 200.0, f32::NAN);
1773 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1774 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1775 }
1776
1777 #[test]
1778 fn scroll_offset_infinite_content_and_viewport_falls_back_without_panicking() {
1779 let mut ctx = vscroll(0.0, 100.0, f32::INFINITY, f32::INFINITY);
1781 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1782 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1783 }
1784
1785 #[test]
1788 fn remap_text_selection_other_dom_is_a_no_op_and_succeeds() {
1789 let mut ctx =
1790 DragContext::text_selection(dom(0), NodeId::new(5), LogicalPosition::zero(), 0);
1791 assert!(ctx.remap_node_ids(dom(1), &nid_map(&[(5, 500)])));
1792 assert_eq!(ctx.as_text_selection().unwrap().anchor_ifc_node, NodeId::new(5));
1793 }
1794
1795 #[test]
1796 fn remap_text_selection_missing_anchor_cancels_the_drag() {
1797 let mut ctx =
1798 DragContext::text_selection(dom(0), NodeId::new(5), LogicalPosition::zero(), 0);
1799 assert!(!ctx.remap_node_ids(dom(0), &BTreeMap::new()));
1800 assert!(!ctx.remap_node_ids(dom(0), &nid_map(&[(6, 7)])));
1801 assert_eq!(ctx.as_text_selection().unwrap().anchor_ifc_node, NodeId::new(5));
1803 }
1804
1805 #[test]
1806 fn remap_text_selection_clears_only_the_missing_auto_scroll_container() {
1807 let mut ctx =
1808 DragContext::text_selection(dom(0), NodeId::new(5), LogicalPosition::zero(), 0);
1809 ctx.as_text_selection_mut().unwrap().auto_scroll_container = Some(NodeId::new(9));
1810
1811 assert!(ctx.remap_node_ids(dom(0), &nid_map(&[(5, 50), (9, 90)])));
1813 let ts = ctx.as_text_selection().unwrap();
1814 assert_eq!(ts.anchor_ifc_node, NodeId::new(50));
1815 assert_eq!(ts.auto_scroll_container, Some(NodeId::new(90)));
1816
1817 assert!(ctx.remap_node_ids(dom(0), &nid_map(&[(50, 51)])));
1819 let ts = ctx.as_text_selection().unwrap();
1820 assert_eq!(ts.anchor_ifc_node, NodeId::new(51));
1821 assert_eq!(ts.auto_scroll_container, None);
1822 }
1823
1824 #[test]
1825 fn remap_scrollbar_empty_map_cancels_the_drag() {
1826 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0); assert!(!ctx.remap_node_ids(dom(0), &BTreeMap::new()));
1828 assert_eq!(
1829 ctx.as_scrollbar_thumb().unwrap().scroll_container_node,
1830 NodeId::new(1)
1831 );
1832 }
1833
1834 #[test]
1835 fn remap_node_drag_missing_node_cancels_and_leaves_targets_alone() {
1836 let mut ctx = DragContext::node_drag(
1837 dom(0),
1838 NodeId::new(1),
1839 LogicalPosition::zero(),
1840 DragData::new(),
1841 0,
1842 );
1843 ctx.as_node_drag_mut().unwrap().current_drop_target = Some(dnid(0, 5)).into();
1844
1845 assert!(!ctx.remap_node_ids(dom(0), &nid_map(&[(5, 50)])));
1847 let nd = ctx.as_node_drag().unwrap();
1848 assert_eq!(nd.node_id, NodeId::new(1));
1849 assert_eq!(
1850 nd.current_drop_target
1851 .into_option()
1852 .unwrap()
1853 .node
1854 .into_crate_internal(),
1855 Some(NodeId::new(5)),
1856 "targets must not be half-remapped on a cancelled drag"
1857 );
1858 }
1859
1860 #[test]
1861 fn remap_node_drag_clears_drop_targets_that_were_removed() {
1862 let mut ctx = DragContext::node_drag(
1863 dom(0),
1864 NodeId::new(1),
1865 LogicalPosition::zero(),
1866 DragData::new(),
1867 0,
1868 );
1869 {
1870 let nd = ctx.as_node_drag_mut().unwrap();
1871 nd.current_drop_target = Some(dnid(0, 5)).into();
1872 nd.previous_drop_target = Some(dnid(0, 6)).into();
1873 }
1874 assert!(ctx.remap_node_ids(dom(0), &nid_map(&[(1, 100)])));
1876 let nd = ctx.as_node_drag().unwrap();
1877 assert_eq!(nd.node_id, NodeId::new(100));
1878 assert!(nd.current_drop_target.into_option().is_none());
1879 assert!(nd.previous_drop_target.into_option().is_none());
1880 }
1881
1882 #[test]
1883 fn remap_does_not_touch_drop_targets_belonging_to_another_dom() {
1884 let mut ctx = DragContext::node_drag(
1885 dom(0),
1886 NodeId::new(1),
1887 LogicalPosition::zero(),
1888 DragData::new(),
1889 0,
1890 );
1891 ctx.as_node_drag_mut().unwrap().current_drop_target = Some(dnid(1, 5)).into();
1893
1894 assert!(ctx.remap_node_ids(dom(0), &nid_map(&[(1, 100), (5, 500)])));
1895 let nd = ctx.as_node_drag().unwrap();
1896 assert_eq!(nd.node_id, NodeId::new(100));
1897 let dt = nd.current_drop_target.into_option().unwrap();
1898 assert_eq!(dt.dom, dom(1));
1899 assert_eq!(dt.node.into_crate_internal(), Some(NodeId::new(5)));
1900 }
1901
1902 #[test]
1903 fn remap_drop_target_without_a_node_id_is_left_intact() {
1904 let mut ctx = DragContext::node_drag(
1906 dom(0),
1907 NodeId::new(1),
1908 LogicalPosition::zero(),
1909 DragData::new(),
1910 0,
1911 );
1912 ctx.as_node_drag_mut().unwrap().current_drop_target = Some(dnid_no_node(0)).into();
1913
1914 assert!(ctx.remap_node_ids(dom(0), &nid_map(&[(1, 1)])));
1915 let dt = ctx
1916 .as_node_drag()
1917 .unwrap()
1918 .current_drop_target
1919 .into_option()
1920 .expect("target must survive");
1921 assert_eq!(dt.dom, dom(0));
1922 assert_eq!(dt.node.into_crate_internal(), None);
1923 }
1924
1925 #[test]
1926 fn remap_drop_target_directly_handles_none_and_foreign_doms() {
1927 let mut none_target = OptionDomNodeId::None;
1929 DragContext::remap_drop_target(&mut none_target, dom(0), &nid_map(&[(1, 2)]));
1930 assert!(none_target.into_option().is_none());
1931
1932 let mut foreign: OptionDomNodeId = Some(dnid(9, 1)).into();
1933 DragContext::remap_drop_target(&mut foreign, dom(0), &nid_map(&[(1, 2)]));
1934 let dt = foreign.into_option().unwrap();
1935 assert_eq!(dt.dom, dom(9));
1936 assert_eq!(dt.node.into_crate_internal(), Some(NodeId::new(1)));
1937
1938 let mut matching: OptionDomNodeId = Some(dnid(0, 1)).into();
1940 DragContext::remap_drop_target(&mut matching, dom(0), &BTreeMap::new());
1941 assert!(matching.into_option().is_none());
1942 }
1943
1944 #[test]
1945 fn remap_file_drop_always_survives_and_clears_stale_targets() {
1946 let mut ctx = DragContext::file_drop(
1947 alloc::vec![AzString::from("/tmp/a")],
1948 LogicalPosition::zero(),
1949 0,
1950 );
1951 assert!(ctx.remap_node_ids(dom(0), &BTreeMap::new()));
1953 assert!(ctx.as_file_drop().unwrap().drop_target.into_option().is_none());
1954
1955 ctx.as_file_drop_mut().unwrap().drop_target = Some(dnid(0, 5)).into();
1957 assert!(ctx.remap_node_ids(dom(0), &nid_map(&[(5, 55)])));
1958 assert_eq!(
1959 ctx.as_file_drop()
1960 .unwrap()
1961 .drop_target
1962 .into_option()
1963 .unwrap()
1964 .node
1965 .into_crate_internal(),
1966 Some(NodeId::new(55))
1967 );
1968
1969 assert!(ctx.remap_node_ids(dom(0), &BTreeMap::new()));
1971 assert!(ctx.as_file_drop().unwrap().drop_target.into_option().is_none());
1972 assert_eq!(ctx.as_file_drop().unwrap().files.len(), 1);
1973 }
1974
1975 #[test]
1976 fn remap_window_drags_always_succeed() {
1977 let mut wm = DragContext::window_move(
1978 LogicalPosition::zero(),
1979 WindowPosition::Uninitialized,
1980 0,
1981 );
1982 assert!(wm.remap_node_ids(dom(0), &BTreeMap::new()));
1983 assert!(wm.remap_node_ids(dom(usize::MAX), &nid_map(&[(1, 2)])));
1984
1985 let mut wr = resize_ctx();
1986 assert!(wr.remap_node_ids(dom(0), &BTreeMap::new()));
1987 assert_eq!(wr.current_position(), LogicalPosition::new(1.0, 2.0));
1988 }
1989
1990 #[test]
1991 fn remap_with_a_hundred_thousand_entries_does_not_hang() {
1992 let mut m = BTreeMap::new();
1993 for i in 0..100_000usize {
1994 m.insert(NodeId::new(i), NodeId::new(i + 1));
1995 }
1996 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0); assert!(ctx.remap_node_ids(dom(0), &m));
1998 assert_eq!(
1999 ctx.as_scrollbar_thumb().unwrap().scroll_container_node,
2000 NodeId::new(2)
2001 );
2002 }
2003
2004 #[test]
2005 fn remap_identity_map_is_a_fixed_point() {
2006 let mut ctx = DragContext::node_drag(
2007 dom(0),
2008 NodeId::new(1),
2009 LogicalPosition::new(1.0, 2.0),
2010 DragData::new(),
2011 0,
2012 );
2013 {
2014 let nd = ctx.as_node_drag_mut().unwrap();
2015 nd.current_drop_target = Some(dnid(0, 5)).into();
2016 nd.previous_drop_target = Some(dnid(0, 5)).into();
2017 }
2018 let before = ctx.clone();
2019 let m = nid_map(&[(1, 1), (5, 5)]);
2020 assert!(ctx.remap_node_ids(dom(0), &m));
2021 assert!(ctx.remap_node_ids(dom(0), &m)); assert_eq!(ctx, before);
2023 }
2024
2025 #[test]
2028 fn drag_delta_new_stores_extremes_verbatim() {
2029 for (dx, dy) in [
2030 (0.0f32, 0.0f32),
2031 (f32::MAX, f32::MIN),
2032 (f32::INFINITY, f32::NEG_INFINITY),
2033 (f32::MIN_POSITIVE, -f32::MIN_POSITIVE),
2034 ] {
2035 let d = DragDelta::new(dx, dy);
2036 assert_eq!(d.dx.to_bits(), dx.to_bits());
2037 assert_eq!(d.dy.to_bits(), dy.to_bits());
2038 }
2039 }
2040
2041 #[test]
2042 fn drag_delta_zero_is_the_neutral_default() {
2043 let z = DragDelta::zero();
2044 assert_eq!(z.dx, 0.0);
2045 assert_eq!(z.dy, 0.0);
2046 assert!(!z.dx.is_sign_negative(), "zero() must be +0.0, not -0.0");
2047 assert_eq!(z, DragDelta::default());
2048 assert_eq!(z, DragDelta::new(0.0, 0.0));
2049 assert_eq!(DragDelta::new(-0.0, -0.0), z);
2051 assert!(DragDelta::new(-0.0, -0.0).dx.is_sign_negative());
2053 }
2054
2055 #[test]
2056 fn drag_delta_nan_is_not_equal_to_itself() {
2057 let n = DragDelta::new(f32::NAN, f32::NAN);
2060 assert_ne!(n, DragDelta::new(f32::NAN, f32::NAN));
2061 assert_ne!(n, DragDelta::zero());
2062 assert!(n.dx.is_nan() && n.dy.is_nan());
2063 assert!(n.partial_cmp(&DragDelta::zero()).is_none());
2065 }
2066}