1use super::*;
5
6struct OutboundStash {
27 live: bool,
29 payload: Option<crate::drag_payload::DragPayload>,
32 pointer: Option<crate::pointer::PointerInfo>,
44}
45
46thread_local! {
47 static OUTBOUND: std::cell::RefCell<OutboundStash> = const {
48 std::cell::RefCell::new(OutboundStash {
49 live: false,
50 payload: None,
51 pointer: None,
52 })
53 };
54}
55
56fn outbound_begin(payload: crate::drag_payload::DragPayload, pointer: crate::pointer::PointerInfo) {
59 OUTBOUND.with(|s| {
60 let mut s = s.borrow_mut();
61 s.live = true;
62 s.payload = Some(payload);
63 s.pointer = Some(pointer);
64 });
65}
66
67fn outbound_take_if_live() -> Option<crate::drag_payload::DragPayload> {
70 OUTBOUND.with(|s| {
71 let mut s = s.borrow_mut();
72 if s.live { s.payload.take() } else { None }
73 })
74}
75
76fn outbound_pointer_if_live() -> Option<crate::pointer::PointerInfo> {
79 OUTBOUND.with(|s| {
80 let s = s.borrow();
81 if s.live { s.pointer } else { None }
82 })
83}
84
85fn outbound_is_live() -> bool {
89 OUTBOUND.with(|s| s.borrow().live)
90}
91
92fn outbound_restash(payload: crate::drag_payload::DragPayload) {
96 OUTBOUND.with(|s| {
97 let mut s = s.borrow_mut();
98 if s.live {
99 s.payload = Some(payload);
100 }
101 });
102}
103
104#[cfg(test)]
106fn has_outbound_typed() -> bool {
107 OUTBOUND.with(|s| s.borrow().payload.is_some())
108}
109
110fn outbound_end() {
113 OUTBOUND.with(|s| {
114 let mut s = s.borrow_mut();
115 s.live = false;
116 s.payload = None;
117 s.pointer = None;
118 });
119}
120
121fn unknown_external_pointer() -> crate::pointer::PointerInfo {
130 let mut info = crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO);
131 info.kind = teksilo_tokens::PointerKind::Unknown;
132 info
133}
134
135#[derive(Copy, Clone, PartialEq, Eq, Debug)]
137enum DragDrive {
138 Move,
140 Drop,
142}
143
144impl WidgetTree {
145 fn drive_drag_session(
157 &mut self,
158 position: teksilo_canvas::Point,
159 which: DragDrive,
160 ops: &mut dyn crate::window::WindowOps,
161 ) -> Option<crate::drag_payload::DropOutcome> {
162 let pointer = self.active_drag.as_ref().map(|d| d.pointer)?;
163 let saved = std::mem::replace(
164 &mut self.current_input,
165 crate::pointer::InputSnapshot::for_drag_session(pointer),
166 );
167 let outcome = match which {
168 DragDrive::Move => {
169 self.handle_drag_move(position, ops);
170 None
171 }
172 DragDrive::Drop => self.handle_drag_drop(position, ops),
173 };
174 self.current_input = saved;
175 outcome
176 }
177
178 pub(super) fn cleanup_drag_preview(&mut self) {
185 let Some((overlay_id, content_id)) = self
186 .active_drag
187 .as_ref()
188 .map(|drag| (drag.preview_overlay_id, drag.preview_content_id))
189 else {
190 return;
191 };
192 if let Some(overlay_id) = overlay_id {
193 self.overlay_manager.dismiss(overlay_id);
194 }
195 if let Some(content_id) = content_id {
196 self.destroy_subtree(content_id);
197 }
198 }
199
200 pub(super) fn cancel_active_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
205 if self.os_drag_pending.take().is_some() {
208 ops.cancel_os_drag();
209 }
210 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
211 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
215 let saved = self.active_drag.as_ref().map(|d| d.pointer).map(|p| {
223 std::mem::replace(
224 &mut self.current_input,
225 crate::pointer::InputSnapshot::for_drag_session(p),
226 )
227 });
228 self.cleanup_drag_preview();
229 self.active_drag = None;
230 self.os_drop_accepted = None;
231 self.release_drag_capture(source);
232 self.current_cursor = crate::widget::CursorIcon::Default;
233 if let Some(prev) = prev_target {
234 self.fire_on_drag_leave(prev, &mut *ops);
235 }
236 if let Some(src) = source {
237 self.fire_on_drag_ended(src, crate::drag_payload::DropOutcome::Cancelled, &mut *ops);
238 }
239 if let Some(saved) = saved {
240 self.current_input = saved;
241 }
242 }
243
244 pub fn begin_external_drag(
276 &mut self,
277 position: teksilo_canvas::Point,
278 data: crate::drag_payload::ExternalDropData,
279 ops: &mut dyn crate::window::WindowOps,
280 ) {
281 if let Some(pending) = self.os_drag_pending {
286 self.confirm_os_drag(pending, &mut *ops);
287 }
288
289 if self.active_drag.is_some() {
293 self.cancel_active_drag(&mut *ops);
294 }
295
296 if let Some(mut payload) = outbound_take_if_live() {
306 payload.enrich_external_from_mime();
310 let pointer = outbound_pointer_if_live().unwrap_or_else(unknown_external_pointer);
313 self.active_drag = Some(crate::drag_state::DragSession {
314 payload,
315 pointer,
316 source_widget: None,
317 is_external: false,
318 current_position: position,
319 current_target: None,
320 feedback: crate::drag_state::DropFeedback::NoFeedback,
321 preview_content_id: None,
322 preview_overlay_id: None,
323 });
324 self.os_drag_reentered = true;
325 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
326 return;
327 }
328
329 self.active_drag = Some(crate::drag_state::DragSession {
330 payload: crate::drag_payload::DragPayload::external(data),
331 pointer: unknown_external_pointer(),
332 source_widget: None,
333 is_external: true,
334 current_position: position,
335 current_target: None,
336 feedback: crate::drag_state::DropFeedback::NoFeedback,
337 preview_content_id: None,
338 preview_overlay_id: None,
339 });
340 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
343 }
344
345 pub fn update_external_drag(
349 &mut self,
350 position: teksilo_canvas::Point,
351 ops: &mut dyn crate::window::WindowOps,
352 ) {
353 if self.active_drag.as_ref().is_some_and(|d| d.is_external) || self.os_drag_reentered {
357 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
358 }
359 }
360
361 pub fn end_external_drag(
368 &mut self,
369 position: teksilo_canvas::Point,
370 data: crate::drag_payload::ExternalDropData,
371 ops: &mut dyn crate::window::WindowOps,
372 ) {
373 if self.os_drag_reentered {
380 self.os_drag_reentered = false;
381 let outcome = self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
382 if self.outbound_drag_source.is_some()
388 && let Some(in_app @ crate::drag_payload::DropOutcome::InApp { .. }) = outcome
389 {
390 self.reentered_drop_outcome = Some(in_app);
391 }
392 outbound_end();
393 return;
394 }
395 if !self.active_drag.as_ref().is_some_and(|d| d.is_external) {
396 return;
397 }
398 if !data.is_empty()
399 && let Some(drag) = self.active_drag.as_mut()
400 {
401 drag.payload = crate::drag_payload::DragPayload::external(data);
402 }
403 self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
404 }
405
406 pub fn abort_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
423 if self.active_drag.is_none() {
424 self.os_drag_reentered = false;
425 return;
426 }
427 self.os_drag_reentered = false;
428 self.cancel_active_drag(&mut *ops);
429 }
430
431 pub fn cancel_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
436 if self.os_drag_reentered {
441 self.reexit_outbound(&mut *ops);
442 } else if self.active_drag.as_ref().is_some_and(|d| d.is_external) {
443 self.cancel_active_drag(&mut *ops);
444 }
445 }
446
447 pub(super) fn process_drag_tick(&mut self, ops: &mut dyn crate::window::WindowOps) {
452 self.reap_dead_reentered_drag(&mut *ops);
454 let Some((target_id, position, pointer)) = self
455 .active_drag
456 .as_ref()
457 .and_then(|d| d.current_target.map(|t| (t, d.current_position, d.pointer)))
458 else {
459 return;
460 };
461 if !self.arena.is_active(target_id) {
462 return;
463 }
464 let bounds = self.arena.bounds(target_id);
465 let local = teksilo_canvas::Point::new(position.x - bounds.x, position.y - bounds.y);
466 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
467 Some(node) => (
468 node.external_handlers.on_drag_tick.take(),
469 node.handlers.on_drag_tick.take(),
470 ),
471 None => return,
472 };
473 if ext_handler.is_none() && own_handler.is_none() {
474 return;
475 }
476 let saved = std::mem::replace(
481 &mut self.current_input,
482 crate::pointer::InputSnapshot::for_drag_session(pointer),
483 );
484 let mut ctx = self.make_event_context(&mut *ops);
485 if let Some(h) = ext_handler.as_mut() {
486 h(local, &mut ctx);
487 }
488 if let Some(h) = own_handler.as_mut() {
489 h(local, &mut ctx);
490 }
491 if let Some(node) = self.arena.get_mut(target_id) {
492 node.external_handlers.on_drag_tick = ext_handler;
493 node.handlers.on_drag_tick = own_handler;
494 }
495 self.collect_from_ctx(ctx, target_id);
496 if self.active_drag.is_some() {
500 self.handle_drag_move(position, &mut *ops);
501 }
502 self.current_input = saved;
503 }
504
505 fn reap_dead_reentered_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
521 if !self.os_drag_reentered || outbound_is_live() {
522 return;
523 }
524 crate::trace_input!(
525 Gestures,
526 "the OS drag this window held as re-entered has ended elsewhere: dropping the session"
527 );
528 self.abort_external_drag(&mut *ops);
529 }
530
531 pub(super) fn fire_on_drag_leave(
537 &mut self,
538 target_id: WidgetId,
539 ops: &mut dyn crate::window::WindowOps,
540 ) {
541 if !self.arena.is_active(target_id) {
542 return;
543 }
544 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
545 Some(node) => (
546 node.external_handlers.on_drag_leave.take(),
547 node.handlers.on_drag_leave.take(),
548 ),
549 None => return,
550 };
551 if ext_handler.is_none() && own_handler.is_none() {
552 self.arena.mark_needs_paint(target_id);
555 return;
556 }
557 let mut ctx = self.make_event_context(&mut *ops);
558 if let Some(h) = ext_handler.as_mut() {
559 h(&mut ctx);
560 }
561 if let Some(h) = own_handler.as_mut() {
562 h(&mut ctx);
563 }
564 if let Some(node) = self.arena.get_mut(target_id) {
565 node.external_handlers.on_drag_leave = ext_handler;
566 node.handlers.on_drag_leave = own_handler;
567 }
568 self.collect_from_ctx(ctx, target_id);
569 self.arena.mark_needs_paint(target_id);
570 }
571
572 pub(super) fn fire_on_drag_ended(
576 &mut self,
577 source_id: WidgetId,
578 outcome: crate::drag_payload::DropOutcome,
579 ops: &mut dyn crate::window::WindowOps,
580 ) {
581 if !self.arena.is_active(source_id) {
582 return;
583 }
584 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(source_id) {
589 Some(node) => (
590 node.external_handlers.on_drag_ended.take(),
591 node.handlers.on_drag_ended.take(),
592 ),
593 None => return,
594 };
595 if ext_handler.is_none() && own_handler.is_none() {
596 return;
597 }
598 let mut ctx = self.make_event_context(&mut *ops);
599 if let Some(h) = ext_handler.as_mut() {
600 h(outcome, &mut ctx);
601 }
602 if let Some(h) = own_handler.as_mut() {
603 h(outcome, &mut ctx);
604 }
605 if let Some(node) = self.arena.get_mut(source_id) {
606 node.external_handlers.on_drag_ended = ext_handler;
607 node.handlers.on_drag_ended = own_handler;
608 }
609 self.collect_from_ctx(ctx, source_id);
610 }
611
612 fn window_content_size(&self) -> Option<(f32, f32)> {
615 Some((self.last_proposal.width?, self.last_proposal.height?))
616 }
617
618 fn is_outside_window(&self, position: teksilo_canvas::Point) -> bool {
621 match self.window_content_size() {
622 Some((w, h)) => {
623 position.x < 0.0 || position.y < 0.0 || position.x > w || position.y > h
624 }
625 None => false,
626 }
627 }
628
629 fn try_escalate_to_os_drag(
637 &mut self,
638 position: teksilo_canvas::Point,
639 sample: bool,
640 ops: &mut dyn crate::window::WindowOps,
641 ) -> bool {
642 if self.os_drag_reentered {
647 if self.is_outside_window(position) {
648 self.reexit_outbound(&mut *ops);
649 return true;
650 }
651 return false;
652 }
653
654 if self.os_drag_pending.is_some() {
657 return false;
658 }
659
660 let (data, handoff, dragging) = match self.active_drag.as_ref() {
662 Some(d)
663 if !d.is_external && d.source_widget.is_some() && d.payload.is_os_exportable() =>
664 {
665 (d.payload.to_outbound(), d.payload.os_handoff(), d.pointer)
666 }
667 _ => return false,
668 };
669 let eager = handoff == crate::drag_payload::OsHandoff::Eager && sample;
673 if !eager && !self.is_outside_window(position) {
674 return false;
675 }
676
677 match ops.begin_os_drag(data, None, dragging.kind) {
681 crate::drag_payload::OsDragStart::Declined => false,
682 crate::drag_payload::OsDragStart::Pending(drag) => {
683 self.os_drag_pending = Some(drag);
684 false
685 }
686 crate::drag_payload::OsDragStart::Started => {
687 self.hand_session_to_os(None, &mut *ops);
688 true
689 }
690 }
691 }
692
693 pub fn confirm_os_drag(
705 &mut self,
706 drag: crate::drag_payload::OsDragId,
707 ops: &mut dyn crate::window::WindowOps,
708 ) {
709 if self.os_drag_pending == Some(drag) {
710 self.os_drag_pending = None;
711 if self.active_drag.is_none() {
712 ops.cancel_os_drag();
713 return;
714 }
715 self.hand_session_to_os(Some(drag), &mut *ops);
716 return;
717 }
718 if self.os_drag_pending.is_none()
719 && self.outbound_drag_source.is_none()
720 && !self.os_drag_reentered
721 {
722 ops.cancel_os_drag();
723 }
724 }
725
726 fn hand_session_to_os(
729 &mut self,
730 drag: Option<crate::drag_payload::OsDragId>,
731 ops: &mut dyn crate::window::WindowOps,
732 ) {
733 let Some(dragging) = self.active_drag.as_ref().map(|d| d.pointer) else {
734 return;
735 };
736 self.os_drag_pending = None;
737 self.outbound_drag_id = drag;
738 self.reentered_drop_outcome = None;
739 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
744 self.cleanup_drag_preview();
745 let drag = self
746 .active_drag
747 .take()
748 .expect("active_drag present (checked above)");
749 self.os_drop_accepted = None;
750 self.release_drag_capture(drag.source_widget);
751 self.current_cursor = crate::widget::CursorIcon::Default;
752 self.outbound_drag_source = drag.source_widget;
753 outbound_begin(drag.payload, dragging);
754 if let Some(prev) = prev_target {
755 self.fire_on_drag_leave(prev, &mut *ops);
756 }
757 self.cancel_pointer_to(
774 dragging.id,
775 crate::pointer::CancelReason::OsDragStarted,
776 self.outbound_drag_source,
777 &mut *ops,
778 );
779 }
780
781 fn reexit_outbound(&mut self, ops: &mut dyn crate::window::WindowOps) {
785 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
786 self.cleanup_drag_preview();
787 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
788 if let Some(drag) = self.active_drag.take() {
789 outbound_restash(drag.payload);
790 }
791 self.os_drag_reentered = false;
792 self.reentered_drop_outcome = None;
793 self.os_drop_accepted = None;
794 self.release_drag_capture(source);
795 self.current_cursor = crate::widget::CursorIcon::Default;
796 if let Some(prev) = prev_target {
797 self.fire_on_drag_leave(prev, &mut *ops);
798 }
799 }
800
801 pub fn handle_os_drag_ended(
805 &mut self,
806 outcome: crate::drag_payload::DropOutcome,
807 ops: &mut dyn crate::window::WindowOps,
808 ) {
809 self.handle_os_drag_ended_for(None, outcome, ops);
810 }
811
812 pub fn handle_os_drag_ended_for(
825 &mut self,
826 drag: Option<crate::drag_payload::OsDragId>,
827 outcome: crate::drag_payload::DropOutcome,
828 ops: &mut dyn crate::window::WindowOps,
829 ) {
830 let waiting = match drag {
831 Some(id) if self.os_drag_pending == Some(id) => true,
832 Some(id) if self.outbound_drag_id == Some(id) => false,
833 Some(id) => {
834 crate::trace_input!(Gestures, "a stale end of OS drag {} is ignored", id.raw());
835 return;
836 }
837 None => self.os_drag_pending.is_some(),
838 };
839 if waiting {
844 self.os_drag_pending = None;
845 self.reentered_drop_outcome = None;
846 self.cancel_active_drag(&mut *ops);
847 return;
848 }
849 outbound_end();
852 self.os_drag_reentered = false;
853 self.outbound_drag_id = None;
854 let outcome = self.reentered_drop_outcome.take().unwrap_or(outcome);
857 if let Some(source) = self.outbound_drag_source.take() {
858 self.fire_on_drag_ended(source, outcome, &mut *ops);
859 }
860 }
861
862 pub fn abort_outbound_drag(&mut self) {
870 if self.outbound_drag_source.take().is_some() {
871 outbound_end();
872 } else if self.os_drag_reentered
873 && let Some(drag) = self.active_drag.take()
874 {
875 outbound_restash(drag.payload);
876 }
877 self.os_drag_reentered = false;
878 self.os_drag_pending = None;
879 self.outbound_drag_id = None;
880 self.reentered_drop_outcome = None;
881 self.os_drop_accepted = None;
882 }
883
884 pub(super) fn handle_drag_move(
889 &mut self,
890 position: teksilo_canvas::Point,
891 ops: &mut dyn crate::window::WindowOps,
892 ) {
893 self.drag_move(position, false, ops);
894 }
895
896 pub(super) fn handle_drag_sample(
900 &mut self,
901 position: teksilo_canvas::Point,
902 ops: &mut dyn crate::window::WindowOps,
903 ) {
904 self.drag_move(position, true, ops);
905 }
906
907 fn drag_move(
908 &mut self,
909 position: teksilo_canvas::Point,
910 sample: bool,
911 ops: &mut dyn crate::window::WindowOps,
912 ) {
913 if let Some(ref mut drag) = self.active_drag {
915 drag.current_position = position;
916 }
917
918 if self.try_escalate_to_os_drag(position, sample, &mut *ops) {
921 return;
922 }
923
924 let preview_content = self
931 .active_drag
932 .as_ref()
933 .and_then(|d| Some((d.preview_overlay_id?, d.preview_content_id?)));
934 if let Some((overlay_id, content_id)) = preview_content {
935 let dragging = self.active_drag.as_ref().map(|d| d.pointer);
941 let placement = match dragging {
942 Some(pointer) => {
943 crate::overlay::OverlayPlacement::at_pointer_for(position, &pointer)
944 }
945 None => crate::overlay::OverlayPlacement::AtPointer(position),
946 };
947 self.overlay_manager.update_placement(overlay_id, placement);
948 self.arena.mark_needs_layout(content_id);
949 }
950
951 let exclude_overlay = self.active_drag.as_ref().and_then(|d| d.preview_overlay_id);
955 let exclude_widget = self.active_drag.as_ref().and_then(|d| d.preview_content_id);
956 let dragging = self.current_input.pointer;
960 let target =
961 self.hit_test_for_excluding(position, &dragging, exclude_overlay, exclude_widget);
962
963 let mut candidate = target.and_then(|t| self.find_drop_target_at_or_above(t));
971 let mut engaged: Option<WidgetId> = None;
972 let mut engaged_feedback = crate::drag_state::DropFeedback::NoFeedback;
973 let mut bubbled_past: Vec<WidgetId> = Vec::new();
974 while let Some(cand) = candidate {
975 let fb = self.fire_on_drag_hover(cand, position, &mut *ops);
976 if fb.is_engaged() {
977 engaged = Some(cand);
978 engaged_feedback = fb;
979 break;
980 }
981 bubbled_past.push(cand);
982 candidate = self.next_drop_target_above(cand);
983 }
984
985 let (new_target, new_feedback) = if engaged.is_some() {
993 for cand in &bubbled_past {
994 self.fire_on_drag_leave(*cand, &mut *ops);
995 }
996 (engaged, engaged_feedback)
997 } else if let Some((&deepest, rest)) = bubbled_past.split_first() {
998 for cand in rest {
999 self.fire_on_drag_leave(*cand, &mut *ops);
1000 }
1001 (Some(deepest), crate::drag_state::DropFeedback::NoFeedback)
1002 } else {
1003 (None, crate::drag_state::DropFeedback::NoFeedback)
1004 };
1005
1006 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
1012 if prev_target != new_target
1013 && let Some(prev) = prev_target
1014 && !bubbled_past.contains(&prev)
1015 {
1016 self.fire_on_drag_leave(prev, &mut *ops);
1017 }
1018 let engaged_now = new_feedback.is_engaged();
1019 let is_os_drag =
1025 self.os_drag_reentered || self.active_drag.as_ref().is_some_and(|d| d.is_external);
1026 if let Some(ref mut drag) = self.active_drag {
1027 drag.current_target = new_target;
1028 drag.feedback = new_feedback;
1029 }
1030 if is_os_drag && self.os_drop_accepted != Some(engaged_now) {
1041 self.os_drop_accepted = Some(engaged_now);
1042 ops.set_drop_accepted(engaged_now);
1043 }
1044 }
1045
1046 fn fire_on_drag_hover(
1051 &mut self,
1052 target_id: WidgetId,
1053 position: teksilo_canvas::Point,
1054 ops: &mut dyn crate::window::WindowOps,
1055 ) -> crate::drag_state::DropFeedback {
1056 use crate::drag_state::DropFeedback;
1057 let target_bounds = self.arena.bounds(target_id);
1058 let local =
1059 teksilo_canvas::Point::new(position.x - target_bounds.x, position.y - target_bounds.y);
1060 let (mut ext_handler, mut own_handler, has_on_drop) = match self.arena.get_mut(target_id) {
1061 Some(node) => {
1062 let has_on_drop = node.any_handler(|h| h.on_drop.is_some());
1063 let ext = node.external_handlers.on_drag_hover.take();
1064 let own = node.handlers.on_drag_hover.take();
1065 (ext, own, has_on_drop)
1066 }
1067 None => return DropFeedback::NoFeedback,
1068 };
1069 if ext_handler.is_none() && own_handler.is_none() {
1071 return if has_on_drop {
1072 DropFeedback::Accept
1073 } else {
1074 DropFeedback::NoFeedback
1075 };
1076 }
1077 let mut feedback = DropFeedback::NoFeedback;
1078 if self.active_drag.is_some() {
1079 let mut ctx = self.make_event_context(&mut *ops);
1080 if let Some(ref drag) = self.active_drag {
1081 if let Some(h) = ext_handler.as_mut() {
1082 feedback = h(&drag.payload, local, &mut ctx);
1083 }
1084 if let Some(h) = own_handler.as_mut() {
1085 feedback = h(&drag.payload, local, &mut ctx);
1086 }
1087 }
1088 if let Some(node) = self.arena.get_mut(target_id) {
1089 node.external_handlers.on_drag_hover = ext_handler;
1090 node.handlers.on_drag_hover = own_handler;
1091 }
1092 self.collect_from_ctx(ctx, target_id);
1093 self.arena.mark_needs_paint(target_id);
1094 } else if let Some(node) = self.arena.get_mut(target_id) {
1095 node.external_handlers.on_drag_hover = ext_handler;
1096 node.handlers.on_drag_hover = own_handler;
1097 }
1098 feedback
1099 }
1100
1101 fn next_drop_target_above(&self, id: WidgetId) -> Option<WidgetId> {
1104 let parent = self.arena.parent(id)?;
1105 self.find_drop_target_at_or_above(parent)
1106 }
1107
1108 pub(super) fn handle_drag_drop(
1111 &mut self,
1112 position: teksilo_canvas::Point,
1113 ops: &mut dyn crate::window::WindowOps,
1114 ) -> Option<crate::drag_payload::DropOutcome> {
1115 if self.os_drag_pending.is_some() && self.active_drag.is_some() {
1120 self.cancel_active_drag(&mut *ops);
1121 return Some(crate::drag_payload::DropOutcome::Cancelled);
1122 }
1123
1124 self.cleanup_drag_preview();
1126
1127 self.active_drag.as_ref()?;
1128
1129 let mut drop_target = self
1140 .active_drag
1141 .as_ref()
1142 .and_then(|d| d.current_target)
1143 .filter(|&t| self.arena.is_active(t));
1144 if drop_target.is_none() {
1145 let dropping = self.current_input.pointer;
1146 let hit = self.hit_test_for(position, &dropping);
1147 let mut candidate = hit.and_then(|t| self.find_drop_target_at_or_above(t));
1148 while let Some(cand) = candidate {
1149 if self
1150 .fire_on_drag_hover(cand, position, &mut *ops)
1151 .is_engaged()
1152 {
1153 drop_target = Some(cand);
1154 break;
1155 }
1156 self.fire_on_drag_leave(cand, &mut *ops);
1159 candidate = self.next_drop_target_above(cand);
1160 }
1161 }
1162
1163 let drag = self.active_drag.take()?;
1165 self.os_drop_accepted = None;
1166 self.release_drag_capture(drag.source_widget);
1167 self.current_cursor = crate::widget::CursorIcon::Default;
1168 let source = drag.source_widget;
1171 let mut outcome = crate::drag_payload::DropOutcome::Cancelled;
1174
1175 if let Some(prev) = drop_target {
1179 self.fire_on_drag_leave(prev, &mut *ops);
1180 }
1181
1182 if let Some(target_id) = drop_target {
1187 let target_bounds = self.arena.bounds(target_id);
1188 let local = teksilo_canvas::Point::new(
1189 position.x - target_bounds.x,
1190 position.y - target_bounds.y,
1191 );
1192 let (taken_own, taken_ext) = match self.arena.get_mut(target_id) {
1193 Some(node) => {
1194 let own = node.handlers.on_drop.take();
1195 let ext = if own.is_none() {
1196 node.external_handlers.on_drop.take()
1197 } else {
1198 None
1199 };
1200 (own, ext)
1201 }
1202 None => (None, None),
1203 };
1204 let picked = if let Some(h) = taken_own {
1205 Some((h, true))
1206 } else {
1207 taken_ext.map(|h| (h, false))
1208 };
1209 if let Some((mut handler, is_own)) = picked {
1210 let mut ctx = self.make_event_context(&mut *ops);
1211 let accepted = handler(drag.payload, local, &mut ctx);
1212 outcome = crate::drag_payload::DropOutcome::InApp { accepted };
1213 if let Some(node) = self.arena.get_mut(target_id) {
1214 if is_own {
1215 node.handlers.on_drop = Some(handler);
1216 } else {
1217 node.external_handlers.on_drop = Some(handler);
1218 }
1219 }
1220 self.collect_from_ctx(ctx, target_id);
1221 self.arena.mark_needs_paint(target_id);
1222 }
1223 }
1224 if let Some(src) = source {
1228 self.fire_on_drag_ended(src, outcome, &mut *ops);
1229 }
1230 Some(outcome)
1231 }
1232
1233 fn find_drop_target_at_or_above(&self, start: WidgetId) -> Option<WidgetId> {
1236 let mut current = Some(start);
1237 while let Some(id) = current {
1238 if let Some(node) = self.arena.get(id)
1239 && node.any_handler(|h| h.on_drop.is_some() || h.on_drag_hover.is_some())
1240 {
1241 return Some(id);
1242 }
1243 current = self.arena.parent(id);
1244 }
1245 None
1246 }
1247}
1248
1249#[cfg(test)]
1250mod tests {
1251 use super::*;
1252 use crate::test_widgets::{FillWidget, StackWidget};
1253 use crate::widget::CursorIcon;
1254 use crate::widget_builder::WidgetBuilder;
1255
1256 #[test]
1257 fn start_drag_creates_session() {
1258 let mut tree = WidgetTree::new();
1259 let source = tree.add(FillWidget::new().on_tap({
1260 move |_pos, ctx: &mut crate::widget::EventContext| {
1261 ctx.start_drag(
1262 ctx.focus_requests.first().copied().unwrap_or_default(),
1263 crate::drag_payload::DragPayload::typed(42_u32),
1264 );
1265 }
1266 }));
1267 tree.layout(SizeProposal::exact(100.0, 50.0));
1268
1269 let mut ctx = crate::widget::EventContext::new();
1271 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1272 tree.collect_from_ctx(ctx, source);
1273
1274 assert!(tree.active_drag.is_some());
1275 let drag = tree.active_drag.as_ref().unwrap();
1276 assert_eq!(drag.source_widget, Some(source));
1277 assert!(!drag.is_external);
1278 assert!(drag.payload.has_typed::<u32>());
1279 }
1280
1281 #[test]
1282 fn drag_move_updates_position() {
1283 let mut tree = WidgetTree::new();
1284 let source = tree.add(FillWidget::new());
1285 tree.layout(SizeProposal::exact(200.0, 100.0));
1286
1287 let mut ctx = crate::widget::EventContext::new();
1289 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("hello"));
1290 tree.collect_from_ctx(ctx, source);
1291 assert!(tree.active_drag.is_some());
1292
1293 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 30.0)));
1295
1296 let drag = tree.active_drag.as_ref().unwrap();
1297 assert!((drag.current_position.x - 50.0).abs() < 0.01);
1298 assert!((drag.current_position.y - 30.0).abs() < 0.01);
1299 }
1300
1301 #[test]
1302 fn escape_cancels_drag() {
1303 let mut tree = WidgetTree::new();
1304 let source = tree.add(FillWidget::new());
1305 tree.layout(SizeProposal::exact(200.0, 100.0));
1306
1307 let mut ctx = crate::widget::EventContext::new();
1308 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(99_i32));
1309 tree.collect_from_ctx(ctx, source);
1310 assert!(tree.active_drag.is_some());
1311
1312 tree.press_key(Key::Escape, Modifiers::NONE);
1313 assert!(tree.active_drag.is_none(), "drag should be cancelled");
1314 }
1315
1316 #[test]
1317 fn drop_on_target_fires_handler() {
1318 use std::cell::Cell;
1319 use std::rc::Rc;
1320
1321 let dropped = Rc::new(Cell::new(false));
1322 let dropped_value = Rc::new(Cell::new(0_u32));
1323 let d = dropped.clone();
1324 let dv = dropped_value.clone();
1325
1326 let mut tree = WidgetTree::new();
1327 let source = tree.add(FillWidget::new());
1328 let _target = tree.add(FillWidget::new().on_drop(move |mut payload, _pos, _ctx| {
1330 d.set(true);
1331 if let Some(val) = payload.take_typed::<u32>() {
1332 dv.set(val);
1333 }
1334 true
1335 }));
1336 tree.layout(SizeProposal::exact(200.0, 100.0));
1337
1338 let mut ctx = crate::widget::EventContext::new();
1340 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1341 tree.collect_from_ctx(ctx, source);
1342
1343 tree.dispatch_event(WidgetEvent::pointer_up(
1345 Point::new(150.0, 50.0),
1346 PointerButton::Primary,
1347 Modifiers::NONE,
1348 ));
1349
1350 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1351 assert!(dropped.get(), "on_drop should have been called");
1352 assert_eq!(dropped_value.get(), 42);
1353 }
1354
1355 #[test]
1356 fn drop_on_no_target_cancels() {
1357 let mut tree = WidgetTree::new();
1358 let source = tree.add(FillWidget::new());
1359 tree.layout(SizeProposal::exact(100.0, 50.0));
1360
1361 let mut ctx = crate::widget::EventContext::new();
1362 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1363 tree.collect_from_ctx(ctx, source);
1364
1365 tree.dispatch_event(WidgetEvent::pointer_up(
1367 Point::new(999.0, 999.0),
1368 PointerButton::Primary,
1369 Modifiers::NONE,
1370 ));
1371
1372 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1373 }
1374
1375 #[test]
1376 fn drop_falls_through_a_destroyed_current_target() {
1377 use std::cell::Cell;
1382 use std::rc::Rc;
1383
1384 let bg_dropped = Rc::new(Cell::new(false));
1385 let bg_sink = bg_dropped.clone();
1386
1387 let mut tree = WidgetTree::new();
1388 let source = tree.add(FillWidget::new());
1392 let _bg = tree.add(FillWidget::new().on_drop(move |_p, _pos, _ctx| {
1393 bg_sink.set(true);
1394 true
1395 }));
1396 let fg = tree.add(
1399 FillWidget::new()
1400 .on_drag_hover(|_payload, _pos, _ctx| {
1401 crate::drag_state::DropFeedback::InsertionLine {
1402 y: 50.0,
1403 width: 200.0,
1404 }
1405 })
1406 .on_drop(|_, _, _| true),
1407 );
1408 tree.layout(SizeProposal::exact(200.0, 100.0));
1409
1410 let mut ctx = crate::widget::EventContext::new();
1411 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1412 tree.collect_from_ctx(ctx, source);
1413
1414 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1416 assert_eq!(
1417 tree.active_drag.as_ref().unwrap().current_target,
1418 Some(fg),
1419 "fg engaged as the current drop target"
1420 );
1421
1422 tree.arena.destroy(fg);
1424 assert!(!tree.arena.is_active(fg));
1425
1426 tree.dispatch_event(WidgetEvent::pointer_up(
1429 Point::new(100.0, 50.0),
1430 PointerButton::Primary,
1431 Modifiers::NONE,
1432 ));
1433
1434 assert!(tree.active_drag.is_none(), "drag session cleared");
1435 assert!(
1436 bg_dropped.get(),
1437 "drop landed on the live background target, not the destroyed one"
1438 );
1439 }
1440
1441 fn mouse_members(
1447 tree: &WidgetTree,
1448 ) -> Vec<(
1449 WidgetId,
1450 crate::gesture::MemberRole,
1451 crate::gesture::MemberState,
1452 )> {
1453 tree.sequence_members(crate::pointer::PointerId::MOUSE)
1454 }
1455
1456 #[test]
1457 fn drag_arming_walks_to_an_ancestor_without_a_dead_zone() {
1458 use crate::gesture::{MemberRole, MemberState};
1462 let mut tree = WidgetTree::new();
1463 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1464 let inner = tree.add(StackWidget::new().child(button));
1465 let ancestor = tree.add(StackWidget::new().child(inner).on_drag(|_phase, _ctx| {}));
1466 tree.layout(SizeProposal::exact(100.0, 100.0));
1467
1468 let b = tree.bounds(button);
1469 tree.pointer_down_button(
1470 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1471 PointerButton::Primary,
1472 );
1473 assert_eq!(
1474 mouse_members(&tree),
1475 vec![(ancestor, MemberRole::Gesture, MemberState::Possible)],
1476 "the draggable ancestor competes when the button press is not in a dead zone"
1477 );
1478 tree.pointer_up_button(
1479 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1480 PointerButton::Primary,
1481 );
1482 }
1483
1484 #[test]
1485 fn gesture_dead_zone_blocks_ancestor_drag_arming() {
1486 use crate::widget_builder::WidgetBuilder;
1492 let mut tree = WidgetTree::new();
1493 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1494 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1495 let _ancestor = tree.add(
1496 StackWidget::new()
1497 .child(dead_zone)
1498 .on_drag(|_phase, _ctx| {}),
1499 );
1500 tree.layout(SizeProposal::exact(100.0, 100.0));
1501
1502 let b = tree.bounds(button);
1503 tree.pointer_down_button(
1504 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1505 PointerButton::Primary,
1506 );
1507 assert!(
1508 mouse_members(&tree).is_empty(),
1509 "a dead zone blocks the draggable ancestor from competing"
1510 );
1511 tree.pointer_up_button(
1512 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1513 PointerButton::Primary,
1514 );
1515 }
1516
1517 #[test]
1518 fn a_dead_zone_boundary_blocks_a_mouse_exactly_as_it_blocks_a_finger() {
1519 use crate::pointer::{
1524 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
1525 };
1526 use crate::widget_builder::WidgetBuilder;
1527
1528 let mut tree = WidgetTree::new();
1529 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1530 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1531 tree.add(
1532 StackWidget::new()
1533 .child(dead_zone)
1534 .on_drag(|_phase, _ctx| {}),
1535 );
1536 tree.layout(SizeProposal::exact(100.0, 100.0));
1537 let b = tree.bounds(button);
1538 let at = Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
1539
1540 tree.pointer_down_button(at, PointerButton::Primary);
1541 assert!(
1542 mouse_members(&tree).is_empty(),
1543 "the mouse enrols no ancestor across the dead zone"
1544 );
1545 tree.pointer_up_button(at, PointerButton::Primary);
1546
1547 let contact = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 41);
1548 let sample = |phase| PointerSample {
1549 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
1550 phase,
1551 position: at,
1552 button: None,
1553 modifiers: Modifiers::NONE,
1554 coalesced: Vec::new(),
1555 };
1556 tree.dispatch_pointer(sample(PointerPhase::Down));
1557 assert!(
1558 tree.sequence_members(contact).is_empty(),
1559 "and neither does a finger"
1560 );
1561 tree.dispatch_pointer(sample(PointerPhase::Up));
1562 }
1563
1564 #[test]
1565 fn drag_hover_calls_on_drag_hover() {
1566 use std::cell::Cell;
1567 use std::rc::Rc;
1568
1569 let hover_count = Rc::new(Cell::new(0));
1570 let hc = hover_count.clone();
1571
1572 let mut tree = WidgetTree::new();
1573 let source = tree.add(FillWidget::new());
1574 let _target = tree.add(
1575 FillWidget::new()
1576 .on_drag_hover(move |_payload, _pos, _ctx| {
1577 hc.set(hc.get() + 1);
1578 crate::drag_state::DropFeedback::InsertionLine {
1579 y: 50.0,
1580 width: 200.0,
1581 }
1582 })
1583 .on_drop(|_, _, _| true),
1584 );
1585 tree.layout(SizeProposal::exact(200.0, 100.0));
1586
1587 let mut ctx = crate::widget::EventContext::new();
1589 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("test"));
1590 tree.collect_from_ctx(ctx, source);
1591
1592 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1594
1595 assert!(
1596 hover_count.get() > 0,
1597 "on_drag_hover should have been called"
1598 );
1599 }
1600
1601 #[test]
1607 fn rejecter_under_engaging_ancestor_leaves_once() {
1608 use std::cell::Cell;
1609 use std::rc::Rc;
1610
1611 let leaves = Rc::new(Cell::new(0));
1612 let lv = leaves.clone();
1613 let engage = Rc::new(Cell::new(false));
1616 let eg = engage.clone();
1617
1618 let mut tree = WidgetTree::new();
1619 let source = tree.add(FillWidget::new());
1620
1621 let child = tree.add(
1623 FillWidget::new()
1624 .on_drag_hover(|_payload, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1625 .on_drag_leave(move |_ctx| lv.set(lv.get() + 1)),
1626 );
1627 let _ancestor = tree.add(StackWidget::new().child(child).on_drag_hover(
1629 move |_payload, _pos, _ctx| {
1630 if eg.get() {
1631 crate::drag_state::DropFeedback::Accept
1632 } else {
1633 crate::drag_state::DropFeedback::NoFeedback
1634 }
1635 },
1636 ));
1637 tree.layout(SizeProposal::exact(200.0, 100.0));
1638
1639 let mut ctx = crate::widget::EventContext::new();
1640 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1641 tree.collect_from_ctx(ctx, source);
1642
1643 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1645 assert_eq!(
1646 tree.active_drag.as_ref().unwrap().current_target,
1647 Some(child)
1648 );
1649 assert_eq!(leaves.get(), 0, "no leave yet — child is freshly tracked");
1650
1651 engage.set(true);
1653 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(101.0, 50.0)));
1654
1655 assert_eq!(
1656 leaves.get(),
1657 1,
1658 "child must receive exactly one on_drag_leave, not two"
1659 );
1660 }
1661
1662 #[test]
1663 fn drop_outside_window_cancels() {
1664 let mut tree = WidgetTree::new();
1665 let source = tree.add(FillWidget::new());
1666 tree.layout(SizeProposal::exact(100.0, 50.0));
1667
1668 let mut ctx = crate::widget::EventContext::new();
1669 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1670 tree.collect_from_ctx(ctx, source);
1671 assert!(tree.active_drag.is_some());
1672
1673 tree.dispatch_event(WidgetEvent::pointer_up(
1675 Point::new(-100.0, -100.0),
1676 PointerButton::Primary,
1677 Modifiers::NONE,
1678 ));
1679
1680 assert!(tree.active_drag.is_none(), "drag should be cleared");
1681 }
1682
1683 #[test]
1684 fn drop_target_rejects_wrong_type() {
1685 use std::cell::Cell;
1686 use std::rc::Rc;
1687
1688 let accepted = Rc::new(Cell::new(false));
1689 let a = accepted.clone();
1690
1691 let mut tree = WidgetTree::new();
1692 let source = tree.add(FillWidget::new());
1693 let _target = tree.add(
1695 FillWidget::new()
1696 .on_drag_hover(|payload, _pos, _ctx| {
1697 if payload.has_typed::<String>() {
1698 crate::drag_state::DropFeedback::InsertionLine {
1699 y: 0.0,
1700 width: 100.0,
1701 }
1702 } else {
1703 crate::drag_state::DropFeedback::NoFeedback
1704 }
1705 })
1706 .on_drop(move |payload, _pos, _ctx| {
1707 if payload.has_typed::<String>() {
1708 a.set(true);
1709 true
1710 } else {
1711 false
1712 }
1713 }),
1714 );
1715 tree.layout(SizeProposal::exact(200.0, 100.0));
1716
1717 let mut ctx = crate::widget::EventContext::new();
1719 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1720 tree.collect_from_ctx(ctx, source);
1721
1722 tree.dispatch_event(WidgetEvent::pointer_up(
1723 Point::new(150.0, 50.0),
1724 PointerButton::Primary,
1725 Modifiers::NONE,
1726 ));
1727
1728 assert!(!accepted.get(), "on_drop should reject wrong payload type");
1729 }
1730
1731 #[test]
1732 fn inter_widget_drop_transfers_payload() {
1733 use std::cell::Cell;
1734 use std::rc::Rc;
1735
1736 let received_value = Rc::new(Cell::new(0_u32));
1737 let rv = received_value.clone();
1738
1739 let mut tree = WidgetTree::new();
1740 let source = tree.add(FillWidget::new());
1741 let _target = tree.add(
1742 FillWidget::new()
1743 .on_drag_hover(|_payload, _pos, _ctx| {
1744 crate::drag_state::DropFeedback::InsertionLine {
1745 y: 0.0,
1746 width: 100.0,
1747 }
1748 })
1749 .on_drop(move |mut payload, _pos, _ctx| {
1750 if let Some(val) = payload.take_typed::<u32>() {
1751 rv.set(val);
1752 true
1753 } else {
1754 false
1755 }
1756 }),
1757 );
1758 tree.layout(SizeProposal::exact(200.0, 100.0));
1759
1760 let mut ctx = crate::widget::EventContext::new();
1762 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(777_u32));
1763 tree.collect_from_ctx(ctx, source);
1764
1765 tree.dispatch_event(WidgetEvent::pointer_up(
1767 Point::new(150.0, 50.0),
1768 PointerButton::Primary,
1769 Modifiers::NONE,
1770 ));
1771
1772 assert_eq!(
1773 received_value.get(),
1774 777,
1775 "Target should receive the typed payload from source"
1776 );
1777 }
1778
1779 #[test]
1780 fn drop_on_child_walks_up_to_ancestor_drop_target() {
1781 use crate::test_widgets::StackWidget;
1782 use std::cell::Cell;
1783 use std::rc::Rc;
1784
1785 let parent_fired = Rc::new(Cell::new(false));
1788 let pf = parent_fired.clone();
1789
1790 let mut tree = WidgetTree::new();
1791 let source = tree.add(FillWidget::new());
1792 let child = tree.add(FillWidget::new());
1793 let _parent = tree.add(StackWidget::new().child(child).on_drop(
1794 move |_payload, _pos, _ctx| {
1795 pf.set(true);
1796 true
1797 },
1798 ));
1799 tree.layout(SizeProposal::exact(200.0, 100.0));
1800
1801 let mut ctx = crate::widget::EventContext::new();
1803 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
1804 tree.collect_from_ctx(ctx, source);
1805
1806 tree.dispatch_event(WidgetEvent::pointer_up(
1809 Point::new(100.0, 50.0),
1810 PointerButton::Primary,
1811 Modifiers::NONE,
1812 ));
1813
1814 assert!(
1815 parent_fired.get(),
1816 "Parent's on_drop should fire via ancestor walk"
1817 );
1818 }
1819
1820 #[test]
1821 fn drop_bubbles_past_a_rejecting_child_to_ancestor() {
1822 use crate::test_widgets::StackWidget;
1823 use std::cell::Cell;
1824 use std::rc::Rc;
1825
1826 let child_drop = Rc::new(Cell::new(false));
1831 let parent_drop = Rc::new(Cell::new(false));
1832 let cd = child_drop.clone();
1833 let pd = parent_drop.clone();
1834
1835 let mut tree = WidgetTree::new();
1836 let source = tree.add(FillWidget::new());
1837 let child = tree.add(
1838 FillWidget::new()
1839 .on_drag_hover(|_p, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1840 .on_drop(move |_p, _pos, _ctx| {
1841 cd.set(true);
1842 false }),
1844 );
1845 let _parent = tree.add(
1846 StackWidget::new()
1847 .child(child)
1848 .on_drop(move |_p, _pos, _ctx| {
1849 pd.set(true);
1850 true
1851 }),
1852 );
1853 tree.layout(SizeProposal::exact(200.0, 100.0));
1854
1855 let mut ctx = crate::widget::EventContext::new();
1856 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u8));
1857 tree.collect_from_ctx(ctx, source);
1858
1859 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1862 tree.dispatch_event(WidgetEvent::pointer_up(
1863 Point::new(100.0, 50.0),
1864 PointerButton::Primary,
1865 Modifiers::NONE,
1866 ));
1867
1868 assert!(parent_drop.get(), "drop bubbles to the accepting ancestor");
1869 assert!(
1870 !child_drop.get(),
1871 "the rejecting child must not receive the drop"
1872 );
1873 }
1874
1875 #[test]
1876 fn drag_preview_overlay_created_and_dismissed() {
1877 let mut tree = WidgetTree::new();
1878 let source = tree.add(FillWidget::new());
1879 tree.layout(SizeProposal::exact(100.0, 50.0));
1880
1881 let overlay_count_before = tree.overlay_manager().len();
1882
1883 let mut ctx = crate::widget::EventContext::new();
1885 ctx.start_drag_with_preview(
1886 source,
1887 crate::drag_payload::DragPayload::typed(0_u32),
1888 Box::new(FillWidget::new()),
1889 );
1890 tree.collect_from_ctx(ctx, source);
1891
1892 assert!(tree.active_drag.is_some(), "drag session should be active");
1893 assert!(
1894 tree.active_drag
1895 .as_ref()
1896 .unwrap()
1897 .preview_overlay_id
1898 .is_some(),
1899 "preview overlay id should be recorded"
1900 );
1901 assert_eq!(
1902 tree.overlay_manager().len(),
1903 overlay_count_before + 1,
1904 "overlay count should increase by one for the preview"
1905 );
1906
1907 tree.dispatch_event(WidgetEvent::pointer_up(
1909 Point::new(999.0, 999.0),
1910 PointerButton::Primary,
1911 Modifiers::NONE,
1912 ));
1913
1914 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1915 assert_eq!(
1916 tree.overlay_manager().len(),
1917 overlay_count_before,
1918 "preview overlay should be dismissed on drop"
1919 );
1920 }
1921
1922 #[test]
1923 fn drag_preview_teardown_releases_what_its_content_registered() {
1924 #[derive(Debug)]
1925 struct SpinningPreview;
1926 impl Widget for SpinningPreview {
1927 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1928 let _ = ctx.animated_quad(crate::animated_quad::AnimatedQuadKind::SpinnerArc {
1929 period: std::time::Duration::from_secs(1),
1930 arc_fraction: 0.25,
1931 stroke_fraction: 0.12,
1932 color: teksilo_tokens::Color::RED.into(),
1933 });
1934 Vec::new()
1935 }
1936 fn layout_response(
1937 &self,
1938 _: SizeProposal,
1939 _: &crate::widget::LayoutContext,
1940 ) -> crate::widget::LayoutResponse {
1941 teksilo_canvas::Size::new(20.0, 20.0).into()
1942 }
1943 }
1944
1945 let mut tree = WidgetTree::new();
1946 let source = tree.add(FillWidget::new());
1947 tree.layout(SizeProposal::exact(100.0, 50.0));
1948 let mut ctx = crate::widget::EventContext::new();
1949 ctx.start_drag_with_preview(
1950 source,
1951 crate::drag_payload::DragPayload::typed(0_u32),
1952 Box::new(SpinningPreview),
1953 );
1954 tree.collect_from_ctx(ctx, source);
1955 assert_eq!(tree.animated_quad_count(), 1, "the preview's build ran");
1956
1957 tree.dispatch_event(WidgetEvent::pointer_up(
1958 Point::new(999.0, 999.0),
1959 PointerButton::Primary,
1960 Modifiers::NONE,
1961 ));
1962 assert!(tree.active_drag.is_none());
1963 assert_eq!(
1964 tree.animated_quad_count(),
1965 0,
1966 "tearing the preview down must release its animated-quad slot"
1967 );
1968 }
1969
1970 #[test]
1971 fn drag_preview_follows_pointer_position() {
1972 let mut tree = WidgetTree::new();
1973 let source = tree.add(FillWidget::new());
1974 tree.layout(SizeProposal::exact(200.0, 100.0));
1975
1976 let mut ctx = crate::widget::EventContext::new();
1977 ctx.start_drag_with_preview(
1978 source,
1979 crate::drag_payload::DragPayload::typed("p"),
1980 Box::new(FillWidget::new()),
1981 );
1982 tree.collect_from_ctx(ctx, source);
1983
1984 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(73.0, 41.0)));
1985
1986 let drag = tree.active_drag.as_ref().expect("active drag");
1987 assert!(
1988 (drag.current_position.x - 73.0).abs() < 0.01
1989 && (drag.current_position.y - 41.0).abs() < 0.01,
1990 "drag session position should track the pointer"
1991 );
1992
1993 let overlay_id = drag.preview_overlay_id.expect("preview overlay");
1994 let overlay = tree
1995 .overlay_manager()
1996 .overlay(overlay_id)
1997 .expect("overlay looked up by id");
1998 match &overlay.placement {
1999 crate::overlay::OverlayPlacement::AtPointer(p) => {
2000 assert!(
2001 (p.x - 73.0).abs() < 0.01 && (p.y - 41.0).abs() < 0.01,
2002 "preview overlay placement should follow pointer"
2003 );
2004 }
2005 other => panic!("expected AtPointer placement, got {:?}", other),
2006 }
2007 }
2008
2009 #[test]
2010 fn escape_during_hover_dismisses_preview() {
2011 let mut tree = WidgetTree::new();
2012 let source = tree.add(FillWidget::new());
2013 let _target = tree.add(
2014 FillWidget::new()
2015 .on_drag_hover(|_payload, _pos, _ctx| {
2016 crate::drag_state::DropFeedback::InsertionLine {
2017 y: 0.0,
2018 width: 100.0,
2019 }
2020 })
2021 .on_drop(|_, _, _| true),
2022 );
2023 tree.layout(SizeProposal::exact(200.0, 100.0));
2024
2025 let overlay_count_before = tree.overlay_manager().len();
2026
2027 let mut ctx = crate::widget::EventContext::new();
2028 ctx.start_drag_with_preview(
2029 source,
2030 crate::drag_payload::DragPayload::typed(0_u32),
2031 Box::new(FillWidget::new()),
2032 );
2033 tree.collect_from_ctx(ctx, source);
2034
2035 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
2037
2038 assert!(tree.active_drag.is_some());
2039 assert_eq!(tree.overlay_manager().len(), overlay_count_before + 1);
2040
2041 tree.press_key(Key::Escape, Modifiers::NONE);
2043
2044 assert!(tree.active_drag.is_none(), "drag must be cancelled");
2045 assert_eq!(
2046 tree.overlay_manager().len(),
2047 overlay_count_before,
2048 "preview overlay must be dismissed after Escape"
2049 );
2050 }
2051
2052 #[test]
2053 fn active_drag_blocks_on_tap_on_other_widgets() {
2054 use std::cell::Cell;
2055 use std::rc::Rc;
2056
2057 let tap_fired = Rc::new(Cell::new(false));
2062 let tf = tap_fired.clone();
2063
2064 let mut tree = WidgetTree::new();
2065 let source = tree.add(FillWidget::new());
2066 let _other = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
2067 tf.set(true);
2068 }));
2069 tree.layout(SizeProposal::exact(200.0, 100.0));
2070
2071 let mut ctx = crate::widget::EventContext::new();
2072 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2073 tree.collect_from_ctx(ctx, source);
2074
2075 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
2078 tree.dispatch_event(WidgetEvent::pointer_up(
2079 Point::new(150.0, 50.0),
2080 PointerButton::Primary,
2081 Modifiers::NONE,
2082 ));
2083
2084 assert!(
2085 !tap_fired.get(),
2086 "on_tap must not fire during an active drag"
2087 );
2088 }
2089
2090 #[test]
2093 fn on_drag_leave_fires_when_pointer_leaves_target_bounds() {
2094 use crate::test_widgets::InsetWidget;
2100 use std::cell::Cell;
2101 use std::rc::Rc;
2102
2103 let leave = Rc::new(Cell::new(0_u32));
2104 let l = leave.clone();
2105
2106 let mut tree = WidgetTree::new();
2107 let source = tree.add(FillWidget::new());
2108 let target = tree.add(
2109 FillWidget::new()
2110 .on_drag_hover(
2111 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2112 y: 0.0,
2113 width: 10.0,
2114 },
2115 )
2116 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2117 .on_drop(|_, _, _| true),
2118 );
2119 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2120 tree.layout(SizeProposal::exact(200.0, 100.0));
2121
2122 let mut ctx = crate::widget::EventContext::new();
2123 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2124 tree.collect_from_ctx(ctx, source);
2125
2126 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2128 assert_eq!(leave.get(), 0, "no leave yet — target just became active");
2129
2130 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
2135 assert_eq!(
2136 leave.get(),
2137 1,
2138 "on_drag_leave fires when pointer exits the target's bounds"
2139 );
2140
2141 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2143 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2144 assert_eq!(
2145 leave.get(),
2146 1,
2147 "leave fires at most once per leave transition"
2148 );
2149
2150 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
2152 assert_eq!(leave.get(), 2);
2153 }
2154
2155 #[test]
2156 fn on_drag_leave_fires_on_drop() {
2157 use std::cell::Cell;
2158 use std::rc::Rc;
2159
2160 let leave = Rc::new(Cell::new(0_u32));
2161 let l = leave.clone();
2162
2163 let mut tree = WidgetTree::new();
2164 let source = tree.add(FillWidget::new());
2165 let _target = tree.add(
2166 FillWidget::new()
2167 .on_drag_hover(
2168 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2169 y: 0.0,
2170 width: 10.0,
2171 },
2172 )
2173 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2174 .on_drop(|_, _, _| true),
2175 );
2176 tree.layout(SizeProposal::exact(200.0, 100.0));
2177
2178 let mut ctx = crate::widget::EventContext::new();
2179 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2180 tree.collect_from_ctx(ctx, source);
2181 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2182 tree.dispatch_event(WidgetEvent::pointer_up(
2183 Point::new(100.0, 50.0),
2184 PointerButton::Primary,
2185 Modifiers::NONE,
2186 ));
2187
2188 assert_eq!(leave.get(), 1, "on_drag_leave fires exactly once on drop");
2189 }
2190
2191 #[test]
2192 fn on_drag_leave_fires_on_escape_cancel() {
2193 use std::cell::Cell;
2194 use std::rc::Rc;
2195
2196 let leave = Rc::new(Cell::new(0_u32));
2197 let l = leave.clone();
2198
2199 let mut tree = WidgetTree::new();
2200 let source = tree.add(FillWidget::new());
2201 let _target = tree.add(
2202 FillWidget::new()
2203 .on_drag_hover(
2204 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2205 y: 0.0,
2206 width: 10.0,
2207 },
2208 )
2209 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2210 .on_drop(|_, _, _| true),
2211 );
2212 tree.layout(SizeProposal::exact(200.0, 100.0));
2213
2214 let mut ctx = crate::widget::EventContext::new();
2215 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2216 tree.collect_from_ctx(ctx, source);
2217 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2218 tree.press_key(Key::Escape, Modifiers::NONE);
2219
2220 assert_eq!(
2221 leave.get(),
2222 1,
2223 "Escape cancel must fire on_drag_leave on the current target"
2224 );
2225 }
2226
2227 #[test]
2228 fn on_drag_leave_fires_when_source_destroyed_mid_drag() {
2229 use std::cell::Cell;
2230 use std::rc::Rc;
2231
2232 let leave = Rc::new(Cell::new(0_u32));
2233 let l = leave.clone();
2234
2235 let mut tree = WidgetTree::new();
2236 let source = tree.add(FillWidget::new());
2237 let _target = tree.add(
2238 FillWidget::new()
2239 .on_drag_hover(
2240 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2241 y: 0.0,
2242 width: 10.0,
2243 },
2244 )
2245 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2246 .on_drop(|_, _, _| true),
2247 );
2248 tree.layout(SizeProposal::exact(200.0, 100.0));
2249
2250 let mut ctx = crate::widget::EventContext::new();
2251 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2252 tree.collect_from_ctx(ctx, source);
2253 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2254
2255 tree.arena.destroy(source);
2256 tree.layout(SizeProposal::exact(200.0, 100.0));
2259
2260 assert!(
2261 tree.active_drag.is_none(),
2262 "active drag should have been cancelled"
2263 );
2264 assert_eq!(
2265 leave.get(),
2266 1,
2267 "on_drag_leave fires on the drop target when the source is torn down"
2268 );
2269 }
2270
2271 #[test]
2272 fn on_drag_tick_fires_per_layout_pass() {
2273 use std::cell::Cell;
2274 use std::rc::Rc;
2275
2276 let ticks = Rc::new(Cell::new(0_u32));
2277 let t = ticks.clone();
2278
2279 let mut tree = WidgetTree::new();
2280 let source = tree.add(FillWidget::new());
2281 let _target = tree.add(
2282 FillWidget::new()
2283 .on_drag_hover(
2284 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2285 y: 0.0,
2286 width: 10.0,
2287 },
2288 )
2289 .on_drag_tick(move |_pos, _ctx| t.set(t.get() + 1))
2290 .on_drop(|_, _, _| true),
2291 );
2292 tree.layout(SizeProposal::exact(200.0, 100.0));
2293
2294 let mut ctx = crate::widget::EventContext::new();
2295 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2296 tree.collect_from_ctx(ctx, source);
2297 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2299 assert_eq!(ticks.get(), 0, "tick shouldn't have fired yet");
2300
2301 tree.layout(SizeProposal::exact(200.0, 100.0));
2302 assert_eq!(ticks.get(), 1);
2303 tree.layout(SizeProposal::exact(200.0, 100.0));
2304 tree.layout(SizeProposal::exact(200.0, 100.0));
2305 assert_eq!(ticks.get(), 3);
2306
2307 tree.dispatch_event(WidgetEvent::pointer_up(
2309 Point::new(100.0, 50.0),
2310 PointerButton::Primary,
2311 Modifiers::NONE,
2312 ));
2313 let after_drop = ticks.get();
2314 tree.layout(SizeProposal::exact(200.0, 100.0));
2315 tree.layout(SizeProposal::exact(200.0, 100.0));
2316 assert_eq!(
2317 ticks.get(),
2318 after_drop,
2319 "on_drag_tick must not fire after drag ends"
2320 );
2321 }
2322
2323 #[test]
2324 fn on_drag_hover_and_on_drop_receive_widget_local_coordinates() {
2325 use crate::test_widgets::InsetWidget;
2332 use std::cell::Cell;
2333 use std::rc::Rc;
2334
2335 let hover_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2336 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2337 let h = hover_local.clone();
2338 let d = drop_local.clone();
2339
2340 let mut tree = WidgetTree::new();
2341 let source = tree.add(FillWidget::new());
2342 let target = tree.add(
2344 FillWidget::new()
2345 .on_drag_hover(move |_p, pos, _ctx| {
2346 h.set(pos);
2347 crate::drag_state::DropFeedback::InsertionLine {
2348 y: 0.0,
2349 width: 10.0,
2350 }
2351 })
2352 .on_drop(move |_payload, pos, _ctx| {
2353 d.set(pos);
2354 true
2355 }),
2356 );
2357 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2358 tree.layout(SizeProposal::exact(200.0, 100.0));
2359
2360 let mut ctx = crate::widget::EventContext::new();
2361 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2362 tree.collect_from_ctx(ctx, source);
2363
2364 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 60.0)));
2368 let hov = hover_local.get();
2369 assert!(
2370 (hov.x - 60.0).abs() < 0.01 && (hov.y - 20.0).abs() < 0.01,
2371 "on_drag_hover should receive local coords, got {:?}",
2372 hov,
2373 );
2374
2375 tree.dispatch_event(WidgetEvent::pointer_up(
2377 Point::new(110.0, 55.0),
2378 PointerButton::Primary,
2379 Modifiers::NONE,
2380 ));
2381 let drp = drop_local.get();
2382 assert!(
2383 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2384 "on_drop should receive local coords, got {:?}",
2385 drp,
2386 );
2387 }
2388
2389 #[test]
2390 fn active_drag_sets_grabbing_cursor() {
2391 let mut tree = WidgetTree::new();
2396 let source = tree.add(FillWidget::new());
2397 tree.layout(SizeProposal::exact(100.0, 50.0));
2398
2399 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2400
2401 let mut ctx = crate::widget::EventContext::new();
2402 ctx.start_drag_with_preview(
2403 source,
2404 crate::drag_payload::DragPayload::typed(0_u32),
2405 Box::new(FillWidget::new()),
2406 );
2407 tree.collect_from_ctx(ctx, source);
2408 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2409
2410 tree.dispatch_event(WidgetEvent::pointer_up(
2412 Point::new(50.0, 25.0),
2413 PointerButton::Primary,
2414 Modifiers::NONE,
2415 ));
2416 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2417 }
2418
2419 #[test]
2420 fn escape_cancel_resets_cursor() {
2421 let mut tree = WidgetTree::new();
2422 let source = tree.add(FillWidget::new());
2423 tree.layout(SizeProposal::exact(100.0, 50.0));
2424
2425 let mut ctx = crate::widget::EventContext::new();
2426 ctx.start_drag_with_preview(
2427 source,
2428 crate::drag_payload::DragPayload::typed(0_u32),
2429 Box::new(FillWidget::new()),
2430 );
2431 tree.collect_from_ctx(ctx, source);
2432 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2433
2434 tree.press_key(Key::Escape, Modifiers::NONE);
2435 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2436 }
2437
2438 #[test]
2439 fn drag_preview_composite_gets_built() {
2440 use std::cell::Cell;
2446 use std::rc::Rc;
2447
2448 let built = Rc::new(Cell::new(false));
2449 let b = built.clone();
2450
2451 #[derive(Debug)]
2452 struct CheckingWidget {
2453 built: Rc<Cell<bool>>,
2454 }
2455 impl Widget for CheckingWidget {
2456 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2457 self.built.set(true);
2458 Vec::new()
2459 }
2460 fn layout_response(
2461 &self,
2462 _: SizeProposal,
2463 _: &crate::widget::LayoutContext,
2464 ) -> crate::widget::LayoutResponse {
2465 teksilo_canvas::Size::new(50.0, 20.0).into()
2466 }
2467 }
2468
2469 let mut tree = WidgetTree::new();
2470 let source = tree.add(FillWidget::new());
2471 tree.layout(SizeProposal::exact(200.0, 100.0));
2472
2473 let mut ctx = crate::widget::EventContext::new();
2474 ctx.start_drag_with_preview(
2475 source,
2476 crate::drag_payload::DragPayload::typed(0_u32),
2477 Box::new(CheckingWidget { built: b }),
2478 );
2479 tree.collect_from_ctx(ctx, source);
2480
2481 assert!(built.get(), "preview's build() must fire on drag start");
2482 }
2483
2484 #[test]
2485 fn preview_placement_drives_layout_needs() {
2486 let mut tree = WidgetTree::new();
2494 let source = tree.add(FillWidget::new());
2495 tree.layout(SizeProposal::exact(200.0, 200.0));
2496
2497 let mut ctx = crate::widget::EventContext::new();
2498 ctx.start_drag_with_preview(
2499 source,
2500 crate::drag_payload::DragPayload::typed(0_u32),
2501 Box::new(FillWidget::new()),
2502 );
2503 tree.collect_from_ctx(ctx, source);
2504
2505 assert!(
2508 tree.needs_layout(),
2509 "drag start must mark preview content for layout"
2510 );
2511 tree.layout(SizeProposal::exact(200.0, 200.0));
2512 assert!(
2513 !tree.needs_layout(),
2514 "layout should have cleared dirty flag"
2515 );
2516
2517 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(75.0, 120.0)));
2520 assert!(
2521 tree.needs_layout(),
2522 "PointerMove during drag must mark preview for layout"
2523 );
2524 }
2525
2526 #[test]
2527 fn scroll_during_drag_routes_to_drop_target() {
2528 use std::cell::Cell;
2529 use std::rc::Rc;
2530
2531 let scroll_count = Rc::new(Cell::new(0_u32));
2532 let sc = scroll_count.clone();
2533
2534 let mut tree = WidgetTree::new();
2535 let source = tree.add(FillWidget::new());
2536 let _target = tree.add(
2537 FillWidget::new()
2538 .on_drag_hover(
2539 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2540 y: 0.0,
2541 width: 10.0,
2542 },
2543 )
2544 .on_scroll(move |event, _ctx| match event {
2545 WidgetEvent::Scroll { .. } => {
2546 sc.set(sc.get() + 1);
2547 EventResponse::Handled
2548 }
2549 _ => EventResponse::Ignored,
2550 })
2551 .on_drop(|_, _, _| true),
2552 );
2553 tree.layout(SizeProposal::exact(200.0, 100.0));
2554
2555 let mut ctx = crate::widget::EventContext::new();
2556 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2557 tree.collect_from_ctx(ctx, source);
2558 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2560
2561 tree.dispatch_event(WidgetEvent::scroll(
2564 crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
2565 Default::default(),
2566 ));
2567 assert_eq!(
2568 scroll_count.get(),
2569 1,
2570 "Scroll during drag must route to the current drop target"
2571 );
2572 }
2573
2574 #[test]
2577 fn external_drop_delivers_files_and_marks_external() {
2578 use crate::drag_payload::ExternalDropData;
2579 use std::cell::RefCell;
2580 use std::path::PathBuf;
2581 use std::rc::Rc;
2582
2583 let dropped_files: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
2584 let was_external = Rc::new(std::cell::Cell::new(false));
2585 let df = dropped_files.clone();
2586 let we = was_external.clone();
2587
2588 let mut tree = WidgetTree::new();
2589 let _target = tree.add(
2590 FillWidget::new()
2591 .on_drag_hover(|payload, _pos, _ctx| {
2592 if payload.is_external() && !payload.files().is_empty() {
2594 crate::drag_state::DropFeedback::HighlightRect {
2595 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2596 color: teksilo_tokens::Color::WHITE,
2597 }
2598 } else {
2599 crate::drag_state::DropFeedback::NoFeedback
2600 }
2601 })
2602 .on_drop(move |payload, _pos, _ctx| {
2603 we.set(payload.is_external());
2604 *df.borrow_mut() = payload.files().to_vec();
2605 true
2606 }),
2607 );
2608 tree.layout(SizeProposal::exact(200.0, 100.0));
2609
2610 let mut noop = crate::window::NoopWindowOps;
2611 let data = ExternalDropData {
2612 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2613 ..Default::default()
2614 };
2615 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2616 assert!(tree.active_drag.is_some());
2617 assert!(tree.active_drag.as_ref().unwrap().is_external);
2618
2619 tree.update_external_drag(Point::new(110.0, 55.0), &mut noop);
2620 let drop_data = ExternalDropData {
2622 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2623 ..Default::default()
2624 };
2625 tree.end_external_drag(Point::new(110.0, 55.0), drop_data, &mut noop);
2626
2627 assert!(
2628 tree.active_drag.is_none(),
2629 "external drag must clear on drop"
2630 );
2631 assert!(was_external.get(), "payload should report external origin");
2632 assert_eq!(
2633 *dropped_files.borrow(),
2634 vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2635 );
2636 }
2637
2638 #[test]
2639 fn external_drop_passes_local_coordinates() {
2640 use crate::drag_payload::ExternalDropData;
2641 use crate::test_widgets::InsetWidget;
2642 use std::cell::Cell;
2643 use std::path::PathBuf;
2644 use std::rc::Rc;
2645
2646 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2647 let d = drop_local.clone();
2648
2649 let mut tree = WidgetTree::new();
2650 let target = tree.add(
2652 FillWidget::new()
2653 .on_drag_hover(
2654 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2655 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2656 color: teksilo_tokens::Color::WHITE,
2657 },
2658 )
2659 .on_drop(move |_payload, pos, _ctx| {
2660 d.set(pos);
2661 true
2662 }),
2663 );
2664 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2665 tree.layout(SizeProposal::exact(200.0, 100.0));
2666
2667 let mut noop = crate::window::NoopWindowOps;
2668 let data = ExternalDropData {
2669 files: vec![PathBuf::from("/tmp/x")],
2670 ..Default::default()
2671 };
2672 tree.begin_external_drag(Point::new(110.0, 55.0), data, &mut noop);
2674 tree.end_external_drag(
2675 Point::new(110.0, 55.0),
2676 ExternalDropData::default(),
2677 &mut noop,
2678 );
2679
2680 let drp = drop_local.get();
2681 assert!(
2682 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2683 "external on_drop should receive local coords, got {:?}",
2684 drp,
2685 );
2686 }
2687
2688 #[test]
2689 fn cancel_external_drag_clears_session_and_fires_leave() {
2690 use crate::drag_payload::ExternalDropData;
2691 use std::cell::Cell;
2692 use std::path::PathBuf;
2693 use std::rc::Rc;
2694
2695 let left = Rc::new(Cell::new(0_u32));
2696 let l = left.clone();
2697
2698 let mut tree = WidgetTree::new();
2699 let _target = tree.add(
2700 FillWidget::new()
2701 .on_drag_hover(
2702 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2703 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2704 color: teksilo_tokens::Color::WHITE,
2705 },
2706 )
2707 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2708 .on_drop(|_, _, _| true),
2709 );
2710 tree.layout(SizeProposal::exact(200.0, 100.0));
2711
2712 let mut noop = crate::window::NoopWindowOps;
2713 let data = ExternalDropData {
2714 files: vec![PathBuf::from("/tmp/x")],
2715 ..Default::default()
2716 };
2717 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2718 assert!(tree.active_drag.is_some());
2719
2720 tree.cancel_external_drag(&mut noop);
2721 assert!(tree.active_drag.is_none(), "cancel must clear the session");
2722 assert_eq!(
2723 left.get(),
2724 1,
2725 "cancel must fire on_drag_leave on the target"
2726 );
2727 }
2728
2729 #[test]
2730 fn external_drag_helpers_noop_without_session() {
2731 let mut tree = WidgetTree::new();
2733 let _t = tree.add(FillWidget::new());
2734 tree.layout(SizeProposal::exact(100.0, 50.0));
2735
2736 let mut noop = crate::window::NoopWindowOps;
2737 tree.update_external_drag(Point::new(10.0, 10.0), &mut noop);
2738 tree.end_external_drag(
2739 Point::new(10.0, 10.0),
2740 crate::drag_payload::ExternalDropData::default(),
2741 &mut noop,
2742 );
2743 tree.cancel_external_drag(&mut noop);
2744 assert!(tree.active_drag.is_none());
2745 }
2746
2747 struct RecordingWindowOps {
2752 started: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OutboundDragData>>>,
2753 started_kinds: std::rc::Rc<std::cell::RefCell<Vec<teksilo_tokens::PointerKind>>>,
2755 start: crate::drag_payload::OsDragStart,
2758 pending_ids: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OsDragId>>>,
2759 cancels: std::rc::Rc<std::cell::Cell<usize>>,
2760 accepts: std::rc::Rc<std::cell::RefCell<Vec<bool>>>,
2762 }
2763
2764 impl RecordingWindowOps {
2765 fn new(succeed: bool) -> Self {
2766 Self::answering(if succeed {
2767 crate::drag_payload::OsDragStart::Started
2768 } else {
2769 crate::drag_payload::OsDragStart::Declined
2770 })
2771 }
2772
2773 fn answering(start: crate::drag_payload::OsDragStart) -> Self {
2774 Self {
2775 started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2776 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2777 start,
2778 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2779 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2780 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2781 }
2782 }
2783 }
2784 impl crate::window::WindowOps for RecordingWindowOps {
2785 fn open_window(
2786 &mut self,
2787 _c: crate::window::WindowConfig,
2788 ) -> crate::window::TeksiloWindowId {
2789 panic!("not used in these tests")
2790 }
2791 fn find_window(&self, _s: &str) -> Option<crate::window::TeksiloWindowId> {
2792 None
2793 }
2794 fn window_state(
2795 &self,
2796 _id: crate::window::TeksiloWindowId,
2797 ) -> Option<crate::window::WindowState> {
2798 None
2799 }
2800 fn windows(&self) -> Vec<crate::window::WindowState> {
2801 Vec::new()
2802 }
2803 fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
2804 fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
2805 fn begin_os_drag(
2806 &mut self,
2807 data: crate::drag_payload::OutboundDragData,
2808 _image: Option<crate::drag_payload::DragImageData>,
2809 pointer: teksilo_tokens::PointerKind,
2810 ) -> crate::drag_payload::OsDragStart {
2811 self.started.borrow_mut().push(data);
2812 self.started_kinds.borrow_mut().push(pointer);
2813 match self.start {
2814 crate::drag_payload::OsDragStart::Pending(_) => {
2815 let id = crate::drag_payload::OsDragId::next();
2816 self.pending_ids.borrow_mut().push(id);
2817 crate::drag_payload::OsDragStart::Pending(id)
2818 }
2819 other => other,
2820 }
2821 }
2822 fn cancel_os_drag(&mut self) {
2823 self.cancels.set(self.cancels.get() + 1);
2824 }
2825 fn set_drop_accepted(&mut self, accepted: bool) {
2826 self.accepts.borrow_mut().push(accepted);
2827 }
2828 }
2829
2830 fn exportable_payload() -> crate::drag_payload::DragPayload {
2831 crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"hi".to_vec())
2832 }
2833
2834 #[test]
2835 fn internal_exportable_drag_escalates_when_leaving_window() {
2836 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2837 let mut ops = RecordingWindowOps {
2838 started: started.clone(),
2839 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2840 start: crate::drag_payload::OsDragStart::Started,
2841 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2842 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2843 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2844 };
2845
2846 let mut tree = WidgetTree::new();
2847 let source = tree.add(FillWidget::new());
2848 tree.layout(SizeProposal::exact(200.0, 100.0));
2849
2850 let mut ctx = crate::widget::EventContext::new();
2851 ctx.start_drag(source, exportable_payload());
2852 tree.collect_from_ctx(ctx, source);
2853 assert!(tree.active_drag.is_some());
2854
2855 tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
2857 assert!(started.borrow().is_empty());
2858 assert!(tree.active_drag.is_some());
2859
2860 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2862 assert_eq!(started.borrow().len(), 1, "begin_os_drag called once");
2863 assert!(
2864 started.borrow()[0].mime.contains_key("text/plain"),
2865 "outbound data carries the payload's mime"
2866 );
2867 assert!(tree.active_drag.is_none(), "in-app session torn down");
2868 assert_eq!(tree.outbound_drag_source, Some(source));
2869 }
2870
2871 #[test]
2872 fn os_drag_ended_fires_source_on_drag_ended() {
2873 use crate::drag_payload::DropOutcome;
2874 use std::cell::Cell;
2875 use std::rc::Rc;
2876
2877 let outcome = Rc::new(Cell::new(None));
2878 let o = outcome.clone();
2879
2880 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2881 let mut ops = RecordingWindowOps {
2882 started,
2883 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2884 start: crate::drag_payload::OsDragStart::Started,
2885 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2886 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2887 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2888 };
2889
2890 let mut tree = WidgetTree::new();
2891 let source =
2892 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2893 tree.layout(SizeProposal::exact(200.0, 100.0));
2894
2895 let mut ctx = crate::widget::EventContext::new();
2896 ctx.start_drag(source, exportable_payload());
2897 tree.collect_from_ctx(ctx, source);
2898 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2900
2901 tree.handle_os_drag_ended(DropOutcome::OsMove, &mut ops);
2902 assert_eq!(outcome.get(), Some(DropOutcome::OsMove));
2903 assert!(
2904 tree.outbound_drag_source.is_none(),
2905 "cleared after delivery"
2906 );
2907 }
2908
2909 #[test]
2910 fn escape_during_an_escalated_drag_asks_the_platform_to_cancel() {
2911 let mut ops = RecordingWindowOps::new(true);
2916 let cancels = ops.cancels.clone();
2917
2918 let mut tree = WidgetTree::new();
2919 let source = tree.add(FillWidget::new());
2920 tree.layout(SizeProposal::exact(200.0, 100.0));
2921
2922 let mut ctx = crate::widget::EventContext::new();
2923 ctx.start_drag(source, exportable_payload());
2924 tree.collect_from_ctx(ctx, source);
2925 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2927
2928 tree.dispatch_event_with_ops(
2929 crate::event::WidgetEvent::KeyDown {
2930 key: crate::event::Key::Escape,
2931 modifiers: crate::event::Modifiers::NONE,
2932 text: None,
2933 },
2934 &mut ops,
2935 );
2936 assert_eq!(cancels.get(), 1, "the platform must be asked to cancel");
2937 assert_eq!(
2938 tree.outbound_drag_source,
2939 Some(source),
2940 "the session stays until the backend reports its terminal outcome — \
2941 tearing it down here would drop the source's on_drag_ended"
2942 );
2943 }
2944
2945 #[test]
2946 fn escape_without_an_os_drag_does_not_touch_the_platform() {
2947 let mut ops = RecordingWindowOps::new(true);
2948 let cancels = ops.cancels.clone();
2949
2950 let mut tree = WidgetTree::new();
2951 tree.add(FillWidget::new());
2952 tree.layout(SizeProposal::exact(200.0, 100.0));
2953
2954 tree.dispatch_event_with_ops(
2955 crate::event::WidgetEvent::KeyDown {
2956 key: crate::event::Key::Escape,
2957 modifiers: crate::event::Modifiers::NONE,
2958 text: None,
2959 },
2960 &mut ops,
2961 );
2962 assert_eq!(cancels.get(), 0);
2963 }
2964
2965 #[test]
2966 fn no_backend_keeps_session_active_on_leave() {
2967 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2970 let mut ops = RecordingWindowOps {
2971 started: started.clone(),
2972 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2973 start: crate::drag_payload::OsDragStart::Declined,
2974 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2975 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2976 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2977 };
2978
2979 let mut tree = WidgetTree::new();
2980 let source = tree.add(FillWidget::new());
2981 tree.layout(SizeProposal::exact(200.0, 100.0));
2982
2983 let mut ctx = crate::widget::EventContext::new();
2984 ctx.start_drag(source, exportable_payload());
2985 tree.collect_from_ctx(ctx, source);
2986 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2987
2988 assert_eq!(started.borrow().len(), 1, "escalation was attempted");
2989 assert!(tree.active_drag.is_some(), "session kept (no backend)");
2990 assert!(tree.outbound_drag_source.is_none());
2991 }
2992
2993 #[test]
2994 fn non_exportable_drag_does_not_escalate() {
2995 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2996 let mut ops = RecordingWindowOps {
2997 started: started.clone(),
2998 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2999 start: crate::drag_payload::OsDragStart::Started,
3000 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3001 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3002 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3003 };
3004
3005 let mut tree = WidgetTree::new();
3006 let source = tree.add(FillWidget::new());
3007 tree.layout(SizeProposal::exact(200.0, 100.0));
3008
3009 let mut ctx = crate::widget::EventContext::new();
3011 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u32));
3012 tree.collect_from_ctx(ctx, source);
3013 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3014
3015 assert!(started.borrow().is_empty(), "no escalation attempt");
3016 assert!(tree.active_drag.is_some(), "session unaffected");
3017 }
3018
3019 #[test]
3020 fn in_app_drop_fires_source_on_drag_ended_with_accepted() {
3021 use crate::drag_payload::DropOutcome;
3022 use std::cell::Cell;
3023 use std::rc::Rc;
3024
3025 let outcome = Rc::new(Cell::new(None));
3026 let o = outcome.clone();
3027
3028 let mut tree = WidgetTree::new();
3029 let source =
3030 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
3031 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
3032 tree.layout(SizeProposal::exact(200.0, 100.0));
3033
3034 let mut ctx = crate::widget::EventContext::new();
3035 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
3036 tree.collect_from_ctx(ctx, source);
3037
3038 tree.dispatch_event(WidgetEvent::pointer_up(
3039 Point::new(150.0, 50.0),
3040 PointerButton::Primary,
3041 Modifiers::NONE,
3042 ));
3043
3044 assert_eq!(outcome.get(), Some(DropOutcome::InApp { accepted: true }));
3045 }
3046
3047 #[test]
3048 fn escape_fires_source_on_drag_ended_cancelled() {
3049 use crate::drag_payload::DropOutcome;
3050 use std::cell::Cell;
3051 use std::rc::Rc;
3052
3053 let outcome = Rc::new(Cell::new(None));
3054 let o = outcome.clone();
3055
3056 let mut tree = WidgetTree::new();
3057 let source =
3058 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
3059 tree.layout(SizeProposal::exact(200.0, 100.0));
3060
3061 let mut ctx = crate::widget::EventContext::new();
3062 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
3063 tree.collect_from_ctx(ctx, source);
3064
3065 tree.press_key(Key::Escape, Modifiers::NONE);
3066 assert_eq!(outcome.get(), Some(DropOutcome::Cancelled));
3067 }
3068
3069 #[test]
3074 fn os_drag_reentry_recovers_typed_payload_for_in_app_drop() {
3075 use crate::drag_payload::{DragPayload, DropOutcome};
3076 use std::cell::Cell;
3077 use std::rc::Rc;
3078
3079 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3080 let mut ops = RecordingWindowOps {
3081 started,
3082 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3083 start: crate::drag_payload::OsDragStart::Started,
3084 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3085 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3086 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3087 };
3088
3089 let got_typed = Rc::new(Cell::new(0_u32));
3090 let ended = Rc::new(Cell::new(0_u32));
3091 let last_outcome = Rc::new(Cell::new(None));
3092 let g = got_typed.clone();
3093 let e = ended.clone();
3094 let lo = last_outcome.clone();
3095
3096 let mut tree = WidgetTree::new();
3097 let source = tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| {
3098 e.set(e.get() + 1);
3099 lo.set(Some(outcome));
3100 }));
3101 let _target =
3102 tree.add(
3103 FillWidget::new().on_drop(move |mut p, _, _| match p.take_typed::<u32>() {
3104 Some(v) => {
3105 g.set(v);
3106 true
3107 }
3108 None => false,
3109 }),
3110 );
3111 tree.layout(SizeProposal::exact(200.0, 100.0));
3112
3113 let mut ctx = crate::widget::EventContext::new();
3115 ctx.start_drag(
3116 source,
3117 DragPayload::typed(123_u32).with_mime("text/plain", b"x".to_vec()),
3118 );
3119 tree.collect_from_ctx(ctx, source);
3120
3121 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3123 assert!(tree.active_drag.is_none());
3124 assert!(
3125 super::has_outbound_typed(),
3126 "typed payload stashed globally"
3127 );
3128
3129 tree.begin_external_drag(
3132 Point::new(100.0, 50.0),
3133 crate::drag_payload::ExternalDropData::default(),
3134 &mut ops,
3135 );
3136 let d = tree.active_drag.as_ref().expect("re-entered session");
3137 assert!(!d.is_external, "re-entry is an internal session");
3138 assert!(d.payload.has_typed::<u32>(), "typed payload recovered");
3139 assert_eq!(
3140 d.payload.text(),
3141 Some("x"),
3142 "external view enriched from MIME so DropZone-style targets also accept"
3143 );
3144 assert!(!super::has_outbound_typed(), "stash taken by the re-entry");
3145
3146 tree.end_external_drag(
3148 Point::new(150.0, 50.0),
3149 crate::drag_payload::ExternalDropData::default(),
3150 &mut ops,
3151 );
3152 assert_eq!(got_typed.get(), 123, "target received the typed payload");
3153 assert_eq!(
3154 ended.get(),
3155 0,
3156 "source on_drag_ended not fired by the drop itself"
3157 );
3158
3159 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
3164 assert_eq!(ended.get(), 1, "on_drag_ended fired exactly once");
3165 assert_eq!(
3166 last_outcome.get(),
3167 Some(DropOutcome::InApp { accepted: true })
3168 );
3169 }
3170
3171 #[test]
3175 fn os_drag_reentry_recovers_typed_payload_across_windows() {
3176 use crate::drag_payload::{DragPayload, DropOutcome};
3177 use std::cell::Cell;
3178 use std::rc::Rc;
3179
3180 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3181 let mut ops = RecordingWindowOps {
3182 started,
3183 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3184 start: crate::drag_payload::OsDragStart::Started,
3185 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3186 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3187 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3188 };
3189
3190 let mut tree_a = WidgetTree::new();
3192 let src = tree_a.add(FillWidget::new());
3193 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3194 let mut ctx = crate::widget::EventContext::new();
3195 ctx.start_drag(
3196 src,
3197 DragPayload::typed(77_u32).with_mime("text/plain", b"x".to_vec()),
3198 );
3199 tree_a.collect_from_ctx(ctx, src);
3200 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3201 assert!(super::has_outbound_typed());
3202 assert_eq!(tree_a.outbound_drag_source, Some(src));
3203
3204 let got = Rc::new(Cell::new(0_u32));
3207 let g = got.clone();
3208 let mut tree_b = WidgetTree::new();
3209 let _t = tree_b.add(FillWidget::new().on_drop(
3210 move |mut p, _, _| match p.take_typed::<u32>() {
3211 Some(v) => {
3212 g.set(v);
3213 true
3214 }
3215 None => false,
3216 },
3217 ));
3218 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3219
3220 tree_b.begin_external_drag(
3221 Point::new(50.0, 50.0),
3222 crate::drag_payload::ExternalDropData::default(),
3223 &mut ops,
3224 );
3225 assert!(
3226 tree_b
3227 .active_drag
3228 .as_ref()
3229 .is_some_and(|d| d.payload.has_typed::<u32>()),
3230 "window B recovered the typed payload"
3231 );
3232 tree_b.end_external_drag(
3233 Point::new(50.0, 50.0),
3234 crate::drag_payload::ExternalDropData::default(),
3235 &mut ops,
3236 );
3237 assert_eq!(
3238 got.get(),
3239 77,
3240 "window B's target received the typed payload"
3241 );
3242
3243 tree_a.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
3245 }
3246
3247 #[test]
3251 fn os_drag_reexit_restashes_payload() {
3252 use crate::drag_payload::DragPayload;
3253 use std::rc::Rc;
3254
3255 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3256 let mut ops = RecordingWindowOps {
3257 started: started.clone(),
3258 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3259 start: crate::drag_payload::OsDragStart::Started,
3260 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3261 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3262 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3263 };
3264
3265 let mut tree = WidgetTree::new();
3266 let source = tree.add(FillWidget::new());
3267 tree.layout(SizeProposal::exact(200.0, 100.0));
3268 let mut ctx = crate::widget::EventContext::new();
3269 ctx.start_drag(
3270 source,
3271 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3272 );
3273 tree.collect_from_ctx(ctx, source);
3274
3275 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3277 assert_eq!(started.borrow().len(), 1, "OS drag started once");
3278 tree.begin_external_drag(
3279 Point::new(100.0, 50.0),
3280 crate::drag_payload::ExternalDropData::default(),
3281 &mut ops,
3282 );
3283 assert!(tree.os_drag_reentered);
3284 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(!tree.os_drag_reentered, "re-exited");
3287 assert!(tree.active_drag.is_none(), "session torn down on re-exit");
3288 assert!(super::has_outbound_typed(), "payload re-stashed");
3289 assert_eq!(
3290 started.borrow().len(),
3291 1,
3292 "no second OS drag started on re-exit"
3293 );
3294 }
3295
3296 #[test]
3300 fn source_window_close_clears_stash_no_hijack() {
3301 use crate::drag_payload::{DragPayload, ExternalDropData};
3302 use std::path::PathBuf;
3303 use std::rc::Rc;
3304
3305 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3306 let mut ops = RecordingWindowOps {
3307 started,
3308 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3309 start: crate::drag_payload::OsDragStart::Started,
3310 pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3311 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3312 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3313 };
3314
3315 let mut tree = WidgetTree::new();
3316 let source = tree.add(FillWidget::new());
3317 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
3318 tree.layout(SizeProposal::exact(200.0, 100.0));
3319 let mut ctx = crate::widget::EventContext::new();
3320 ctx.start_drag(
3321 source,
3322 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3323 );
3324 tree.collect_from_ctx(ctx, source);
3325 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(super::has_outbound_typed());
3327 assert_eq!(tree.outbound_drag_source, Some(source));
3328
3329 tree.abort_outbound_drag();
3331 assert!(
3332 !super::has_outbound_typed(),
3333 "stash cleared when the source window closes"
3334 );
3335 assert!(tree.outbound_drag_source.is_none());
3336
3337 tree.begin_external_drag(
3340 Point::new(50.0, 50.0),
3341 ExternalDropData {
3342 files: vec![PathBuf::from("/tmp/real")],
3343 ..Default::default()
3344 },
3345 &mut ops,
3346 );
3347 let d = tree.active_drag.as_ref().expect("external session");
3348 assert!(
3349 d.is_external,
3350 "stale stash did not hijack the new external drag"
3351 );
3352 assert!(
3353 !d.payload.has_typed::<u32>(),
3354 "no stale typed payload leaked in"
3355 );
3356 assert_eq!(d.payload.files(), &[PathBuf::from("/tmp/real")]);
3357 }
3358
3359 use std::cell::{Cell, RefCell};
3364 use std::rc::Rc;
3365
3366 fn drag_arming_leaf(
3374 slot: Rc<Cell<Option<WidgetId>>>,
3375 payload: impl Fn() -> crate::drag_payload::DragPayload + 'static,
3376 ) -> impl crate::widget::Widget {
3377 let armed = Cell::new(false);
3378 FillWidget::new().on_pointer_event(move |event, ctx| {
3379 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3380 && !armed.replace(true)
3381 && let Some(id) = slot.get()
3382 {
3383 ctx.start_drag(id, payload());
3384 }
3385 crate::event::EventResponse::Ignored
3386 })
3387 }
3388
3389 fn touch_press(
3391 tree: &mut WidgetTree,
3392 at: Point,
3393 ops: &mut dyn crate::window::WindowOps,
3394 ) -> crate::pointer::PointerId {
3395 use crate::pointer::{
3396 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
3397 };
3398 use std::sync::atomic::{AtomicU64, Ordering};
3399 static NEXT: AtomicU64 = AtomicU64::new(91_000);
3400 let contact = PointerIdAllocator::global().begin(
3401 BackendDeviceKey::DEFAULT,
3402 NEXT.fetch_add(1, Ordering::Relaxed),
3403 );
3404 tree.dispatch_pointer_with_ops(
3405 PointerSample {
3406 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
3407 phase: PointerPhase::Down,
3408 position: at,
3409 button: None,
3410 modifiers: Modifiers::NONE,
3411 coalesced: Vec::new(),
3412 },
3413 ops,
3414 );
3415 contact
3416 }
3417
3418 fn placement_of(
3420 tree: &WidgetTree,
3421 id: crate::overlay::OverlayId,
3422 ) -> crate::overlay::OverlayPlacement {
3423 tree.overlay_manager
3424 .stack
3425 .iter()
3426 .find(|o| o.id == id)
3427 .map(|o| o.placement.clone())
3428 .expect("the overlay is in the stack")
3429 }
3430
3431 #[test]
3439 fn a_drag_tick_reports_the_device_that_started_the_drag() {
3440 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3441 let s = seen.clone();
3442
3443 let mut tree = WidgetTree::new();
3444 let target = tree.add(
3445 FillWidget::new()
3446 .on_drop(|_, _, _| true)
3447 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3448 );
3449 tree.layout(SizeProposal::exact(200.0, 100.0));
3450
3451 let mut ctx = crate::widget::EventContext::new();
3453 ctx.start_drag(target, crate::drag_payload::DragPayload::typed(1_u8));
3454 tree.collect_from_ctx(ctx, target);
3455 tree.handle_drag_move(Point::new(50.0, 50.0), &mut crate::window::NoopWindowOps);
3456 tree.layout(SizeProposal::exact(200.0, 100.0));
3457 assert_eq!(
3458 *seen.borrow(),
3459 vec![teksilo_tokens::PointerKind::Mouse],
3460 "a mouse drag still reports the mouse"
3461 );
3462 tree.cancel_active_drag(&mut crate::window::NoopWindowOps);
3463 seen.borrow_mut().clear();
3464
3465 let mut ops = crate::window::NoopWindowOps;
3468 let s = seen.clone();
3469 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3470 let armed = Cell::new(false);
3471 let s2 = slot.clone();
3472 let mut tree = WidgetTree::new();
3473 let row = tree.add(
3474 FillWidget::new()
3475 .on_pointer_event(move |event, ctx| {
3476 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3477 && !armed.replace(true)
3478 && let Some(id) = s2.get()
3479 {
3480 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(2_u8));
3481 }
3482 crate::event::EventResponse::Ignored
3483 })
3484 .on_drop(|_, _, _| true)
3485 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3486 );
3487 slot.set(Some(row));
3488 tree.layout(SizeProposal::exact(200.0, 100.0));
3489 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3490 assert!(tree.active_drag.is_some(), "the finger armed a drag");
3491 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3492 tree.layout(SizeProposal::exact(200.0, 100.0));
3493 assert_eq!(
3494 *seen.borrow(),
3495 vec![teksilo_tokens::PointerKind::Touch],
3496 "and a finger drag reports the finger, from a tick that has no sample"
3497 );
3498 }
3499
3500 #[test]
3508 fn the_preview_avoids_a_coarse_contact_and_still_pins_a_cursor() {
3509 use crate::overlay::OverlayPlacement;
3510
3511 let mut ops = crate::window::NoopWindowOps;
3512 let at = Point::new(60.0, 40.0);
3513
3514 let mut tree = WidgetTree::new();
3516 let source = tree.add(FillWidget::new());
3517 tree.layout(SizeProposal::exact(200.0, 100.0));
3518 let mut ctx = crate::widget::EventContext::new();
3519 ctx.start_drag_with_preview(
3520 source,
3521 crate::drag_payload::DragPayload::typed(1_u8),
3522 Box::new(FillWidget::new()),
3523 );
3524 tree.collect_from_ctx(ctx, source);
3525 tree.handle_drag_move(at, &mut ops);
3526 let overlay = tree
3527 .active_drag
3528 .as_ref()
3529 .and_then(|d| d.preview_overlay_id)
3530 .expect("the preview overlay exists");
3531 assert!(
3532 matches!(placement_of(&tree, overlay), OverlayPlacement::AtPointer(p) if p == at),
3533 "a mouse keeps AtPointer at the reported point, unchanged",
3534 );
3535
3536 let mut tree = WidgetTree::new();
3539 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3540 let source = tree.add(drag_arming_leaf_with_preview(slot.clone()));
3541 slot.set(Some(source));
3542 tree.layout(SizeProposal::exact(200.0, 100.0));
3543 touch_press(&mut tree, at, &mut ops);
3544 assert_eq!(
3545 tree.active_drag.as_ref().map(|d| d.pointer.kind),
3546 Some(teksilo_tokens::PointerKind::Touch),
3547 "the session recorded the finger",
3548 );
3549 tree.handle_drag_move(at, &mut ops);
3550 let overlay = tree
3551 .active_drag
3552 .as_ref()
3553 .and_then(|d| d.preview_overlay_id)
3554 .expect("the preview overlay exists");
3555 match placement_of(&tree, overlay) {
3556 OverlayPlacement::AtPointerAvoiding { point, avoid } => {
3557 assert_eq!(point, at);
3558 assert!(
3559 avoid.contains(at),
3560 "the rectangle to clear is centred on the contact, so no \
3561 placement that honours it can put the preview under the hand",
3562 );
3563 }
3564 other => panic!("a coarse pointer must avoid its own contact, got {other:?}"),
3565 }
3566 }
3567
3568 fn drag_arming_leaf_with_preview(
3571 slot: Rc<Cell<Option<WidgetId>>>,
3572 ) -> impl crate::widget::Widget {
3573 let armed = Cell::new(false);
3574 FillWidget::new().on_pointer_event(move |event, ctx| {
3575 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3576 && !armed.replace(true)
3577 && let Some(id) = slot.get()
3578 {
3579 ctx.start_drag_with_preview(
3580 id,
3581 crate::drag_payload::DragPayload::typed(1_u8),
3582 Box::new(FillWidget::new()),
3583 );
3584 }
3585 crate::event::EventResponse::Ignored
3586 })
3587 }
3588
3589 #[test]
3598 fn an_os_drag_started_by_a_finger_cancels_that_finger() {
3599 let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
3600 Rc::new(RefCell::new(Vec::new()));
3601 let c = cancelled.clone();
3602
3603 let mut ops = RecordingWindowOps::new(true);
3604 let kinds = ops.started_kinds.clone();
3605
3606 let mut tree = WidgetTree::new();
3607 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3608 let armed = Cell::new(false);
3609 let s2 = slot.clone();
3610 let source = tree.add(
3611 FillWidget::new()
3612 .on_pointer_event(move |event, ctx| {
3613 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3614 && !armed.replace(true)
3615 && let Some(id) = s2.get()
3616 {
3617 ctx.start_drag(id, exportable_payload());
3618 }
3619 crate::event::EventResponse::Ignored
3620 })
3621 .on_pointer_cancel(move |pointer, reason, _ctx| {
3622 c.borrow_mut().push((pointer.id, reason));
3623 }),
3624 );
3625 slot.set(Some(source));
3626 tree.layout(SizeProposal::exact(200.0, 100.0));
3627
3628 let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
3629 assert!(tree.active_drag.is_some());
3630
3631 tree.handle_drag_move(Point::new(-40.0, 50.0), &mut ops);
3633 assert_eq!(tree.outbound_drag_source, Some(source));
3634 assert_eq!(
3635 *kinds.borrow(),
3636 vec![teksilo_tokens::PointerKind::Touch],
3637 "the platform is told which device is dragging — Wayland needs the \
3638 touch-down serial, not a button serial"
3639 );
3640 assert_eq!(
3641 *cancelled.borrow(),
3642 vec![(contact, crate::pointer::CancelReason::OsDragStarted)],
3643 "the finger is the pointer revoked"
3644 );
3645 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3646 }
3647
3648 #[test]
3655 fn an_escape_cancelled_finger_drag_reports_the_finger_to_its_source() {
3656 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3657 let s = seen.clone();
3658
3659 let mut ops = crate::window::NoopWindowOps;
3660 let mut tree = WidgetTree::new();
3661 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3662 let armed = Cell::new(false);
3663 let s2 = slot.clone();
3664 let source = tree.add(
3665 FillWidget::new()
3666 .on_pointer_event(move |event, ctx| {
3667 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3668 && !armed.replace(true)
3669 && let Some(id) = s2.get()
3670 {
3671 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(1_u8));
3672 }
3673 crate::event::EventResponse::Ignored
3674 })
3675 .on_drag_ended(move |_outcome, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3676 );
3677 slot.set(Some(source));
3678 tree.layout(SizeProposal::exact(200.0, 100.0));
3679
3680 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3681 assert!(tree.active_drag.is_some());
3682 tree.dispatch_event_with_ops(
3683 crate::event::WidgetEvent::KeyDown {
3684 key: crate::event::Key::Escape,
3685 modifiers: crate::event::Modifiers::NONE,
3686 text: None,
3687 },
3688 &mut ops,
3689 );
3690 assert_eq!(*seen.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
3691 }
3692
3693 #[test]
3699 fn the_accept_setter_receives_the_widget_verdict() {
3700 use crate::drag_state::DropFeedback;
3701
3702 let verdict = Rc::new(Cell::new(false));
3703 let v = verdict.clone();
3704 let mut ops = RecordingWindowOps::new(true);
3705 let accepts = ops.accepts.clone();
3706
3707 let mut tree = WidgetTree::new();
3708 tree.add(
3709 FillWidget::new()
3710 .on_drag_hover(move |_p, _pos, _ctx| {
3711 if v.get() {
3712 DropFeedback::Accept
3713 } else {
3714 DropFeedback::NoFeedback
3715 }
3716 })
3717 .on_drop(|_, _, _| true),
3718 );
3719 tree.layout(SizeProposal::exact(200.0, 100.0));
3720
3721 tree.begin_external_drag(
3722 Point::new(50.0, 50.0),
3723 crate::drag_payload::ExternalDropData {
3724 files: vec![std::path::PathBuf::from("/tmp/a.png")],
3725 ..Default::default()
3726 },
3727 &mut ops,
3728 );
3729 assert_eq!(
3730 *accepts.borrow(),
3731 vec![false],
3732 "a refusing target is reported to the OS as a refusal"
3733 );
3734
3735 tree.update_external_drag(Point::new(52.0, 50.0), &mut ops);
3738 assert_eq!(
3739 *accepts.borrow(),
3740 vec![false],
3741 "an unchanged answer is not re-sent"
3742 );
3743
3744 verdict.set(true);
3746 tree.update_external_drag(Point::new(54.0, 50.0), &mut ops);
3747 assert_eq!(
3748 *accepts.borrow(),
3749 vec![false, true],
3750 "and a change is pushed once"
3751 );
3752 }
3753
3754 #[test]
3757 fn an_in_app_drag_pushes_no_os_accept_state() {
3758 let mut ops = RecordingWindowOps::new(true);
3759 let accepts = ops.accepts.clone();
3760
3761 let mut tree = WidgetTree::new();
3762 let source = tree.add(FillWidget::new());
3763 tree.add(FillWidget::new().on_drop(|_, _, _| true));
3764 tree.layout(SizeProposal::exact(200.0, 100.0));
3765
3766 let mut ctx = crate::widget::EventContext::new();
3767 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
3768 tree.collect_from_ctx(ctx, source);
3769 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3770
3771 assert!(accepts.borrow().is_empty());
3772 }
3773
3774 #[test]
3783 fn a_cancelled_os_drag_tears_the_source_down_exactly_once() {
3784 use crate::drag_payload::{DragPayload, DropOutcome};
3785
3786 let ended = Rc::new(RefCell::new(Vec::new()));
3787 let e = ended.clone();
3788 let mut ops = RecordingWindowOps::new(true);
3789
3790 let mut tree_a = WidgetTree::new();
3792 let src = tree_a.add(
3793 FillWidget::new().on_drag_ended(move |outcome, _ctx| e.borrow_mut().push(outcome)),
3794 );
3795 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3796 let mut ctx = crate::widget::EventContext::new();
3797 ctx.start_drag(
3798 src,
3799 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3800 );
3801 tree_a.collect_from_ctx(ctx, src);
3802 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3803 assert!(super::outbound_is_live());
3804
3805 let left = Rc::new(Cell::new(0_u32));
3807 let l = left.clone();
3808 let mut tree_b = WidgetTree::new();
3809 tree_b.add(
3810 FillWidget::new()
3811 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3812 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3813 .on_drop(|_, _, _| true),
3814 );
3815 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3816 let accepts = ops.accepts.clone();
3817 accepts.borrow_mut().clear();
3818 tree_b.begin_external_drag(
3819 Point::new(50.0, 50.0),
3820 crate::drag_payload::ExternalDropData::default(),
3821 &mut ops,
3822 );
3823 assert!(tree_b.os_drag_reentered);
3824 assert_eq!(
3825 *accepts.borrow(),
3826 vec![true],
3827 "a re-entered app drag still negotiates with the OS: its offer is \
3828 live and a refusal must still reach the compositor's cursor",
3829 );
3830
3831 tree_b.abort_external_drag(&mut ops);
3834 assert!(tree_b.active_drag.is_none(), "B's session is gone");
3835 assert!(!tree_b.os_drag_reentered);
3836 assert_eq!(left.get(), 1, "B's highlighted target was cleared");
3837 assert!(
3838 ended.borrow().is_empty(),
3839 "the abort must not fire the source's on_drag_ended — the source \
3840 window owns that"
3841 );
3842
3843 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3845 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
3846
3847 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3849 assert_eq!(
3850 *ended.borrow(),
3851 vec![DropOutcome::Cancelled],
3852 "exactly once"
3853 );
3854 }
3855
3856 #[test]
3866 fn a_reentered_session_is_reaped_when_the_os_drag_ends_elsewhere() {
3867 use crate::drag_payload::{DragPayload, DropOutcome};
3868
3869 let mut ops = RecordingWindowOps::new(true);
3870
3871 let mut tree_a = WidgetTree::new();
3872 let src = tree_a.add(FillWidget::new());
3873 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3874 let mut ctx = crate::widget::EventContext::new();
3875 ctx.start_drag(
3876 src,
3877 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3878 );
3879 tree_a.collect_from_ctx(ctx, src);
3880 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3881
3882 let left = Rc::new(Cell::new(0_u32));
3883 let l = left.clone();
3884 let mut tree_b = WidgetTree::new();
3885 tree_b.add(
3886 FillWidget::new()
3887 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3888 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3889 .on_drop(|_, _, _| true),
3890 );
3891 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3892 tree_b.begin_external_drag(
3893 Point::new(50.0, 50.0),
3894 crate::drag_payload::ExternalDropData::default(),
3895 &mut ops,
3896 );
3897 assert!(tree_b.active_drag.is_some() && tree_b.os_drag_reentered);
3898
3899 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3901 assert!(
3902 tree_b.active_drag.is_some(),
3903 "B has not been told anything yet"
3904 );
3905
3906 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3908 assert!(tree_b.active_drag.is_none(), "the dead session was reaped");
3909 assert!(!tree_b.os_drag_reentered);
3910 assert_eq!(left.get(), 1, "and its highlighted target was cleared");
3911 }
3912
3913 #[test]
3917 fn a_reentered_app_drag_recovers_the_device_that_started_it() {
3918 let mut ops = RecordingWindowOps::new(true);
3919
3920 let mut tree_a = WidgetTree::new();
3921 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3922 let src = tree_a.add(drag_arming_leaf(slot.clone(), exportable_payload));
3923 slot.set(Some(src));
3924 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3925 touch_press(&mut tree_a, Point::new(20.0, 50.0), &mut ops);
3926 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3927 assert!(super::outbound_is_live());
3928
3929 let mut tree_b = WidgetTree::new();
3930 tree_b.add(FillWidget::new().on_drop(|_, _, _| true));
3931 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3932 tree_b.begin_external_drag(
3933 Point::new(50.0, 50.0),
3934 crate::drag_payload::ExternalDropData::default(),
3935 &mut ops,
3936 );
3937 assert_eq!(
3938 tree_b.active_drag.as_ref().map(|d| d.pointer.kind),
3939 Some(teksilo_tokens::PointerKind::Touch),
3940 );
3941
3942 let mut tree_c = WidgetTree::new();
3944 tree_c.add(FillWidget::new().on_drop(|_, _, _| true));
3945 tree_c.layout(SizeProposal::exact(200.0, 100.0));
3946 tree_a.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3947 tree_c.begin_external_drag(
3948 Point::new(50.0, 50.0),
3949 crate::drag_payload::ExternalDropData::default(),
3950 &mut ops,
3951 );
3952 assert_eq!(
3953 tree_c.active_drag.as_ref().map(|d| d.pointer.kind),
3954 Some(teksilo_tokens::PointerKind::Unknown),
3955 "no OS names the source's device to a destination",
3956 );
3957 }
3958
3959 #[test]
3963 fn restash_after_drag_ended_is_noop() {
3964 use crate::drag_payload::DragPayload;
3965
3966 super::outbound_begin(
3967 DragPayload::typed(1_u32).with_mime("text/plain", b"x".to_vec()),
3968 crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO),
3969 );
3970 assert!(super::has_outbound_typed());
3971 let held = super::outbound_take_if_live().expect("payload taken while live");
3973 assert!(!super::has_outbound_typed());
3974 super::outbound_end();
3976 super::outbound_restash(held);
3978 assert!(
3979 !super::has_outbound_typed(),
3980 "ended drag is not resurrected by a racing re-stash"
3981 );
3982 assert!(super::outbound_take_if_live().is_none());
3984 }
3985
3986 fn pending() -> crate::drag_payload::OsDragStart {
3991 crate::drag_payload::OsDragStart::Pending(crate::drag_payload::OsDragId::next())
3992 }
3993
3994 fn last_pending(ops: &RecordingWindowOps) -> crate::drag_payload::OsDragId {
3996 *ops.pending_ids.borrow().last().expect("a pending start")
3997 }
3998
3999 fn eager_payload() -> crate::drag_payload::DragPayload {
4000 exportable_payload().with_os_handoff(crate::drag_payload::OsHandoff::Eager)
4001 }
4002
4003 fn tree_dragging(
4006 payload: crate::drag_payload::DragPayload,
4007 ) -> (
4008 WidgetTree,
4009 WidgetId,
4010 Rc<RefCell<Vec<crate::drag_payload::DropOutcome>>>,
4011 ) {
4012 let ended: Rc<RefCell<Vec<crate::drag_payload::DropOutcome>>> =
4013 Rc::new(RefCell::new(Vec::new()));
4014 let e = ended.clone();
4015 let mut tree = WidgetTree::new();
4016 let source =
4017 tree.add(FillWidget::new().on_drag_ended(move |o, _ctx| e.borrow_mut().push(o)));
4018 tree.layout(SizeProposal::exact(200.0, 100.0));
4019 let mut ctx = crate::widget::EventContext::new();
4020 ctx.start_drag(source, payload);
4021 tree.collect_from_ctx(ctx, source);
4022 assert!(tree.active_drag.is_some());
4023 (tree, source, ended)
4024 }
4025
4026 #[test]
4027 fn an_eager_drag_hands_off_on_the_first_pointer_move_inside_the_window() {
4028 let mut ops = RecordingWindowOps::new(true);
4029 let started = ops.started.clone();
4030 let (mut tree, source, ended) = tree_dragging(eager_payload());
4031
4032 tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(100.0, 50.0)), &mut ops);
4034 assert_eq!(started.borrow().len(), 1, "begin_os_drag asked once");
4035 assert!(tree.active_drag.is_none(), "in-app session handed over");
4036 assert_eq!(tree.outbound_drag_source, Some(source));
4037 assert!(super::has_outbound_typed(), "typed payload parked");
4038
4039 tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(120.0, 50.0)), &mut ops);
4041 assert_eq!(started.borrow().len(), 1);
4042 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::OsCopy, &mut ops);
4043 assert_eq!(
4044 *ended.borrow(),
4045 vec![crate::drag_payload::DropOutcome::OsCopy]
4046 );
4047 }
4048
4049 #[test]
4050 fn an_at_window_edge_drag_stays_in_app_while_the_pointer_is_inside() {
4051 let mut ops = RecordingWindowOps::new(true);
4052 let started = ops.started.clone();
4053 let (mut tree, source, _ended) = tree_dragging(exportable_payload());
4054 tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(100.0, 50.0)), &mut ops);
4055 assert!(started.borrow().is_empty());
4056 assert!(tree.active_drag.is_some());
4057 tree.handle_drag_sample(Point::new(-5.0, 50.0), &mut ops);
4058 assert_eq!(started.borrow().len(), 1, "the window edge still hands off");
4059 assert_eq!(tree.outbound_drag_source, Some(source));
4060 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4061 }
4062
4063 #[test]
4064 fn an_eager_drag_waits_for_a_pointer_sample_rather_than_a_drag_tick() {
4065 let mut ops = RecordingWindowOps::new(true);
4066 let started = ops.started.clone();
4067 let (mut tree, _source, _ended) = tree_dragging(eager_payload());
4068
4069 tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
4072 tree.layout(SizeProposal::exact(200.0, 100.0));
4073 assert!(started.borrow().is_empty(), "no handoff from a tick");
4074 assert!(tree.active_drag.is_some());
4075
4076 tree.handle_drag_sample(Point::new(101.0, 50.0), &mut ops);
4077 assert_eq!(started.borrow().len(), 1, "the next sample hands off");
4078 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4079 }
4080
4081 #[test]
4082 fn a_refused_eager_handoff_is_asked_again_on_the_next_sample() {
4083 let mut ops = RecordingWindowOps::new(false);
4084 let started = ops.started.clone();
4085 let (mut tree, source, _ended) = tree_dragging(eager_payload());
4086
4087 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4088 assert_eq!(started.borrow().len(), 1);
4089 assert!(
4090 tree.active_drag.is_some(),
4091 "a refusal keeps the in-app drag"
4092 );
4093 tree.handle_drag_sample(Point::new(101.0, 50.0), &mut ops);
4094 assert_eq!(
4095 started.borrow().len(),
4096 2,
4097 "no latch: the next move asks again"
4098 );
4099
4100 ops.start = crate::drag_payload::OsDragStart::Started;
4101 tree.handle_drag_sample(Point::new(102.0, 50.0), &mut ops);
4102 assert_eq!(started.borrow().len(), 3);
4103 assert_eq!(tree.outbound_drag_source, Some(source), "handed off once");
4104 tree.handle_drag_sample(Point::new(103.0, 50.0), &mut ops);
4105 assert_eq!(started.borrow().len(), 3);
4106 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4107 }
4108
4109 #[test]
4110 fn eager_without_anything_to_export_never_hands_off() {
4111 let mut ops = RecordingWindowOps::new(true);
4112 let started = ops.started.clone();
4113 let (mut tree, _source, _ended) = tree_dragging(
4114 crate::drag_payload::DragPayload::typed(1_u32)
4115 .with_os_handoff(crate::drag_payload::OsHandoff::Eager),
4116 );
4117 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4118 tree.handle_drag_sample(Point::new(-5.0, 50.0), &mut ops);
4119 assert!(started.borrow().is_empty());
4120 assert!(tree.active_drag.is_some());
4121
4122 let mut tree = WidgetTree::new();
4124 tree.add(FillWidget::new().on_drop(|_, _, _| true));
4125 tree.layout(SizeProposal::exact(200.0, 100.0));
4126 let data = crate::drag_payload::ExternalDropData {
4127 text: Some("x".into()),
4128 ..Default::default()
4129 };
4130 tree.begin_external_drag(Point::new(10.0, 10.0), data, &mut ops);
4131 tree.update_external_drag(Point::new(-5.0, 10.0), &mut ops);
4132 assert!(started.borrow().is_empty());
4133 }
4134
4135 #[test]
4136 fn a_pending_start_keeps_the_in_app_drag_until_the_platform_confirms_it() {
4137 let mut ops = RecordingWindowOps::answering(pending());
4138 let started = ops.started.clone();
4139 let (mut tree, source, ended) = tree_dragging(eager_payload());
4140
4141 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4142 assert_eq!(started.borrow().len(), 1);
4143 assert!(
4144 tree.active_drag.is_some(),
4145 "the session waits for the platform"
4146 );
4147 assert!(tree.outbound_drag_source.is_none());
4148 assert!(
4149 !super::has_outbound_typed(),
4150 "nothing parked before the start"
4151 );
4152
4153 tree.handle_drag_sample(Point::new(110.0, 50.0), &mut ops);
4156 tree.handle_drag_sample(Point::new(-5.0, 50.0), &mut ops);
4157 assert_eq!(started.borrow().len(), 1, "one request per start");
4158 assert!(tree.active_drag.is_some());
4159
4160 tree.confirm_os_drag(last_pending(&ops), &mut ops);
4161 assert!(
4162 tree.active_drag.is_none(),
4163 "handed over at the confirmation"
4164 );
4165 assert_eq!(tree.outbound_drag_source, Some(source));
4166 assert!(super::has_outbound_typed());
4167 assert!(ended.borrow().is_empty());
4168 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::OsCopy, &mut ops);
4169 assert_eq!(
4170 *ended.borrow(),
4171 vec![crate::drag_payload::DropOutcome::OsCopy]
4172 );
4173 }
4174
4175 #[test]
4176 fn a_release_before_the_native_drag_starts_ends_the_drag_as_cancelled() {
4177 use crate::drag_payload::DropOutcome;
4178 let mut ops = RecordingWindowOps::answering(pending());
4179 let cancels = ops.cancels.clone();
4180 let dropped = Rc::new(Cell::new(0_u32));
4181 let d = dropped.clone();
4182 let (mut tree, _source, ended) = tree_dragging(eager_payload());
4183 tree.add(FillWidget::new().on_drop(move |_, _, _| {
4184 d.set(d.get() + 1);
4185 true
4186 }));
4187 tree.layout(SizeProposal::exact(200.0, 100.0));
4188
4189 tree.handle_drag_sample(Point::new(150.0, 50.0), &mut ops);
4190 tree.dispatch_event_with_ops(
4191 WidgetEvent::pointer_up(
4192 Point::new(150.0, 50.0),
4193 PointerButton::Primary,
4194 Modifiers::NONE,
4195 ),
4196 &mut ops,
4197 );
4198 assert!(tree.active_drag.is_none());
4199 assert_eq!(dropped.get(), 0, "no drop is delivered");
4200 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
4201 assert_eq!(cancels.get(), 1, "the platform is told to drop the start");
4202
4203 tree.confirm_os_drag(last_pending(&ops), &mut ops);
4206 assert_eq!(cancels.get(), 2);
4207 assert!(tree.outbound_drag_source.is_none());
4208 tree.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
4209 assert_eq!(ended.borrow().len(), 1, "on_drag_ended fired once");
4210 tree.assert_no_leaked_pointer_state();
4211 }
4212
4213 #[test]
4214 fn escape_before_the_native_drag_starts_abandons_the_start() {
4215 use crate::drag_payload::DropOutcome;
4216 let mut ops = RecordingWindowOps::answering(pending());
4217 let cancels = ops.cancels.clone();
4218 let (mut tree, _source, ended) = tree_dragging(eager_payload());
4219 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4220 tree.dispatch_event_with_ops(
4221 WidgetEvent::KeyDown {
4222 key: Key::Escape,
4223 modifiers: Modifiers::NONE,
4224 text: None,
4225 },
4226 &mut ops,
4227 );
4228 assert_eq!(cancels.get(), 1);
4229 assert!(tree.os_drag_pending.is_none());
4230 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
4231 }
4232
4233 #[test]
4234 fn a_start_that_fails_on_the_platform_ends_the_drag_once() {
4235 use crate::drag_payload::DropOutcome;
4236 let mut ops = RecordingWindowOps::answering(pending());
4237 let cancels = ops.cancels.clone();
4238 let (mut tree, _source, ended) = tree_dragging(eager_payload());
4239 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4240
4241 tree.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
4244 assert!(tree.active_drag.is_none());
4245 assert!(tree.os_drag_pending.is_none());
4246 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
4247 assert_eq!(cancels.get(), 0, "the backend ended it already");
4248 }
4249
4250 #[test]
4251 fn closing_the_window_drops_a_pending_start() {
4252 let mut ops = RecordingWindowOps::answering(pending());
4253 let (mut tree, _source, _ended) = tree_dragging(eager_payload());
4254 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4255 assert!(tree.os_drag_pending.is_some());
4256 tree.abort_outbound_drag();
4257 assert!(tree.os_drag_pending.is_none());
4258 assert!(!super::outbound_is_live());
4259 }
4260
4261 #[test]
4262 fn an_inbound_drag_before_the_confirmation_is_the_native_drag_itself() {
4263 let mut ops = RecordingWindowOps::answering(pending());
4264 let (mut tree, source, _ended) = tree_dragging(eager_payload());
4265 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4266
4267 tree.begin_external_drag(
4268 Point::new(100.0, 50.0),
4269 crate::drag_payload::ExternalDropData::default(),
4270 &mut ops,
4271 );
4272 assert_eq!(tree.outbound_drag_source, Some(source), "handed over first");
4273 assert!(tree.os_drag_reentered, "then re-entered as our own drag");
4274 assert!(
4275 tree.active_drag
4276 .as_ref()
4277 .is_some_and(|d| d.payload.has_typed::<u32>()),
4278 "with the typed payload recovered"
4279 );
4280 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4281 }
4282
4283 #[test]
4288 fn a_leave_while_the_pointer_is_inside_the_window_keeps_the_os_drag_alive() {
4289 use crate::drag_payload::DropOutcome;
4290 let mut ops = RecordingWindowOps::new(true);
4291 let (mut tree, _source, ended) = tree_dragging(eager_payload());
4292 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4293 tree.begin_external_drag(
4294 Point::new(100.0, 50.0),
4295 crate::drag_payload::ExternalDropData::default(),
4296 &mut ops,
4297 );
4298 assert!(tree.os_drag_reentered);
4299
4300 tree.update_external_drag(Point::new(110.0, 50.0), &mut ops);
4301 tree.cancel_external_drag(&mut ops);
4302 assert!(!tree.os_drag_reentered);
4303 assert!(tree.active_drag.is_none());
4304 assert!(super::has_outbound_typed(), "payload parked again");
4305 assert!(ended.borrow().is_empty(), "the drag is not over");
4306
4307 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
4308 assert_eq!(*ended.borrow(), vec![DropOutcome::OsCopy]);
4309 }
4310
4311 #[test]
4312 fn an_eager_drag_dropped_back_on_its_own_window_reports_the_in_app_outcome() {
4313 use crate::drag_payload::DropOutcome;
4314 for accepts in [true, false] {
4315 let mut ops = RecordingWindowOps::new(true);
4316 let (mut tree, _source, ended) = tree_dragging(eager_payload());
4317 tree.add(FillWidget::new().on_drop(move |_, _, _| accepts));
4318 tree.layout(SizeProposal::exact(200.0, 100.0));
4319
4320 tree.handle_drag_sample(Point::new(150.0, 50.0), &mut ops);
4321 tree.begin_external_drag(
4322 Point::new(150.0, 50.0),
4323 crate::drag_payload::ExternalDropData::default(),
4324 &mut ops,
4325 );
4326 tree.end_external_drag(
4327 Point::new(150.0, 50.0),
4328 crate::drag_payload::ExternalDropData::default(),
4329 &mut ops,
4330 );
4331 assert!(
4332 ended.borrow().is_empty(),
4333 "the platform still has to end it"
4334 );
4335 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
4337 assert_eq!(
4338 *ended.borrow(),
4339 vec![DropOutcome::InApp { accepted: accepts }]
4340 );
4341 }
4342
4343 let mut ops = RecordingWindowOps::new(true);
4346 let (mut tree, _source, ended) = tree_dragging(eager_payload());
4347 tree.handle_drag_sample(Point::new(150.0, 50.0), &mut ops);
4348 tree.begin_external_drag(
4349 Point::new(150.0, 50.0),
4350 crate::drag_payload::ExternalDropData::default(),
4351 &mut ops,
4352 );
4353 tree.cancel_external_drag(&mut ops);
4354 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
4355 assert_eq!(*ended.borrow(), vec![DropOutcome::OsCopy]);
4356 }
4357
4358 #[test]
4359 fn an_eager_drag_asks_the_platform_once_however_often_it_comes_back() {
4360 let mut ops = RecordingWindowOps::new(true);
4361 let started = ops.started.clone();
4362 for round in 1..=2 {
4363 let (mut tree, _source, _ended) = tree_dragging(eager_payload());
4364 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4365 for _ in 0..2 {
4366 tree.begin_external_drag(
4367 Point::new(100.0, 50.0),
4368 crate::drag_payload::ExternalDropData::default(),
4369 &mut ops,
4370 );
4371 tree.update_external_drag(Point::new(-5.0, 50.0), &mut ops);
4372 assert!(!tree.os_drag_reentered, "re-exited");
4373 }
4374 assert_eq!(started.borrow().len(), round, "one start per drag");
4375 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4376 }
4377 }
4378
4379 #[test]
4380 fn an_eager_finger_drag_tells_the_platform_it_is_a_finger_and_revokes_it() {
4381 let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
4382 Rc::new(RefCell::new(Vec::new()));
4383 let c = cancelled.clone();
4384 let mut ops = RecordingWindowOps::answering(pending());
4385 let kinds = ops.started_kinds.clone();
4386
4387 let mut tree = WidgetTree::new();
4388 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
4389 let source = tree.add(
4390 drag_arming_leaf(slot.clone(), eager_payload).on_pointer_cancel(
4391 move |pointer, reason, _ctx| {
4392 c.borrow_mut().push((pointer.id, reason));
4393 },
4394 ),
4395 );
4396 slot.set(Some(source));
4397 tree.layout(SizeProposal::exact(200.0, 100.0));
4398 let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
4399 assert!(tree.active_drag.is_some());
4400
4401 tree.dispatch_pointer_with_ops(
4402 crate::pointer::PointerSample {
4403 pointer: crate::pointer::PointerInfo::touch(
4404 contact,
4405 crate::pointer::EventTime::from_millis(30),
4406 ),
4407 phase: crate::pointer::PointerPhase::Move,
4408 position: Point::new(40.0, 50.0),
4409 button: None,
4410 modifiers: Modifiers::NONE,
4411 coalesced: Vec::new(),
4412 },
4413 &mut ops,
4414 );
4415 assert_eq!(*kinds.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
4416 assert!(
4417 cancelled.borrow().is_empty(),
4418 "nothing revoked before the start"
4419 );
4420
4421 tree.confirm_os_drag(last_pending(&ops), &mut ops);
4423 assert_eq!(
4424 *cancelled.borrow(),
4425 vec![(contact, crate::pointer::CancelReason::OsDragStarted)]
4426 );
4427 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4428 tree.assert_no_leaked_pointer_state();
4429 }
4430
4431 #[test]
4434 fn a_late_event_of_an_earlier_drag_does_not_touch_the_next_one() {
4435 use crate::drag_payload::DropOutcome;
4436 let mut ops = RecordingWindowOps::answering(pending());
4437 let cancels = ops.cancels.clone();
4438 let (mut tree, source, ended) = tree_dragging(eager_payload());
4439
4440 tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
4442 let first = last_pending(&ops);
4443 tree.dispatch_event_with_ops(
4444 WidgetEvent::pointer_up(
4445 Point::new(100.0, 50.0),
4446 PointerButton::Primary,
4447 Modifiers::NONE,
4448 ),
4449 &mut ops,
4450 );
4451 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
4452 assert_eq!(cancels.get(), 1);
4453
4454 let mut ctx = crate::widget::EventContext::new();
4456 ctx.start_drag(source, eager_payload());
4457 tree.collect_from_ctx(ctx, source);
4458 tree.handle_drag_sample(Point::new(110.0, 50.0), &mut ops);
4459 let second = last_pending(&ops);
4460 assert_ne!(first, second);
4461
4462 tree.confirm_os_drag(first, &mut ops);
4464 tree.handle_os_drag_ended_for(Some(first), DropOutcome::Cancelled, &mut ops);
4465 assert_eq!(tree.os_drag_pending, Some(second), "drag 2 still waits");
4466 assert!(tree.active_drag.is_some(), "with its in-app session");
4467 assert_eq!(cancels.get(), 1, "and its start is not cancelled");
4468 assert_eq!(ended.borrow().len(), 1);
4469
4470 tree.confirm_os_drag(second, &mut ops);
4472 assert_eq!(tree.outbound_drag_source, Some(source));
4473 tree.handle_os_drag_ended_for(Some(first), DropOutcome::OsCopy, &mut ops);
4474 assert_eq!(ended.borrow().len(), 1, "a stale end does not end drag 2");
4475 tree.handle_os_drag_ended_for(Some(second), DropOutcome::OsCopy, &mut ops);
4476 assert_eq!(
4477 *ended.borrow(),
4478 vec![DropOutcome::Cancelled, DropOutcome::OsCopy]
4479 );
4480 }
4481
4482 #[test]
4488 fn a_reentered_drag_ignores_the_windows_own_pointer_samples() {
4489 use crate::drag_payload::DropOutcome;
4490 let mut ops = RecordingWindowOps::new(true);
4491 let dropped = Rc::new(Cell::new(0_u32));
4492 let d = dropped.clone();
4493 let ended: Rc<RefCell<Vec<DropOutcome>>> = Rc::new(RefCell::new(Vec::new()));
4494 let e = ended.clone();
4495 let mut tree = WidgetTree::new();
4496 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
4497 let row = tree.add(
4498 drag_arming_leaf(slot.clone(), eager_payload)
4499 .on_drop(move |_, _, _| {
4500 d.set(d.get() + 1);
4501 true
4502 })
4503 .on_drag_ended(move |o, _| e.borrow_mut().push(o)),
4504 );
4505 slot.set(Some(row));
4506 tree.layout(SizeProposal::exact(200.0, 100.0));
4507
4508 let at = Point::new(50.0, 50.0);
4509 tree.dispatch_event_with_ops(
4510 WidgetEvent::pointer_down(at, PointerButton::Primary, Modifiers::NONE),
4511 &mut ops,
4512 );
4513 tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(60.0, 50.0)), &mut ops);
4514 assert_eq!(
4515 tree.outbound_drag_source,
4516 Some(row),
4517 "handed to the platform"
4518 );
4519
4520 tree.begin_external_drag(
4522 at,
4523 crate::drag_payload::ExternalDropData::default(),
4524 &mut ops,
4525 );
4526 assert!(tree.os_drag_reentered);
4527
4528 tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(70.0, 50.0)), &mut ops);
4530 tree.dispatch_event_with_ops(
4531 WidgetEvent::pointer_up(
4532 Point::new(70.0, 50.0),
4533 PointerButton::Primary,
4534 Modifiers::NONE,
4535 ),
4536 &mut ops,
4537 );
4538 assert_eq!(dropped.get(), 0, "the window's release is not the drop");
4539 assert!(
4540 tree.os_drag_reentered,
4541 "the re-entered session is still there"
4542 );
4543
4544 tree.end_external_drag(
4546 Point::new(70.0, 50.0),
4547 crate::drag_payload::ExternalDropData::default(),
4548 &mut ops,
4549 );
4550 assert_eq!(dropped.get(), 1);
4551 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
4552 assert_eq!(*ended.borrow(), vec![DropOutcome::InApp { accepted: true }]);
4553 tree.assert_no_leaked_pointer_state();
4554 }
4555
4556 #[test]
4560 fn a_reentered_drag_leaves_other_pointers_alone() {
4561 let mut ops = RecordingWindowOps::new(true);
4562 let hovered = Rc::new(Cell::new(0_u32));
4563 let h = hovered.clone();
4564 let mut tree = WidgetTree::new();
4565 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
4566 let row = tree.add(drag_arming_leaf(slot.clone(), eager_payload).on_hover(
4567 move |entered, _| {
4568 if entered {
4569 h.set(h.get() + 1);
4570 }
4571 },
4572 ));
4573 slot.set(Some(row));
4574 tree.layout(SizeProposal::exact(200.0, 100.0));
4575
4576 let at = Point::new(50.0, 50.0);
4577 tree.dispatch_event_with_ops(
4578 WidgetEvent::pointer_down(at, PointerButton::Primary, Modifiers::NONE),
4579 &mut ops,
4580 );
4581 tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(60.0, 50.0)), &mut ops);
4582 tree.begin_external_drag(
4583 at,
4584 crate::drag_payload::ExternalDropData::default(),
4585 &mut ops,
4586 );
4587 assert!(tree.os_drag_reentered);
4588 let before = hovered.get();
4589
4590 tree.pen_hover(Point::new(70.0, 50.0));
4591 assert!(
4592 hovered.get() > before,
4593 "the pen's hover reaches the widget under it"
4594 );
4595 assert!(tree.os_drag_reentered, "and leaves the session alone");
4596 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
4597 }
4598}