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 || drag.track_length_px <= 0.0 {
651 return Some(drag.start_scroll_offset);
652 }
653
654 let thumb_length = (drag.viewport_length_px / drag.content_length_px) * drag.track_length_px;
656 let scrollable_track = drag.track_length_px - thumb_length;
657
658 if scrollable_track <= 0.0 {
659 return Some(drag.start_scroll_offset);
660 }
661
662 let scroll_ratio = mouse_delta / scrollable_track;
664 let scroll_delta = scroll_ratio * scrollable_range;
665
666 let new_offset = drag.start_scroll_offset + scroll_delta;
668
669 Some(new_offset.clamp(0.0, scrollable_range))
671 }
672
673 fn remap_drop_target(
676 target: &mut OptionDomNodeId,
677 dom_id: DomId,
678 node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
679 ) {
680 let dt = match target.into_option() {
681 Some(dt) if dt.dom == dom_id => dt,
682 _ => return,
683 };
684 let Some(old_nid) = dt.node.into_crate_internal() else {
685 return;
686 };
687 if let Some(&new_nid) = node_id_map.get(&old_nid) {
688 *target = Some(DomNodeId {
689 dom: dom_id,
690 node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_nid)),
691 }).into();
692 } else {
693 *target = OptionDomNodeId::None;
694 }
695 }
696
697 pub fn remap_node_ids(
703 &mut self,
704 dom_id: DomId,
705 node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
706 ) -> bool {
707 match &mut self.drag_type {
708 ActiveDragType::TextSelection(ref mut drag) => {
709 if drag.dom_id != dom_id {
710 return true;
711 }
712 if let Some(&new_id) = node_id_map.get(&drag.anchor_ifc_node) {
713 drag.anchor_ifc_node = new_id;
714 } else {
715 return false; }
717 if let Some(ref mut container) = drag.auto_scroll_container {
718 if let Some(&new_id) = node_id_map.get(container) {
719 *container = new_id;
720 } else {
721 drag.auto_scroll_container = None;
722 }
723 }
724 true
725 }
726 ActiveDragType::ScrollbarThumb(ref mut drag) => {
727 if drag.dom_id != dom_id {
730 return true;
731 }
732 if let Some(&new_id) = node_id_map.get(&drag.scroll_container_node) {
733 drag.scroll_container_node = new_id;
734 true
735 } else {
736 false }
738 }
739 ActiveDragType::Node(ref mut drag) => {
740 if drag.dom_id != dom_id {
741 return true;
742 }
743 if let Some(&new_id) = node_id_map.get(&drag.node_id) {
744 drag.node_id = new_id;
745 } else {
746 return false; }
748 Self::remap_drop_target(&mut drag.current_drop_target, dom_id, node_id_map);
753 Self::remap_drop_target(&mut drag.previous_drop_target, dom_id, node_id_map);
754 true
755 }
756 ActiveDragType::WindowMove(_) | ActiveDragType::WindowResize(_) => true,
758 ActiveDragType::FileDrop(ref mut drag) => {
759 Self::remap_drop_target(&mut drag.drop_target, dom_id, node_id_map);
760 true
761 }
762 }
763 }
764}
765
766azul_css::impl_option!(
767 DragContext,
768 OptionDragContext,
769 copy = false,
770 [Debug, Clone, PartialEq]
771);
772
773
774#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
777#[repr(C)]
778pub struct DragDelta {
779 pub dx: f32,
780 pub dy: f32,
781}
782
783impl DragDelta {
784 #[inline]
785 #[must_use] pub const fn new(dx: f32, dy: f32) -> Self {
786 Self { dx, dy }
787 }
788 #[inline]
789 #[must_use] pub const fn zero() -> Self {
790 Self::new(0.0, 0.0)
791 }
792}
793
794impl_option!(
795 DragDelta,
796 OptionDragDelta,
797 [Debug, Copy, Clone, PartialEq, PartialOrd]
798);
799
800#[cfg(test)]
801mod audit_tests {
802 use super::*;
803 use crate::styled_dom::NodeHierarchyItemId;
804
805 fn node_map(from: usize, to: usize) -> alloc::collections::BTreeMap<NodeId, NodeId> {
806 let mut m = alloc::collections::BTreeMap::new();
807 m.insert(NodeId::new(from), NodeId::new(to));
808 m
809 }
810
811 fn dnid(dom: usize, node: usize) -> DomNodeId {
812 DomNodeId {
813 dom: DomId { inner: dom },
814 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
815 }
816 }
817
818 #[test]
819 fn scrollbar_remap_scoped_to_dom() {
820 let mut ctx = DragContext::scrollbar_thumb(
821 DomId { inner: 0 },
822 NodeId::new(3),
823 ScrollbarAxis::Vertical,
824 LogicalPosition::zero(),
825 0.0, 100.0, 300.0, 100.0,
826 1,
827 );
828 let ok = ctx.remap_node_ids(DomId { inner: 1 }, &node_map(3, 99));
830 assert!(ok);
831 assert_eq!(ctx.as_scrollbar_thumb().unwrap().scroll_container_node, NodeId::new(3));
832
833 let ok2 = ctx.remap_node_ids(DomId { inner: 0 }, &node_map(3, 99));
835 assert!(ok2);
836 assert_eq!(ctx.as_scrollbar_thumb().unwrap().scroll_container_node, NodeId::new(99));
837 }
838
839 #[test]
840 fn node_drag_remaps_previous_drop_target() {
841 let mut ctx = DragContext::node_drag(
842 DomId { inner: 0 },
843 NodeId::new(1),
844 LogicalPosition::zero(),
845 DragData::new(),
846 2,
847 );
848 {
849 let nd = ctx.as_node_drag_mut().unwrap();
850 nd.current_drop_target = Some(dnid(0, 5)).into();
851 nd.previous_drop_target = Some(dnid(0, 6)).into();
852 }
853 let mut m = alloc::collections::BTreeMap::new();
855 m.insert(NodeId::new(1), NodeId::new(1));
856 m.insert(NodeId::new(5), NodeId::new(50));
857 m.insert(NodeId::new(6), NodeId::new(60));
858 assert!(ctx.remap_node_ids(DomId { inner: 0 }, &m));
859
860 let nd = ctx.as_node_drag().unwrap();
861 let cur = nd.current_drop_target.into_option().unwrap().node.into_crate_internal().unwrap();
862 let prev = nd.previous_drop_target.into_option().unwrap().node.into_crate_internal().unwrap();
863 assert_eq!(cur, NodeId::new(50));
864 assert_eq!(prev, NodeId::new(60)); }
866}
867
868#[cfg(test)]
869mod autotest_generated {
870 use alloc::collections::BTreeMap;
871 use alloc::string::{String, ToString};
872
873 use super::*;
874 use crate::geom::PhysicalPosition;
875 use crate::styled_dom::NodeHierarchyItemId;
876
877 fn dom(i: usize) -> DomId {
880 DomId { inner: i }
881 }
882
883 fn dnid(dom_idx: usize, node: usize) -> DomNodeId {
884 DomNodeId {
885 dom: dom(dom_idx),
886 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
887 }
888 }
889
890 fn dnid_no_node(dom_idx: usize) -> DomNodeId {
893 DomNodeId {
894 dom: dom(dom_idx),
895 node: NodeHierarchyItemId::from_crate_internal(None),
896 }
897 }
898
899 fn nid_map(pairs: &[(usize, usize)]) -> BTreeMap<NodeId, NodeId> {
900 let mut m = BTreeMap::new();
901 for (from, to) in pairs {
902 m.insert(NodeId::new(*from), NodeId::new(*to));
903 }
904 m
905 }
906
907 fn vscroll(
909 start_scroll_offset: f32,
910 track_length_px: f32,
911 content_length_px: f32,
912 viewport_length_px: f32,
913 ) -> DragContext {
914 DragContext::scrollbar_thumb(
915 dom(0),
916 NodeId::new(1),
917 ScrollbarAxis::Vertical,
918 LogicalPosition::zero(),
919 start_scroll_offset,
920 track_length_px,
921 content_length_px,
922 viewport_length_px,
923 7,
924 )
925 }
926
927 fn resize_ctx() -> DragContext {
928 DragContext::new(
929 ActiveDragType::WindowResize(WindowResizeDrag {
930 edge: WindowResizeEdge::BottomRight,
931 start_position: LogicalPosition::new(1.0, 2.0),
932 current_position: LogicalPosition::new(1.0, 2.0),
933 initial_width: u32::MAX,
934 initial_height: 0,
935 }),
936 u64::MAX,
937 )
938 }
939
940 #[test]
944 fn dragdata_new_is_empty_and_matches_default() {
945 let d = DragData::new();
946 assert_eq!(d.data.len(), 0);
947 assert!(d.data.is_empty());
948 assert_eq!(d.effect_allowed, DragEffect::Uninitialized);
949 assert_eq!(d, DragData::default());
950 assert!(d.get_data("text/plain").is_none());
951 assert!(d.get_text().is_none());
952 }
953
954 #[test]
955 fn get_data_valid_minimal_positive_control() {
956 let mut d = DragData::new();
957 d.set_data("text/plain", b"hi".to_vec());
958 assert_eq!(d.get_data("text/plain"), Some(&b"hi"[..]));
959 }
960
961 #[test]
962 fn get_data_empty_key_on_empty_and_populated_returns_none() {
963 let empty = DragData::new();
964 assert!(empty.get_data("").is_none());
965
966 let mut d = DragData::new();
967 d.set_data("text/plain", b"x".to_vec());
968 assert!(d.get_data("").is_none());
969 }
970
971 #[test]
972 fn get_data_empty_key_is_a_real_key_when_stored() {
973 let mut d = DragData::new();
975 d.set_data("", b"empty-key".to_vec());
976 assert_eq!(d.get_data(""), Some(&b"empty-key"[..]));
977 assert!(d.get_data("text/plain").is_none());
978 }
979
980 #[test]
981 fn get_data_whitespace_only_keys_return_none() {
982 let mut d = DragData::new();
983 d.set_data("text/plain", b"x".to_vec());
984 for k in [" ", "\t\n", "\r\n", "\u{a0}", "\u{2028}"] {
985 assert!(d.get_data(k).is_none(), "whitespace key {k:?} matched");
986 }
987 }
988
989 #[test]
990 fn get_data_garbage_bytes_return_none_without_panicking() {
991 let mut d = DragData::new();
992 d.set_data("text/plain", b"x".to_vec());
993 for k in [
994 "\u{0}",
995 "\u{0}\u{1}\u{2}\u{7f}",
996 "\u{feff}",
997 "%%%;;;///",
998 "text/plain\u{0}",
999 "\u{0}text/plain",
1000 ] {
1001 assert!(d.get_data(k).is_none(), "garbage key {k:?} matched");
1002 }
1003 }
1004
1005 #[test]
1006 fn get_data_leading_trailing_junk_is_not_trimmed() {
1007 let mut d = DragData::new();
1008 d.set_data("text/plain", b"x".to_vec());
1009 assert!(d.get_data(" text/plain ").is_none());
1011 assert!(d.get_data("text/plain;garbage").is_none());
1012 assert!(d.get_data("text/plain ").is_none());
1013 assert!(d.get_data(" text/plain").is_none());
1014 assert_eq!(d.get_data("text/plain"), Some(&b"x"[..]));
1015 }
1016
1017 #[test]
1018 fn get_data_is_case_sensitive() {
1019 let mut d = DragData::new();
1021 d.set_data("text/plain", b"x".to_vec());
1022 assert!(d.get_data("TEXT/PLAIN").is_none());
1023 assert!(d.get_data("Text/Plain").is_none());
1024 }
1025
1026 #[test]
1027 fn get_data_boundary_number_strings_return_none() {
1028 let mut d = DragData::new();
1029 d.set_data("text/plain", b"x".to_vec());
1030 for k in [
1031 "0",
1032 "-0",
1033 "9223372036854775807", "-9223372036854775808", "18446744073709551615", "1e400",
1037 "NaN",
1038 "inf",
1039 "-inf",
1040 "1.7976931348623157e308",
1041 "5e-324",
1042 ] {
1043 assert!(d.get_data(k).is_none(), "numeric key {k:?} matched");
1044 }
1045 }
1046
1047 #[test]
1048 fn get_data_unicode_keys_round_trip() {
1049 let mut d = DragData::new();
1050 let emoji = "application/x-\u{1F600};charset=utf-8";
1051 let combining = "text/e\u{0301}"; d.set_data(emoji, b"grin".to_vec());
1053 d.set_data(combining, b"acute".to_vec());
1054
1055 assert_eq!(d.get_data(emoji), Some(&b"grin"[..]));
1056 assert_eq!(d.get_data(combining), Some(&b"acute"[..]));
1057 assert!(d.get_data("text/\u{e9}").is_none());
1059 assert_eq!(d.data.len(), 2);
1060 }
1061
1062 #[test]
1063 fn get_data_extremely_long_key_does_not_panic_or_hang() {
1064 let huge: String = "a".repeat(1_000_000);
1065 let mut d = DragData::new();
1066 d.set_data("text/plain", b"x".to_vec());
1067 assert!(d.get_data(&huge).is_none());
1069 d.set_data(huge.as_str(), b"huge-key".to_vec());
1071 assert_eq!(d.get_data(&huge), Some(&b"huge-key"[..]));
1072 let mut nearly = huge.clone();
1074 let _ = nearly.pop();
1075 nearly.push('b');
1076 assert!(d.get_data(&nearly).is_none());
1077 }
1078
1079 #[test]
1080 fn get_data_deeply_nested_brackets_do_not_stack_overflow() {
1081 let nested: String = "[".repeat(10_000);
1084 let mut d = DragData::new();
1085 assert!(d.get_data(&nested).is_none());
1086 d.set_data(nested.as_str(), b"nested".to_vec());
1087 assert_eq!(d.get_data(&nested), Some(&b"nested"[..]));
1088 }
1089
1090 #[test]
1091 fn set_data_replaces_existing_entry_for_same_mime() {
1092 let mut d = DragData::new();
1093 d.set_data("text/plain", b"first".to_vec());
1094 d.set_data("text/plain", b"second".to_vec());
1095 assert_eq!(d.data.len(), 1, "duplicate MIME key was appended");
1096 assert_eq!(d.get_data("text/plain"), Some(&b"second"[..]));
1097 }
1098
1099 #[test]
1100 fn set_data_empty_payload_is_some_empty_not_none() {
1101 let mut d = DragData::new();
1102 d.set_data("application/octet-stream", Vec::new());
1103 assert_eq!(d.get_data("application/octet-stream"), Some(&b""[..]));
1105 assert!(d.get_data("application/octet-stream").is_some());
1106 }
1107
1108 #[test]
1109 fn set_data_huge_payload_round_trips() {
1110 let mut d = DragData::new();
1111 let payload = alloc::vec![0xABu8; 1 << 20]; d.set_data("application/octet-stream", payload);
1113 let got = d.get_data("application/octet-stream").unwrap();
1114 assert_eq!(got.len(), 1 << 20);
1115 assert!(got.iter().all(|b| *b == 0xAB));
1116 }
1117
1118 #[test]
1119 fn set_data_binary_payload_is_not_utf8_validated() {
1120 let mut d = DragData::new();
1121 d.set_data("application/octet-stream", alloc::vec![0xFF, 0x00, 0xFE]);
1122 assert_eq!(d.get_data("application/octet-stream"), Some(&[0xFFu8, 0x00, 0xFE][..]));
1123 }
1124
1125 #[test]
1126 fn set_data_many_distinct_mime_types_all_retrievable() {
1127 let mut d = DragData::new();
1128 for i in 0..1_000usize {
1129 let mut key = String::from("application/x-");
1130 key.push_str(&i.to_string());
1131 d.set_data(key.as_str(), alloc::vec![(i % 251) as u8]);
1132 }
1133 assert_eq!(d.data.len(), 1_000);
1134 assert_eq!(d.get_data("application/x-0"), Some(&[0u8][..]));
1135 assert_eq!(d.get_data("application/x-999"), Some(&[(999 % 251) as u8][..]));
1136 assert!(d.get_data("application/x-1000").is_none());
1137 }
1138
1139 #[test]
1140 fn set_text_get_text_round_trip_unicode() {
1141 for s in [
1142 "hello",
1143 "",
1144 "\u{1F600}\u{1F4A9}",
1145 "e\u{0301}\u{0300}\u{0308}", "\u{200B}zero-width",
1147 "line1\nline2\r\n\ttab",
1148 "\u{0}interior nul\u{0}",
1149 ] {
1150 let mut d = DragData::new();
1151 d.set_text(s);
1152 let got = d.get_text().expect("text/plain must be present");
1153 assert_eq!(got.as_str(), s, "round-trip failed for {s:?}");
1154 }
1155 }
1156
1157 #[test]
1158 fn set_text_empty_is_some_not_none() {
1159 let mut d = DragData::new();
1160 d.set_text("");
1161 assert!(d.get_text().is_some());
1162 assert_eq!(d.get_text().unwrap().as_str(), "");
1163 assert_eq!(d.get_data("text/plain"), Some(&b""[..]));
1164 }
1165
1166 #[test]
1167 fn set_text_huge_string_round_trips() {
1168 let huge: String = "\u{1F600}".repeat(100_000); let mut d = DragData::new();
1170 d.set_text(huge.as_str());
1171 let got = d.get_text().unwrap();
1172 assert_eq!(got.as_str().len(), 400_000);
1173 assert_eq!(got.as_str(), huge.as_str());
1174 assert_eq!(d.data.len(), 1);
1175 }
1176
1177 #[test]
1178 fn set_text_twice_replaces_and_does_not_grow() {
1179 let mut d = DragData::new();
1180 d.set_text("one");
1181 d.set_text("two");
1182 assert_eq!(d.data.len(), 1);
1183 assert_eq!(d.get_text().unwrap().as_str(), "two");
1184 }
1185
1186 #[test]
1187 fn set_text_then_set_data_on_same_mime_wins() {
1188 let mut d = DragData::new();
1189 d.set_text("text");
1190 d.set_data("text/plain", b"raw".to_vec());
1191 assert_eq!(d.data.len(), 1);
1192 assert_eq!(d.get_text().unwrap().as_str(), "raw");
1193 }
1194
1195 #[test]
1196 fn get_text_on_invalid_utf8_yields_empty_string_not_panic() {
1197 let mut d = DragData::new();
1201 d.set_data("text/plain", alloc::vec![0xFF, 0xFE, 0x80]);
1202 let got = d.get_text().expect("entry exists, so Some");
1203 assert_eq!(got.as_str(), "");
1204 assert_eq!(d.get_data("text/plain"), Some(&[0xFFu8, 0xFE, 0x80][..]));
1205 }
1206
1207 #[test]
1208 fn get_text_truncated_utf8_yields_empty_string() {
1209 let mut d = DragData::new();
1210 d.set_data("text/plain", alloc::vec![0xF0, 0x9F, 0x98]);
1212 assert_eq!(d.get_text().unwrap().as_str(), "");
1213 }
1214
1215 #[test]
1216 fn get_text_is_none_when_only_other_mimes_present() {
1217 let mut d = DragData::new();
1218 d.set_data("text/html", b"<b>x</b>".to_vec());
1219 assert!(d.get_text().is_none());
1220 }
1221
1222 #[test]
1225 fn drag_context_new_preserves_session_id_and_is_not_cancelled() {
1226 for sid in [0u64, 1, u64::MAX] {
1227 let ctx = DragContext::new(
1228 ActiveDragType::WindowMove(WindowMoveDrag {
1229 start_position: LogicalPosition::zero(),
1230 current_position: LogicalPosition::zero(),
1231 initial_window_position: WindowPosition::Uninitialized,
1232 }),
1233 sid,
1234 );
1235 assert_eq!(ctx.session_id, sid);
1236 assert!(!ctx.cancelled);
1237 }
1238 }
1239
1240 #[test]
1241 fn text_selection_invariants_at_extremes() {
1242 let pos = LogicalPosition::new(f32::MIN, f32::MAX);
1243 let ctx = DragContext::text_selection(
1244 dom(usize::MAX),
1245 NodeId::new(usize::MAX),
1246 pos,
1247 u64::MAX,
1248 );
1249 let ts = ctx.as_text_selection().expect("must be a text selection");
1250 assert_eq!(ts.dom_id, dom(usize::MAX));
1251 assert_eq!(ts.anchor_ifc_node, NodeId::new(usize::MAX));
1252 assert!(ts.anchor_cursor.is_none());
1253 assert!(ts.auto_scroll_container.is_none());
1254 assert_eq!(ts.auto_scroll_direction, AutoScrollDirection::None);
1255 assert_eq!(ts.start_mouse_position.x.to_bits(), f32::MIN.to_bits());
1258 assert_eq!(ts.start_mouse_position.y.to_bits(), f32::MAX.to_bits());
1259 assert_eq!(
1260 ts.current_mouse_position.x.to_bits(),
1261 ts.start_mouse_position.x.to_bits()
1262 );
1263 assert_eq!(ctx.session_id, u64::MAX);
1264 }
1265
1266 #[test]
1267 fn text_selection_with_nan_position_does_not_panic() {
1268 let ctx = DragContext::text_selection(
1269 dom(0),
1270 NodeId::ZERO,
1271 LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
1272 0,
1273 );
1274 let ts = ctx.as_text_selection().unwrap();
1275 assert!(ts.start_mouse_position.x.is_nan());
1276 assert!(ts.current_mouse_position.x.is_nan());
1277 assert_eq!(ts.start_mouse_position.y, f32::NEG_INFINITY);
1278 }
1279
1280 #[test]
1281 fn scrollbar_thumb_stores_all_float_metrics_verbatim_incl_nan_inf() {
1282 let ctx = DragContext::scrollbar_thumb(
1283 dom(3),
1284 NodeId::new(9),
1285 ScrollbarAxis::Horizontal,
1286 LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
1287 f32::NAN,
1288 f32::INFINITY,
1289 -0.0,
1290 f32::MAX,
1291 0,
1292 );
1293 let sb = ctx.as_scrollbar_thumb().unwrap();
1294 assert_eq!(sb.dom_id, dom(3));
1295 assert_eq!(sb.scroll_container_node, NodeId::new(9));
1296 assert_eq!(sb.axis, ScrollbarAxis::Horizontal);
1297 assert!(sb.start_scroll_offset.is_nan(), "NaN was mangled at construction");
1298 assert_eq!(sb.track_length_px, f32::INFINITY);
1299 assert!(sb.content_length_px.is_sign_negative(), "-0.0 lost its sign");
1300 assert_eq!(sb.content_length_px, 0.0);
1301 assert_eq!(sb.viewport_length_px, f32::MAX);
1302 assert_eq!(sb.current_mouse_position.x, f32::INFINITY);
1303 }
1304
1305 #[test]
1306 fn scrollbar_thumb_all_zero_is_constructible() {
1307 let ctx = vscroll(0.0, 0.0, 0.0, 0.0);
1308 assert!(ctx.is_scrollbar_thumb());
1309 let sb = ctx.as_scrollbar_thumb().unwrap();
1310 assert_eq!(sb.start_scroll_offset, 0.0);
1311 assert_eq!(sb.track_length_px, 0.0);
1312 }
1313
1314 #[test]
1315 fn node_drag_invariants_hold_after_construction() {
1316 let mut data = DragData::new();
1317 data.set_text("payload");
1318 let ctx = DragContext::node_drag(
1319 dom(2),
1320 NodeId::new(usize::MAX),
1321 LogicalPosition::new(-1.5, 2.5),
1322 data,
1323 u64::MAX,
1324 );
1325 let nd = ctx.as_node_drag().unwrap();
1326 assert_eq!(nd.dom_id, dom(2));
1327 assert_eq!(nd.node_id, NodeId::new(usize::MAX));
1328 assert_eq!(nd.start_position, nd.current_position);
1329 assert_eq!(nd.drag_offset, LogicalPosition::zero());
1330 assert!(nd.current_drop_target.into_option().is_none());
1331 assert!(nd.previous_drop_target.into_option().is_none());
1332 assert!(!nd.drop_accepted);
1333 assert_eq!(nd.drop_effect, DropEffect::None);
1334 assert_eq!(nd.drag_data.get_text().unwrap().as_str(), "payload");
1335 }
1336
1337 #[test]
1338 fn node_drag_with_empty_drag_data_is_fine() {
1339 let ctx = DragContext::node_drag(
1340 dom(0),
1341 NodeId::ZERO,
1342 LogicalPosition::new(f32::NAN, f32::NAN),
1343 DragData::new(),
1344 0,
1345 );
1346 let nd = ctx.as_node_drag().unwrap();
1347 assert!(nd.drag_data.data.is_empty());
1348 assert!(nd.start_position.x.is_nan());
1349 assert!(nd.current_position.x.is_nan());
1350 }
1351
1352 #[test]
1353 fn window_move_preserves_initial_window_position_extremes() {
1354 for wp in [
1355 WindowPosition::Uninitialized,
1356 WindowPosition::Initialized(PhysicalPosition {
1357 x: i32::MIN,
1358 y: i32::MAX,
1359 }),
1360 WindowPosition::Initialized(PhysicalPosition { x: 0, y: 0 }),
1361 ] {
1362 let ctx = DragContext::window_move(
1363 LogicalPosition::new(f32::MAX, f32::MIN),
1364 wp,
1365 u64::MAX,
1366 );
1367 let wm = ctx.as_window_move().unwrap();
1368 assert_eq!(wm.initial_window_position, wp);
1369 assert_eq!(wm.start_position.x.to_bits(), f32::MAX.to_bits());
1370 assert_eq!(
1371 wm.current_position.y.to_bits(),
1372 wm.start_position.y.to_bits()
1373 );
1374 }
1375 }
1376
1377 #[test]
1378 fn file_drop_empty_file_list_is_allowed() {
1379 let ctx = DragContext::file_drop(Vec::new(), LogicalPosition::zero(), 0);
1380 let fd = ctx.as_file_drop().unwrap();
1381 assert_eq!(fd.files.len(), 0);
1382 assert!(fd.drop_target.into_option().is_none());
1383 assert_eq!(fd.drop_effect, DropEffect::Copy);
1384 }
1385
1386 #[test]
1387 fn file_drop_unicode_and_pathological_filenames_round_trip() {
1388 let files = alloc::vec![
1389 AzString::from(""),
1390 AzString::from("/tmp/\u{1F4C1}/f\u{0301}ile.txt"),
1391 AzString::from("C:\\Windows\\..\\..\\etc\\passwd"),
1392 AzString::from("a".repeat(4096)),
1393 AzString::from("with\nnewline\tand\u{0}nul"),
1394 ];
1395 let ctx = DragContext::file_drop(files, LogicalPosition::new(1.0, 2.0), 42);
1396 let fd = ctx.as_file_drop().unwrap();
1397 assert_eq!(fd.files.len(), 5);
1398 assert_eq!(fd.files.as_slice()[0].as_str(), "");
1399 assert_eq!(fd.files.as_slice()[3].as_str().len(), 4096);
1400 assert_eq!(fd.files.as_slice()[4].as_str(), "with\nnewline\tand\u{0}nul");
1401 assert_eq!(ctx.session_id, 42);
1402 }
1403
1404 #[test]
1405 fn file_drop_ten_thousand_files_does_not_hang() {
1406 let mut files = Vec::with_capacity(10_000);
1407 for i in 0..10_000usize {
1408 files.push(AzString::from(i.to_string()));
1409 }
1410 let ctx = DragContext::file_drop(files, LogicalPosition::zero(), 1);
1411 assert_eq!(ctx.as_file_drop().unwrap().files.len(), 10_000);
1412 }
1413
1414 fn one_of_each() -> [DragContext; 6] {
1417 [
1418 DragContext::text_selection(dom(0), NodeId::ZERO, LogicalPosition::zero(), 0),
1419 vscroll(0.0, 100.0, 200.0, 100.0),
1420 DragContext::node_drag(
1421 dom(0),
1422 NodeId::ZERO,
1423 LogicalPosition::zero(),
1424 DragData::new(),
1425 0,
1426 ),
1427 DragContext::window_move(
1428 LogicalPosition::zero(),
1429 WindowPosition::Uninitialized,
1430 0,
1431 ),
1432 resize_ctx(),
1433 DragContext::file_drop(Vec::new(), LogicalPosition::zero(), 0),
1434 ]
1435 }
1436
1437 #[test]
1438 fn predicates_are_mutually_exclusive_across_every_variant() {
1439 for (i, ctx) in one_of_each().iter().enumerate() {
1440 let flags = [
1441 ctx.is_text_selection(),
1442 ctx.is_scrollbar_thumb(),
1443 ctx.is_node_drag(),
1444 ctx.is_window_move(),
1445 ctx.is_file_drop(),
1446 ];
1447 let set = flags.iter().filter(|f| **f).count();
1448 if i == 4 {
1449 assert_eq!(set, 0, "WindowResize matched a predicate");
1451 } else {
1452 assert_eq!(set, 1, "variant {i} matched {set} predicates, expected 1");
1453 assert!(flags[if i == 5 { 4 } else { i }], "wrong predicate for {i}");
1454 }
1455 }
1456 }
1457
1458 #[test]
1459 fn as_accessors_return_none_for_every_non_matching_variant() {
1460 for (i, ctx) in one_of_each().iter().enumerate() {
1461 assert_eq!(ctx.as_text_selection().is_some(), i == 0);
1462 assert_eq!(ctx.as_scrollbar_thumb().is_some(), i == 1);
1463 assert_eq!(ctx.as_node_drag().is_some(), i == 2);
1464 assert_eq!(ctx.as_window_move().is_some(), i == 3);
1465 assert_eq!(ctx.as_file_drop().is_some(), i == 5);
1466 }
1467 }
1468
1469 #[test]
1470 fn as_mut_accessors_return_none_for_every_non_matching_variant() {
1471 for (i, ctx) in one_of_each().iter_mut().enumerate() {
1472 assert_eq!(ctx.as_text_selection_mut().is_some(), i == 0);
1473 assert_eq!(ctx.as_scrollbar_thumb_mut().is_some(), i == 1);
1474 assert_eq!(ctx.as_node_drag_mut().is_some(), i == 2);
1475 assert_eq!(ctx.as_file_drop_mut().is_some(), i == 5);
1476 }
1477 }
1478
1479 #[test]
1480 fn as_text_selection_mut_writes_are_visible_through_shared_getter() {
1481 let mut ctx =
1482 DragContext::text_selection(dom(0), NodeId::new(1), LogicalPosition::zero(), 0);
1483 {
1484 let ts = ctx.as_text_selection_mut().unwrap();
1485 ts.auto_scroll_direction = AutoScrollDirection::DownRight;
1486 ts.auto_scroll_container = Some(NodeId::new(77));
1487 }
1488 let ts = ctx.as_text_selection().unwrap();
1489 assert_eq!(ts.auto_scroll_direction, AutoScrollDirection::DownRight);
1490 assert_eq!(ts.auto_scroll_container, Some(NodeId::new(77)));
1491 }
1492
1493 #[test]
1494 fn as_scrollbar_thumb_mut_writes_are_visible_through_shared_getter() {
1495 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1496 ctx.as_scrollbar_thumb_mut().unwrap().start_scroll_offset = f32::NAN;
1497 assert!(ctx.as_scrollbar_thumb().unwrap().start_scroll_offset.is_nan());
1498 }
1499
1500 #[test]
1501 fn as_node_drag_mut_writes_are_visible_through_shared_getter() {
1502 let mut ctx = DragContext::node_drag(
1503 dom(0),
1504 NodeId::ZERO,
1505 LogicalPosition::zero(),
1506 DragData::new(),
1507 0,
1508 );
1509 {
1510 let nd = ctx.as_node_drag_mut().unwrap();
1511 nd.drop_accepted = true;
1512 nd.drop_effect = DropEffect::Move;
1513 nd.current_drop_target = Some(dnid(0, 4)).into();
1514 }
1515 let nd = ctx.as_node_drag().unwrap();
1516 assert!(nd.drop_accepted);
1517 assert_eq!(nd.drop_effect, DropEffect::Move);
1518 assert_eq!(
1519 nd.current_drop_target
1520 .into_option()
1521 .unwrap()
1522 .node
1523 .into_crate_internal(),
1524 Some(NodeId::new(4))
1525 );
1526 }
1527
1528 #[test]
1529 fn as_file_drop_mut_writes_are_visible_through_shared_getter() {
1530 let mut ctx = DragContext::file_drop(Vec::new(), LogicalPosition::zero(), 0);
1531 ctx.as_file_drop_mut().unwrap().drop_effect = DropEffect::Link;
1532 assert_eq!(ctx.as_file_drop().unwrap().drop_effect, DropEffect::Link);
1533 }
1534
1535 #[test]
1538 fn update_position_moves_current_for_every_variant_and_leaves_start_alone() {
1539 let new_pos = LogicalPosition::new(123.5, -456.25);
1540 for (i, mut ctx) in one_of_each().into_iter().enumerate() {
1541 let start_before = ctx.start_position();
1542 ctx.update_position(new_pos);
1543 assert_eq!(ctx.current_position(), new_pos, "variant {i} did not move");
1544 if i == 5 {
1545 assert_eq!(ctx.start_position(), new_pos);
1548 } else {
1549 assert_eq!(ctx.start_position(), start_before, "variant {i} start moved");
1550 }
1551 }
1552 }
1553
1554 #[test]
1555 fn update_position_with_nan_and_inf_is_stored_verbatim() {
1556 for mut ctx in one_of_each() {
1557 ctx.update_position(LogicalPosition::new(f32::NAN, f32::INFINITY));
1558 let cur = ctx.current_position();
1559 assert!(cur.x.is_nan());
1560 assert_eq!(cur.y, f32::INFINITY);
1561
1562 ctx.update_position(LogicalPosition::new(f32::MIN, f32::NEG_INFINITY));
1563 let cur = ctx.current_position();
1564 assert_eq!(cur.x.to_bits(), f32::MIN.to_bits());
1565 assert_eq!(cur.y, f32::NEG_INFINITY);
1566 }
1567 }
1568
1569 #[test]
1570 fn update_position_is_idempotent_and_last_write_wins() {
1571 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1572 for i in 0..1000u32 {
1573 ctx.update_position(LogicalPosition::new(i as f32, -(i as f32)));
1574 }
1575 assert_eq!(ctx.current_position(), LogicalPosition::new(999.0, -999.0));
1576 assert_eq!(ctx.start_position(), LogicalPosition::zero());
1577 }
1578
1579 #[test]
1580 fn window_resize_position_getters_work_without_a_predicate() {
1581 let mut ctx = resize_ctx();
1582 assert_eq!(ctx.start_position(), LogicalPosition::new(1.0, 2.0));
1583 assert_eq!(ctx.current_position(), LogicalPosition::new(1.0, 2.0));
1584 ctx.update_position(LogicalPosition::new(-3.0, -4.0));
1585 assert_eq!(ctx.current_position(), LogicalPosition::new(-3.0, -4.0));
1586 assert_eq!(ctx.start_position(), LogicalPosition::new(1.0, 2.0));
1587 assert!(ctx.as_text_selection().is_none());
1589 assert!(ctx.as_window_move().is_none());
1590 }
1591
1592 #[test]
1595 fn scroll_offset_is_none_for_non_scrollbar_drags() {
1596 for (i, ctx) in one_of_each().iter().enumerate() {
1597 if i == 1 {
1598 continue;
1599 }
1600 assert!(
1601 ctx.calculate_scrollbar_scroll_offset().is_none(),
1602 "variant {i} returned Some"
1603 );
1604 }
1605 }
1606
1607 #[test]
1608 fn scroll_offset_basic_vertical_half_track() {
1609 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1613 ctx.update_position(LogicalPosition::new(0.0, 25.0));
1614 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(50.0));
1615 }
1616
1617 #[test]
1618 fn scroll_offset_horizontal_uses_x_and_ignores_y() {
1619 let mut ctx = DragContext::scrollbar_thumb(
1620 dom(0),
1621 NodeId::new(1),
1622 ScrollbarAxis::Horizontal,
1623 LogicalPosition::zero(),
1624 0.0,
1625 100.0,
1626 200.0,
1627 100.0,
1628 0,
1629 );
1630 ctx.update_position(LogicalPosition::new(0.0, 9999.0));
1632 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1633 ctx.update_position(LogicalPosition::new(25.0, 9999.0));
1634 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(50.0));
1635 }
1636
1637 #[test]
1638 fn scroll_offset_clamps_to_range_on_huge_and_infinite_drags() {
1639 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0); for (y, expect) in [
1641 (1.0e30f32, 100.0f32),
1642 (f32::MAX, 100.0),
1643 (f32::INFINITY, 100.0),
1644 (-1.0e30, 0.0),
1645 (f32::MIN, 0.0),
1646 (f32::NEG_INFINITY, 0.0),
1647 ] {
1648 ctx.update_position(LogicalPosition::new(0.0, y));
1649 assert_eq!(
1650 ctx.calculate_scrollbar_scroll_offset(),
1651 Some(expect),
1652 "y = {y}"
1653 );
1654 }
1655 }
1656
1657 #[test]
1658 fn scroll_offset_result_always_within_range_for_finite_inputs() {
1659 let mut ctx = vscroll(30.0, 80.0, 500.0, 120.0);
1660 let range = 500.0 - 120.0;
1661 for y in [-1e9f32, -1.0, 0.0, 0.5, 1.0, 37.0, 1e9] {
1662 ctx.update_position(LogicalPosition::new(0.0, y));
1663 let off = ctx.calculate_scrollbar_scroll_offset().unwrap();
1664 assert!(
1665 (0.0..=range).contains(&off),
1666 "offset {off} escaped [0, {range}] for y = {y}"
1667 );
1668 }
1669 }
1670
1671 #[test]
1672 fn scroll_offset_out_of_range_start_offset_is_clamped_back_in() {
1673 let ctx = vscroll(9999.0, 100.0, 200.0, 100.0);
1675 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(100.0));
1676 let ctx = vscroll(-9999.0, 100.0, 200.0, 100.0);
1677 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(0.0));
1678 }
1679
1680 #[test]
1681 fn scroll_offset_non_scrollable_content_returns_start_offset_unchanged() {
1682 let mut ctx = vscroll(7.0, 100.0, 50.0, 100.0);
1685 ctx.update_position(LogicalPosition::new(0.0, 1000.0));
1686 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(7.0));
1687
1688 let ctx = vscroll(7.0, 100.0, 100.0, 100.0); assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(7.0));
1690
1691 let ctx = vscroll(7.0, 100.0, 0.0, 0.0); assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(7.0));
1693 }
1694
1695 #[test]
1696 fn scroll_offset_zero_or_negative_track_returns_start_offset() {
1697 for track in [0.0f32, -1.0, -1e30, f32::NEG_INFINITY] {
1698 let mut ctx = vscroll(7.0, track, 200.0, 100.0);
1699 ctx.update_position(LogicalPosition::new(0.0, 50.0));
1700 assert_eq!(
1701 ctx.calculate_scrollbar_scroll_offset(),
1702 Some(7.0),
1703 "track = {track}"
1704 );
1705 }
1706 }
1707
1708 #[test]
1709 fn scroll_offset_negative_content_and_viewport_return_start_offset() {
1710 let mut ctx = vscroll(3.0, 100.0, -200.0, -100.0);
1711 ctx.update_position(LogicalPosition::new(0.0, 50.0));
1712 assert_eq!(ctx.calculate_scrollbar_scroll_offset(), Some(3.0));
1714 }
1715
1716 #[test]
1717 fn scroll_offset_nan_start_offset_propagates_nan_without_panicking() {
1718 let mut ctx = vscroll(f32::NAN, 100.0, 200.0, 100.0);
1719 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1720 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1721 assert!(off.is_nan());
1723 }
1724
1725 #[test]
1726 fn scroll_offset_nan_track_length_propagates_nan_without_panicking() {
1727 let mut ctx = vscroll(0.0, f32::NAN, 200.0, 100.0);
1728 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1729 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1730 assert!(off.is_nan());
1734 }
1735
1736 #[test]
1737 fn scroll_offset_nan_mouse_position_propagates_nan_without_panicking() {
1738 let mut ctx = vscroll(0.0, 100.0, 200.0, 100.0);
1739 ctx.update_position(LogicalPosition::new(0.0, f32::NAN));
1740 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1741 assert!(off.is_nan());
1742 }
1743
1744 #[test]
1745 fn scroll_offset_infinite_content_yields_nan_on_zero_mouse_delta() {
1746 let ctx = vscroll(0.0, 100.0, f32::INFINITY, 100.0);
1750 let off = ctx.calculate_scrollbar_scroll_offset().expect("Some");
1751 assert!(off.is_nan(), "expected the 0 * inf NaN, got {off}");
1752 }
1753
1754 #[test]
1761 #[should_panic]
1762 fn scroll_offset_nan_content_length_panics_in_clamp() {
1763 let mut ctx = vscroll(0.0, 100.0, f32::NAN, 100.0);
1764 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1765 let _ = ctx.calculate_scrollbar_scroll_offset();
1766 }
1767
1768 #[test]
1769 #[should_panic]
1770 fn scroll_offset_nan_viewport_length_panics_in_clamp() {
1771 let mut ctx = vscroll(0.0, 100.0, 200.0, f32::NAN);
1772 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1773 let _ = ctx.calculate_scrollbar_scroll_offset();
1774 }
1775
1776 #[test]
1777 #[should_panic]
1778 fn scroll_offset_infinite_content_and_viewport_panics_in_clamp() {
1779 let mut ctx = vscroll(0.0, 100.0, f32::INFINITY, f32::INFINITY);
1781 ctx.update_position(LogicalPosition::new(0.0, 10.0));
1782 let _ = ctx.calculate_scrollbar_scroll_offset();
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}