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 ) {
162 let Some(pointer) = self.active_drag.as_ref().map(|d| d.pointer) else {
163 return;
164 };
165 let saved = std::mem::replace(
166 &mut self.current_input,
167 crate::pointer::InputSnapshot::for_drag_session(pointer),
168 );
169 match which {
170 DragDrive::Move => self.handle_drag_move(position, ops),
171 DragDrive::Drop => self.handle_drag_drop(position, ops),
172 }
173 self.current_input = saved;
174 }
175
176 pub(super) fn cleanup_drag_preview(&mut self) {
183 let Some((overlay_id, content_id)) = self
184 .active_drag
185 .as_ref()
186 .map(|drag| (drag.preview_overlay_id, drag.preview_content_id))
187 else {
188 return;
189 };
190 if let Some(overlay_id) = overlay_id {
191 self.overlay_manager.dismiss(overlay_id);
192 }
193 if let Some(content_id) = content_id {
194 self.destroy_subtree(content_id);
195 }
196 }
197
198 pub(super) fn cancel_active_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
203 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
204 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
208 let saved = self.active_drag.as_ref().map(|d| d.pointer).map(|p| {
216 std::mem::replace(
217 &mut self.current_input,
218 crate::pointer::InputSnapshot::for_drag_session(p),
219 )
220 });
221 self.cleanup_drag_preview();
222 self.active_drag = None;
223 self.os_drop_accepted = None;
224 self.release_drag_capture(source);
225 self.current_cursor = crate::widget::CursorIcon::Default;
226 if let Some(prev) = prev_target {
227 self.fire_on_drag_leave(prev, &mut *ops);
228 }
229 if let Some(src) = source {
230 self.fire_on_drag_ended(src, crate::drag_payload::DropOutcome::Cancelled, &mut *ops);
231 }
232 if let Some(saved) = saved {
233 self.current_input = saved;
234 }
235 }
236
237 pub fn begin_external_drag(
269 &mut self,
270 position: teksilo_canvas::Point,
271 data: crate::drag_payload::ExternalDropData,
272 ops: &mut dyn crate::window::WindowOps,
273 ) {
274 if self.active_drag.is_some() {
278 self.cancel_active_drag(&mut *ops);
279 }
280
281 if let Some(mut payload) = outbound_take_if_live() {
291 payload.enrich_external_from_mime();
295 let pointer = outbound_pointer_if_live().unwrap_or_else(unknown_external_pointer);
298 self.active_drag = Some(crate::drag_state::DragSession {
299 payload,
300 pointer,
301 source_widget: None,
302 is_external: false,
303 current_position: position,
304 current_target: None,
305 feedback: crate::drag_state::DropFeedback::NoFeedback,
306 preview_content_id: None,
307 preview_overlay_id: None,
308 });
309 self.os_drag_reentered = true;
310 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
311 return;
312 }
313
314 self.active_drag = Some(crate::drag_state::DragSession {
315 payload: crate::drag_payload::DragPayload::external(data),
316 pointer: unknown_external_pointer(),
317 source_widget: None,
318 is_external: true,
319 current_position: position,
320 current_target: None,
321 feedback: crate::drag_state::DropFeedback::NoFeedback,
322 preview_content_id: None,
323 preview_overlay_id: None,
324 });
325 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
328 }
329
330 pub fn update_external_drag(
334 &mut self,
335 position: teksilo_canvas::Point,
336 ops: &mut dyn crate::window::WindowOps,
337 ) {
338 if self.active_drag.as_ref().is_some_and(|d| d.is_external) || self.os_drag_reentered {
342 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
343 }
344 }
345
346 pub fn end_external_drag(
353 &mut self,
354 position: teksilo_canvas::Point,
355 data: crate::drag_payload::ExternalDropData,
356 ops: &mut dyn crate::window::WindowOps,
357 ) {
358 if self.os_drag_reentered {
365 self.os_drag_reentered = false;
366 self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
367 outbound_end();
368 return;
369 }
370 if !self.active_drag.as_ref().is_some_and(|d| d.is_external) {
371 return;
372 }
373 if !data.is_empty()
374 && let Some(drag) = self.active_drag.as_mut()
375 {
376 drag.payload = crate::drag_payload::DragPayload::external(data);
377 }
378 self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
379 }
380
381 pub fn abort_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
398 if self.active_drag.is_none() {
399 self.os_drag_reentered = false;
400 return;
401 }
402 self.os_drag_reentered = false;
403 self.cancel_active_drag(&mut *ops);
404 }
405
406 pub fn cancel_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
411 if self.os_drag_reentered {
416 self.reexit_outbound(&mut *ops);
417 } else if self.active_drag.as_ref().is_some_and(|d| d.is_external) {
418 self.cancel_active_drag(&mut *ops);
419 }
420 }
421
422 pub(super) fn process_drag_tick(&mut self, ops: &mut dyn crate::window::WindowOps) {
427 self.reap_dead_reentered_drag(&mut *ops);
429 let Some((target_id, position, pointer)) = self
430 .active_drag
431 .as_ref()
432 .and_then(|d| d.current_target.map(|t| (t, d.current_position, d.pointer)))
433 else {
434 return;
435 };
436 if !self.arena.is_active(target_id) {
437 return;
438 }
439 let bounds = self.arena.bounds(target_id);
440 let local = teksilo_canvas::Point::new(position.x - bounds.x, position.y - bounds.y);
441 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
442 Some(node) => (
443 node.external_handlers.on_drag_tick.take(),
444 node.handlers.on_drag_tick.take(),
445 ),
446 None => return,
447 };
448 if ext_handler.is_none() && own_handler.is_none() {
449 return;
450 }
451 let saved = std::mem::replace(
456 &mut self.current_input,
457 crate::pointer::InputSnapshot::for_drag_session(pointer),
458 );
459 let mut ctx = self.make_event_context(&mut *ops);
460 if let Some(h) = ext_handler.as_mut() {
461 h(local, &mut ctx);
462 }
463 if let Some(h) = own_handler.as_mut() {
464 h(local, &mut ctx);
465 }
466 if let Some(node) = self.arena.get_mut(target_id) {
467 node.external_handlers.on_drag_tick = ext_handler;
468 node.handlers.on_drag_tick = own_handler;
469 }
470 self.collect_from_ctx(ctx, target_id);
471 if self.active_drag.is_some() {
475 self.handle_drag_move(position, &mut *ops);
476 }
477 self.current_input = saved;
478 }
479
480 fn reap_dead_reentered_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
496 if !self.os_drag_reentered || outbound_is_live() {
497 return;
498 }
499 crate::trace_input!(
500 Gestures,
501 "the OS drag this window held as re-entered has ended elsewhere: dropping the session"
502 );
503 self.abort_external_drag(&mut *ops);
504 }
505
506 pub(super) fn fire_on_drag_leave(
512 &mut self,
513 target_id: WidgetId,
514 ops: &mut dyn crate::window::WindowOps,
515 ) {
516 if !self.arena.is_active(target_id) {
517 return;
518 }
519 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
520 Some(node) => (
521 node.external_handlers.on_drag_leave.take(),
522 node.handlers.on_drag_leave.take(),
523 ),
524 None => return,
525 };
526 if ext_handler.is_none() && own_handler.is_none() {
527 self.arena.mark_needs_paint(target_id);
530 return;
531 }
532 let mut ctx = self.make_event_context(&mut *ops);
533 if let Some(h) = ext_handler.as_mut() {
534 h(&mut ctx);
535 }
536 if let Some(h) = own_handler.as_mut() {
537 h(&mut ctx);
538 }
539 if let Some(node) = self.arena.get_mut(target_id) {
540 node.external_handlers.on_drag_leave = ext_handler;
541 node.handlers.on_drag_leave = own_handler;
542 }
543 self.collect_from_ctx(ctx, target_id);
544 self.arena.mark_needs_paint(target_id);
545 }
546
547 pub(super) fn fire_on_drag_ended(
551 &mut self,
552 source_id: WidgetId,
553 outcome: crate::drag_payload::DropOutcome,
554 ops: &mut dyn crate::window::WindowOps,
555 ) {
556 if !self.arena.is_active(source_id) {
557 return;
558 }
559 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(source_id) {
564 Some(node) => (
565 node.external_handlers.on_drag_ended.take(),
566 node.handlers.on_drag_ended.take(),
567 ),
568 None => return,
569 };
570 if ext_handler.is_none() && own_handler.is_none() {
571 return;
572 }
573 let mut ctx = self.make_event_context(&mut *ops);
574 if let Some(h) = ext_handler.as_mut() {
575 h(outcome, &mut ctx);
576 }
577 if let Some(h) = own_handler.as_mut() {
578 h(outcome, &mut ctx);
579 }
580 if let Some(node) = self.arena.get_mut(source_id) {
581 node.external_handlers.on_drag_ended = ext_handler;
582 node.handlers.on_drag_ended = own_handler;
583 }
584 self.collect_from_ctx(ctx, source_id);
585 }
586
587 fn window_content_size(&self) -> Option<(f32, f32)> {
590 Some((self.last_proposal.width?, self.last_proposal.height?))
591 }
592
593 fn is_outside_window(&self, position: teksilo_canvas::Point) -> bool {
596 match self.window_content_size() {
597 Some((w, h)) => {
598 position.x < 0.0 || position.y < 0.0 || position.x > w || position.y > h
599 }
600 None => false,
601 }
602 }
603
604 fn try_escalate_to_os_drag(
610 &mut self,
611 position: teksilo_canvas::Point,
612 ops: &mut dyn crate::window::WindowOps,
613 ) -> bool {
614 if self.os_drag_reentered {
619 if self.is_outside_window(position) {
620 self.reexit_outbound(&mut *ops);
621 return true;
622 }
623 return false;
624 }
625
626 let data = match self.active_drag.as_ref() {
628 Some(d)
629 if !d.is_external && d.source_widget.is_some() && d.payload.is_os_exportable() =>
630 {
631 d.payload.to_outbound()
632 }
633 _ => return false,
634 };
635 if !self.is_outside_window(position) {
636 return false;
637 }
638
639 let dragging = match self.active_drag.as_ref() {
643 Some(d) => d.pointer,
644 None => return false,
645 };
646 if !ops.begin_os_drag(data, None, dragging.kind) {
647 return false;
648 }
649
650 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
655 self.cleanup_drag_preview();
656 let drag = self
657 .active_drag
658 .take()
659 .expect("active_drag present (matched above)");
660 self.os_drop_accepted = None;
661 self.release_drag_capture(drag.source_widget);
662 self.current_cursor = crate::widget::CursorIcon::Default;
663 self.outbound_drag_source = drag.source_widget;
664 outbound_begin(drag.payload, dragging);
665 if let Some(prev) = prev_target {
666 self.fire_on_drag_leave(prev, &mut *ops);
667 }
668 self.cancel_pointer_to(
681 dragging.id,
682 crate::pointer::CancelReason::OsDragStarted,
683 self.outbound_drag_source,
684 &mut *ops,
685 );
686 true
687 }
688
689 fn reexit_outbound(&mut self, ops: &mut dyn crate::window::WindowOps) {
693 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
694 self.cleanup_drag_preview();
695 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
696 if let Some(drag) = self.active_drag.take() {
697 outbound_restash(drag.payload);
698 }
699 self.os_drag_reentered = false;
700 self.os_drop_accepted = None;
701 self.release_drag_capture(source);
702 self.current_cursor = crate::widget::CursorIcon::Default;
703 if let Some(prev) = prev_target {
704 self.fire_on_drag_leave(prev, &mut *ops);
705 }
706 }
707
708 pub fn handle_os_drag_ended(
713 &mut self,
714 outcome: crate::drag_payload::DropOutcome,
715 ops: &mut dyn crate::window::WindowOps,
716 ) {
717 outbound_end();
720 self.os_drag_reentered = false;
721 if let Some(source) = self.outbound_drag_source.take() {
722 self.fire_on_drag_ended(source, outcome, &mut *ops);
723 }
724 }
725
726 pub fn abort_outbound_drag(&mut self) {
734 if self.outbound_drag_source.take().is_some() {
735 outbound_end();
736 } else if self.os_drag_reentered
737 && let Some(drag) = self.active_drag.take()
738 {
739 outbound_restash(drag.payload);
740 }
741 self.os_drag_reentered = false;
742 self.os_drop_accepted = None;
743 }
744
745 pub(super) fn handle_drag_move(
748 &mut self,
749 position: teksilo_canvas::Point,
750 ops: &mut dyn crate::window::WindowOps,
751 ) {
752 if let Some(ref mut drag) = self.active_drag {
754 drag.current_position = position;
755 }
756
757 if self.try_escalate_to_os_drag(position, &mut *ops) {
760 return;
761 }
762
763 let preview_content = self
770 .active_drag
771 .as_ref()
772 .and_then(|d| Some((d.preview_overlay_id?, d.preview_content_id?)));
773 if let Some((overlay_id, content_id)) = preview_content {
774 let dragging = self.active_drag.as_ref().map(|d| d.pointer);
780 let placement = match dragging {
781 Some(pointer) => {
782 crate::overlay::OverlayPlacement::at_pointer_for(position, &pointer)
783 }
784 None => crate::overlay::OverlayPlacement::AtPointer(position),
785 };
786 self.overlay_manager.update_placement(overlay_id, placement);
787 self.arena.mark_needs_layout(content_id);
788 }
789
790 let exclude_overlay = self.active_drag.as_ref().and_then(|d| d.preview_overlay_id);
794 let exclude_widget = self.active_drag.as_ref().and_then(|d| d.preview_content_id);
795 let dragging = self.current_input.pointer;
799 let target =
800 self.hit_test_for_excluding(position, &dragging, exclude_overlay, exclude_widget);
801
802 let mut candidate = target.and_then(|t| self.find_drop_target_at_or_above(t));
810 let mut engaged: Option<WidgetId> = None;
811 let mut engaged_feedback = crate::drag_state::DropFeedback::NoFeedback;
812 let mut bubbled_past: Vec<WidgetId> = Vec::new();
813 while let Some(cand) = candidate {
814 let fb = self.fire_on_drag_hover(cand, position, &mut *ops);
815 if fb.is_engaged() {
816 engaged = Some(cand);
817 engaged_feedback = fb;
818 break;
819 }
820 bubbled_past.push(cand);
821 candidate = self.next_drop_target_above(cand);
822 }
823
824 let (new_target, new_feedback) = if engaged.is_some() {
832 for cand in &bubbled_past {
833 self.fire_on_drag_leave(*cand, &mut *ops);
834 }
835 (engaged, engaged_feedback)
836 } else if let Some((&deepest, rest)) = bubbled_past.split_first() {
837 for cand in rest {
838 self.fire_on_drag_leave(*cand, &mut *ops);
839 }
840 (Some(deepest), crate::drag_state::DropFeedback::NoFeedback)
841 } else {
842 (None, crate::drag_state::DropFeedback::NoFeedback)
843 };
844
845 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
851 if prev_target != new_target
852 && let Some(prev) = prev_target
853 && !bubbled_past.contains(&prev)
854 {
855 self.fire_on_drag_leave(prev, &mut *ops);
856 }
857 let engaged_now = new_feedback.is_engaged();
858 let is_os_drag =
864 self.os_drag_reentered || self.active_drag.as_ref().is_some_and(|d| d.is_external);
865 if let Some(ref mut drag) = self.active_drag {
866 drag.current_target = new_target;
867 drag.feedback = new_feedback;
868 }
869 if is_os_drag && self.os_drop_accepted != Some(engaged_now) {
880 self.os_drop_accepted = Some(engaged_now);
881 ops.set_drop_accepted(engaged_now);
882 }
883 }
884
885 fn fire_on_drag_hover(
890 &mut self,
891 target_id: WidgetId,
892 position: teksilo_canvas::Point,
893 ops: &mut dyn crate::window::WindowOps,
894 ) -> crate::drag_state::DropFeedback {
895 use crate::drag_state::DropFeedback;
896 let target_bounds = self.arena.bounds(target_id);
897 let local =
898 teksilo_canvas::Point::new(position.x - target_bounds.x, position.y - target_bounds.y);
899 let (mut ext_handler, mut own_handler, has_on_drop) = match self.arena.get_mut(target_id) {
900 Some(node) => {
901 let has_on_drop = node.any_handler(|h| h.on_drop.is_some());
902 let ext = node.external_handlers.on_drag_hover.take();
903 let own = node.handlers.on_drag_hover.take();
904 (ext, own, has_on_drop)
905 }
906 None => return DropFeedback::NoFeedback,
907 };
908 if ext_handler.is_none() && own_handler.is_none() {
910 return if has_on_drop {
911 DropFeedback::Accept
912 } else {
913 DropFeedback::NoFeedback
914 };
915 }
916 let mut feedback = DropFeedback::NoFeedback;
917 if self.active_drag.is_some() {
918 let mut ctx = self.make_event_context(&mut *ops);
919 if let Some(ref drag) = self.active_drag {
920 if let Some(h) = ext_handler.as_mut() {
921 feedback = h(&drag.payload, local, &mut ctx);
922 }
923 if let Some(h) = own_handler.as_mut() {
924 feedback = h(&drag.payload, local, &mut ctx);
925 }
926 }
927 if let Some(node) = self.arena.get_mut(target_id) {
928 node.external_handlers.on_drag_hover = ext_handler;
929 node.handlers.on_drag_hover = own_handler;
930 }
931 self.collect_from_ctx(ctx, target_id);
932 self.arena.mark_needs_paint(target_id);
933 } else if let Some(node) = self.arena.get_mut(target_id) {
934 node.external_handlers.on_drag_hover = ext_handler;
935 node.handlers.on_drag_hover = own_handler;
936 }
937 feedback
938 }
939
940 fn next_drop_target_above(&self, id: WidgetId) -> Option<WidgetId> {
943 let parent = self.arena.parent(id)?;
944 self.find_drop_target_at_or_above(parent)
945 }
946
947 pub(super) fn handle_drag_drop(
949 &mut self,
950 position: teksilo_canvas::Point,
951 ops: &mut dyn crate::window::WindowOps,
952 ) {
953 self.cleanup_drag_preview();
955
956 if self.active_drag.is_none() {
957 return;
958 }
959
960 let mut drop_target = self
971 .active_drag
972 .as_ref()
973 .and_then(|d| d.current_target)
974 .filter(|&t| self.arena.is_active(t));
975 if drop_target.is_none() {
976 let dropping = self.current_input.pointer;
977 let hit = self.hit_test_for(position, &dropping);
978 let mut candidate = hit.and_then(|t| self.find_drop_target_at_or_above(t));
979 while let Some(cand) = candidate {
980 if self
981 .fire_on_drag_hover(cand, position, &mut *ops)
982 .is_engaged()
983 {
984 drop_target = Some(cand);
985 break;
986 }
987 self.fire_on_drag_leave(cand, &mut *ops);
990 candidate = self.next_drop_target_above(cand);
991 }
992 }
993
994 let drag = match self.active_drag.take() {
996 Some(d) => d,
997 None => return,
998 };
999 self.os_drop_accepted = None;
1000 self.release_drag_capture(drag.source_widget);
1001 self.current_cursor = crate::widget::CursorIcon::Default;
1002 let source = drag.source_widget;
1005 let mut outcome = crate::drag_payload::DropOutcome::Cancelled;
1008
1009 if let Some(prev) = drop_target {
1013 self.fire_on_drag_leave(prev, &mut *ops);
1014 }
1015
1016 if let Some(target_id) = drop_target {
1021 let target_bounds = self.arena.bounds(target_id);
1022 let local = teksilo_canvas::Point::new(
1023 position.x - target_bounds.x,
1024 position.y - target_bounds.y,
1025 );
1026 let (taken_own, taken_ext) = match self.arena.get_mut(target_id) {
1027 Some(node) => {
1028 let own = node.handlers.on_drop.take();
1029 let ext = if own.is_none() {
1030 node.external_handlers.on_drop.take()
1031 } else {
1032 None
1033 };
1034 (own, ext)
1035 }
1036 None => (None, None),
1037 };
1038 let picked = if let Some(h) = taken_own {
1039 Some((h, true))
1040 } else {
1041 taken_ext.map(|h| (h, false))
1042 };
1043 if let Some((mut handler, is_own)) = picked {
1044 let mut ctx = self.make_event_context(&mut *ops);
1045 let accepted = handler(drag.payload, local, &mut ctx);
1046 outcome = crate::drag_payload::DropOutcome::InApp { accepted };
1047 if let Some(node) = self.arena.get_mut(target_id) {
1048 if is_own {
1049 node.handlers.on_drop = Some(handler);
1050 } else {
1051 node.external_handlers.on_drop = Some(handler);
1052 }
1053 }
1054 self.collect_from_ctx(ctx, target_id);
1055 self.arena.mark_needs_paint(target_id);
1056 }
1057 }
1058 if let Some(src) = source {
1062 self.fire_on_drag_ended(src, outcome, &mut *ops);
1063 }
1064 }
1065
1066 fn find_drop_target_at_or_above(&self, start: WidgetId) -> Option<WidgetId> {
1069 let mut current = Some(start);
1070 while let Some(id) = current {
1071 if let Some(node) = self.arena.get(id)
1072 && node.any_handler(|h| h.on_drop.is_some() || h.on_drag_hover.is_some())
1073 {
1074 return Some(id);
1075 }
1076 current = self.arena.parent(id);
1077 }
1078 None
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use crate::test_widgets::{FillWidget, StackWidget};
1086 use crate::widget::CursorIcon;
1087 use crate::widget_builder::WidgetBuilder;
1088
1089 #[test]
1090 fn start_drag_creates_session() {
1091 let mut tree = WidgetTree::new();
1092 let source = tree.add(FillWidget::new().on_tap({
1093 move |_pos, ctx: &mut crate::widget::EventContext| {
1094 ctx.start_drag(
1095 ctx.focus_requests.first().copied().unwrap_or_default(),
1096 crate::drag_payload::DragPayload::typed(42_u32),
1097 );
1098 }
1099 }));
1100 tree.layout(SizeProposal::exact(100.0, 50.0));
1101
1102 let mut ctx = crate::widget::EventContext::new();
1104 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1105 tree.collect_from_ctx(ctx, source);
1106
1107 assert!(tree.active_drag.is_some());
1108 let drag = tree.active_drag.as_ref().unwrap();
1109 assert_eq!(drag.source_widget, Some(source));
1110 assert!(!drag.is_external);
1111 assert!(drag.payload.has_typed::<u32>());
1112 }
1113
1114 #[test]
1115 fn drag_move_updates_position() {
1116 let mut tree = WidgetTree::new();
1117 let source = tree.add(FillWidget::new());
1118 tree.layout(SizeProposal::exact(200.0, 100.0));
1119
1120 let mut ctx = crate::widget::EventContext::new();
1122 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("hello"));
1123 tree.collect_from_ctx(ctx, source);
1124 assert!(tree.active_drag.is_some());
1125
1126 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 30.0)));
1128
1129 let drag = tree.active_drag.as_ref().unwrap();
1130 assert!((drag.current_position.x - 50.0).abs() < 0.01);
1131 assert!((drag.current_position.y - 30.0).abs() < 0.01);
1132 }
1133
1134 #[test]
1135 fn escape_cancels_drag() {
1136 let mut tree = WidgetTree::new();
1137 let source = tree.add(FillWidget::new());
1138 tree.layout(SizeProposal::exact(200.0, 100.0));
1139
1140 let mut ctx = crate::widget::EventContext::new();
1141 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(99_i32));
1142 tree.collect_from_ctx(ctx, source);
1143 assert!(tree.active_drag.is_some());
1144
1145 tree.press_key(Key::Escape, Modifiers::NONE);
1146 assert!(tree.active_drag.is_none(), "drag should be cancelled");
1147 }
1148
1149 #[test]
1150 fn drop_on_target_fires_handler() {
1151 use std::cell::Cell;
1152 use std::rc::Rc;
1153
1154 let dropped = Rc::new(Cell::new(false));
1155 let dropped_value = Rc::new(Cell::new(0_u32));
1156 let d = dropped.clone();
1157 let dv = dropped_value.clone();
1158
1159 let mut tree = WidgetTree::new();
1160 let source = tree.add(FillWidget::new());
1161 let _target = tree.add(FillWidget::new().on_drop(move |mut payload, _pos, _ctx| {
1163 d.set(true);
1164 if let Some(val) = payload.take_typed::<u32>() {
1165 dv.set(val);
1166 }
1167 true
1168 }));
1169 tree.layout(SizeProposal::exact(200.0, 100.0));
1170
1171 let mut ctx = crate::widget::EventContext::new();
1173 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1174 tree.collect_from_ctx(ctx, source);
1175
1176 tree.dispatch_event(WidgetEvent::pointer_up(
1178 Point::new(150.0, 50.0),
1179 PointerButton::Primary,
1180 Modifiers::NONE,
1181 ));
1182
1183 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1184 assert!(dropped.get(), "on_drop should have been called");
1185 assert_eq!(dropped_value.get(), 42);
1186 }
1187
1188 #[test]
1189 fn drop_on_no_target_cancels() {
1190 let mut tree = WidgetTree::new();
1191 let source = tree.add(FillWidget::new());
1192 tree.layout(SizeProposal::exact(100.0, 50.0));
1193
1194 let mut ctx = crate::widget::EventContext::new();
1195 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1196 tree.collect_from_ctx(ctx, source);
1197
1198 tree.dispatch_event(WidgetEvent::pointer_up(
1200 Point::new(999.0, 999.0),
1201 PointerButton::Primary,
1202 Modifiers::NONE,
1203 ));
1204
1205 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1206 }
1207
1208 #[test]
1209 fn drop_falls_through_a_destroyed_current_target() {
1210 use std::cell::Cell;
1215 use std::rc::Rc;
1216
1217 let bg_dropped = Rc::new(Cell::new(false));
1218 let bg_sink = bg_dropped.clone();
1219
1220 let mut tree = WidgetTree::new();
1221 let source = tree.add(FillWidget::new());
1225 let _bg = tree.add(FillWidget::new().on_drop(move |_p, _pos, _ctx| {
1226 bg_sink.set(true);
1227 true
1228 }));
1229 let fg = tree.add(
1232 FillWidget::new()
1233 .on_drag_hover(|_payload, _pos, _ctx| {
1234 crate::drag_state::DropFeedback::InsertionLine {
1235 y: 50.0,
1236 width: 200.0,
1237 }
1238 })
1239 .on_drop(|_, _, _| true),
1240 );
1241 tree.layout(SizeProposal::exact(200.0, 100.0));
1242
1243 let mut ctx = crate::widget::EventContext::new();
1244 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1245 tree.collect_from_ctx(ctx, source);
1246
1247 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1249 assert_eq!(
1250 tree.active_drag.as_ref().unwrap().current_target,
1251 Some(fg),
1252 "fg engaged as the current drop target"
1253 );
1254
1255 tree.arena.destroy(fg);
1257 assert!(!tree.arena.is_active(fg));
1258
1259 tree.dispatch_event(WidgetEvent::pointer_up(
1262 Point::new(100.0, 50.0),
1263 PointerButton::Primary,
1264 Modifiers::NONE,
1265 ));
1266
1267 assert!(tree.active_drag.is_none(), "drag session cleared");
1268 assert!(
1269 bg_dropped.get(),
1270 "drop landed on the live background target, not the destroyed one"
1271 );
1272 }
1273
1274 fn mouse_members(
1280 tree: &WidgetTree,
1281 ) -> Vec<(
1282 WidgetId,
1283 crate::gesture::MemberRole,
1284 crate::gesture::MemberState,
1285 )> {
1286 tree.sequence_members(crate::pointer::PointerId::MOUSE)
1287 }
1288
1289 #[test]
1290 fn drag_arming_walks_to_an_ancestor_without_a_dead_zone() {
1291 use crate::gesture::{MemberRole, MemberState};
1295 let mut tree = WidgetTree::new();
1296 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1297 let inner = tree.add(StackWidget::new().child(button));
1298 let ancestor = tree.add(StackWidget::new().child(inner).on_drag(|_phase, _ctx| {}));
1299 tree.layout(SizeProposal::exact(100.0, 100.0));
1300
1301 let b = tree.bounds(button);
1302 tree.pointer_down_button(
1303 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1304 PointerButton::Primary,
1305 );
1306 assert_eq!(
1307 mouse_members(&tree),
1308 vec![(ancestor, MemberRole::Gesture, MemberState::Possible)],
1309 "the draggable ancestor competes when the button press is not in a dead zone"
1310 );
1311 tree.pointer_up_button(
1312 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1313 PointerButton::Primary,
1314 );
1315 }
1316
1317 #[test]
1318 fn gesture_dead_zone_blocks_ancestor_drag_arming() {
1319 use crate::widget_builder::WidgetBuilder;
1325 let mut tree = WidgetTree::new();
1326 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1327 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1328 let _ancestor = tree.add(
1329 StackWidget::new()
1330 .child(dead_zone)
1331 .on_drag(|_phase, _ctx| {}),
1332 );
1333 tree.layout(SizeProposal::exact(100.0, 100.0));
1334
1335 let b = tree.bounds(button);
1336 tree.pointer_down_button(
1337 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1338 PointerButton::Primary,
1339 );
1340 assert!(
1341 mouse_members(&tree).is_empty(),
1342 "a dead zone blocks the draggable ancestor from competing"
1343 );
1344 tree.pointer_up_button(
1345 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1346 PointerButton::Primary,
1347 );
1348 }
1349
1350 #[test]
1351 fn a_dead_zone_boundary_blocks_a_mouse_exactly_as_it_blocks_a_finger() {
1352 use crate::pointer::{
1357 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
1358 };
1359 use crate::widget_builder::WidgetBuilder;
1360
1361 let mut tree = WidgetTree::new();
1362 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1363 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1364 tree.add(
1365 StackWidget::new()
1366 .child(dead_zone)
1367 .on_drag(|_phase, _ctx| {}),
1368 );
1369 tree.layout(SizeProposal::exact(100.0, 100.0));
1370 let b = tree.bounds(button);
1371 let at = Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
1372
1373 tree.pointer_down_button(at, PointerButton::Primary);
1374 assert!(
1375 mouse_members(&tree).is_empty(),
1376 "the mouse enrols no ancestor across the dead zone"
1377 );
1378 tree.pointer_up_button(at, PointerButton::Primary);
1379
1380 let contact = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 41);
1381 let sample = |phase| PointerSample {
1382 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
1383 phase,
1384 position: at,
1385 button: None,
1386 modifiers: Modifiers::NONE,
1387 coalesced: Vec::new(),
1388 };
1389 tree.dispatch_pointer(sample(PointerPhase::Down));
1390 assert!(
1391 tree.sequence_members(contact).is_empty(),
1392 "and neither does a finger"
1393 );
1394 tree.dispatch_pointer(sample(PointerPhase::Up));
1395 }
1396
1397 #[test]
1398 fn drag_hover_calls_on_drag_hover() {
1399 use std::cell::Cell;
1400 use std::rc::Rc;
1401
1402 let hover_count = Rc::new(Cell::new(0));
1403 let hc = hover_count.clone();
1404
1405 let mut tree = WidgetTree::new();
1406 let source = tree.add(FillWidget::new());
1407 let _target = tree.add(
1408 FillWidget::new()
1409 .on_drag_hover(move |_payload, _pos, _ctx| {
1410 hc.set(hc.get() + 1);
1411 crate::drag_state::DropFeedback::InsertionLine {
1412 y: 50.0,
1413 width: 200.0,
1414 }
1415 })
1416 .on_drop(|_, _, _| true),
1417 );
1418 tree.layout(SizeProposal::exact(200.0, 100.0));
1419
1420 let mut ctx = crate::widget::EventContext::new();
1422 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("test"));
1423 tree.collect_from_ctx(ctx, source);
1424
1425 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1427
1428 assert!(
1429 hover_count.get() > 0,
1430 "on_drag_hover should have been called"
1431 );
1432 }
1433
1434 #[test]
1440 fn rejecter_under_engaging_ancestor_leaves_once() {
1441 use std::cell::Cell;
1442 use std::rc::Rc;
1443
1444 let leaves = Rc::new(Cell::new(0));
1445 let lv = leaves.clone();
1446 let engage = Rc::new(Cell::new(false));
1449 let eg = engage.clone();
1450
1451 let mut tree = WidgetTree::new();
1452 let source = tree.add(FillWidget::new());
1453
1454 let child = tree.add(
1456 FillWidget::new()
1457 .on_drag_hover(|_payload, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1458 .on_drag_leave(move |_ctx| lv.set(lv.get() + 1)),
1459 );
1460 let _ancestor = tree.add(StackWidget::new().child(child).on_drag_hover(
1462 move |_payload, _pos, _ctx| {
1463 if eg.get() {
1464 crate::drag_state::DropFeedback::Accept
1465 } else {
1466 crate::drag_state::DropFeedback::NoFeedback
1467 }
1468 },
1469 ));
1470 tree.layout(SizeProposal::exact(200.0, 100.0));
1471
1472 let mut ctx = crate::widget::EventContext::new();
1473 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1474 tree.collect_from_ctx(ctx, source);
1475
1476 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1478 assert_eq!(
1479 tree.active_drag.as_ref().unwrap().current_target,
1480 Some(child)
1481 );
1482 assert_eq!(leaves.get(), 0, "no leave yet — child is freshly tracked");
1483
1484 engage.set(true);
1486 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(101.0, 50.0)));
1487
1488 assert_eq!(
1489 leaves.get(),
1490 1,
1491 "child must receive exactly one on_drag_leave, not two"
1492 );
1493 }
1494
1495 #[test]
1496 fn drop_outside_window_cancels() {
1497 let mut tree = WidgetTree::new();
1498 let source = tree.add(FillWidget::new());
1499 tree.layout(SizeProposal::exact(100.0, 50.0));
1500
1501 let mut ctx = crate::widget::EventContext::new();
1502 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1503 tree.collect_from_ctx(ctx, source);
1504 assert!(tree.active_drag.is_some());
1505
1506 tree.dispatch_event(WidgetEvent::pointer_up(
1508 Point::new(-100.0, -100.0),
1509 PointerButton::Primary,
1510 Modifiers::NONE,
1511 ));
1512
1513 assert!(tree.active_drag.is_none(), "drag should be cleared");
1514 }
1515
1516 #[test]
1517 fn drop_target_rejects_wrong_type() {
1518 use std::cell::Cell;
1519 use std::rc::Rc;
1520
1521 let accepted = Rc::new(Cell::new(false));
1522 let a = accepted.clone();
1523
1524 let mut tree = WidgetTree::new();
1525 let source = tree.add(FillWidget::new());
1526 let _target = tree.add(
1528 FillWidget::new()
1529 .on_drag_hover(|payload, _pos, _ctx| {
1530 if payload.has_typed::<String>() {
1531 crate::drag_state::DropFeedback::InsertionLine {
1532 y: 0.0,
1533 width: 100.0,
1534 }
1535 } else {
1536 crate::drag_state::DropFeedback::NoFeedback
1537 }
1538 })
1539 .on_drop(move |payload, _pos, _ctx| {
1540 if payload.has_typed::<String>() {
1541 a.set(true);
1542 true
1543 } else {
1544 false
1545 }
1546 }),
1547 );
1548 tree.layout(SizeProposal::exact(200.0, 100.0));
1549
1550 let mut ctx = crate::widget::EventContext::new();
1552 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1553 tree.collect_from_ctx(ctx, source);
1554
1555 tree.dispatch_event(WidgetEvent::pointer_up(
1556 Point::new(150.0, 50.0),
1557 PointerButton::Primary,
1558 Modifiers::NONE,
1559 ));
1560
1561 assert!(!accepted.get(), "on_drop should reject wrong payload type");
1562 }
1563
1564 #[test]
1565 fn inter_widget_drop_transfers_payload() {
1566 use std::cell::Cell;
1567 use std::rc::Rc;
1568
1569 let received_value = Rc::new(Cell::new(0_u32));
1570 let rv = received_value.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(|_payload, _pos, _ctx| {
1577 crate::drag_state::DropFeedback::InsertionLine {
1578 y: 0.0,
1579 width: 100.0,
1580 }
1581 })
1582 .on_drop(move |mut payload, _pos, _ctx| {
1583 if let Some(val) = payload.take_typed::<u32>() {
1584 rv.set(val);
1585 true
1586 } else {
1587 false
1588 }
1589 }),
1590 );
1591 tree.layout(SizeProposal::exact(200.0, 100.0));
1592
1593 let mut ctx = crate::widget::EventContext::new();
1595 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(777_u32));
1596 tree.collect_from_ctx(ctx, source);
1597
1598 tree.dispatch_event(WidgetEvent::pointer_up(
1600 Point::new(150.0, 50.0),
1601 PointerButton::Primary,
1602 Modifiers::NONE,
1603 ));
1604
1605 assert_eq!(
1606 received_value.get(),
1607 777,
1608 "Target should receive the typed payload from source"
1609 );
1610 }
1611
1612 #[test]
1613 fn drop_on_child_walks_up_to_ancestor_drop_target() {
1614 use crate::test_widgets::StackWidget;
1615 use std::cell::Cell;
1616 use std::rc::Rc;
1617
1618 let parent_fired = Rc::new(Cell::new(false));
1621 let pf = parent_fired.clone();
1622
1623 let mut tree = WidgetTree::new();
1624 let source = tree.add(FillWidget::new());
1625 let child = tree.add(FillWidget::new());
1626 let _parent = tree.add(StackWidget::new().child(child).on_drop(
1627 move |_payload, _pos, _ctx| {
1628 pf.set(true);
1629 true
1630 },
1631 ));
1632 tree.layout(SizeProposal::exact(200.0, 100.0));
1633
1634 let mut ctx = crate::widget::EventContext::new();
1636 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
1637 tree.collect_from_ctx(ctx, source);
1638
1639 tree.dispatch_event(WidgetEvent::pointer_up(
1642 Point::new(100.0, 50.0),
1643 PointerButton::Primary,
1644 Modifiers::NONE,
1645 ));
1646
1647 assert!(
1648 parent_fired.get(),
1649 "Parent's on_drop should fire via ancestor walk"
1650 );
1651 }
1652
1653 #[test]
1654 fn drop_bubbles_past_a_rejecting_child_to_ancestor() {
1655 use crate::test_widgets::StackWidget;
1656 use std::cell::Cell;
1657 use std::rc::Rc;
1658
1659 let child_drop = Rc::new(Cell::new(false));
1664 let parent_drop = Rc::new(Cell::new(false));
1665 let cd = child_drop.clone();
1666 let pd = parent_drop.clone();
1667
1668 let mut tree = WidgetTree::new();
1669 let source = tree.add(FillWidget::new());
1670 let child = tree.add(
1671 FillWidget::new()
1672 .on_drag_hover(|_p, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1673 .on_drop(move |_p, _pos, _ctx| {
1674 cd.set(true);
1675 false }),
1677 );
1678 let _parent = tree.add(
1679 StackWidget::new()
1680 .child(child)
1681 .on_drop(move |_p, _pos, _ctx| {
1682 pd.set(true);
1683 true
1684 }),
1685 );
1686 tree.layout(SizeProposal::exact(200.0, 100.0));
1687
1688 let mut ctx = crate::widget::EventContext::new();
1689 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u8));
1690 tree.collect_from_ctx(ctx, source);
1691
1692 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1695 tree.dispatch_event(WidgetEvent::pointer_up(
1696 Point::new(100.0, 50.0),
1697 PointerButton::Primary,
1698 Modifiers::NONE,
1699 ));
1700
1701 assert!(parent_drop.get(), "drop bubbles to the accepting ancestor");
1702 assert!(
1703 !child_drop.get(),
1704 "the rejecting child must not receive the drop"
1705 );
1706 }
1707
1708 #[test]
1709 fn drag_preview_overlay_created_and_dismissed() {
1710 let mut tree = WidgetTree::new();
1711 let source = tree.add(FillWidget::new());
1712 tree.layout(SizeProposal::exact(100.0, 50.0));
1713
1714 let overlay_count_before = tree.overlay_manager().len();
1715
1716 let mut ctx = crate::widget::EventContext::new();
1718 ctx.start_drag_with_preview(
1719 source,
1720 crate::drag_payload::DragPayload::typed(0_u32),
1721 Box::new(FillWidget::new()),
1722 );
1723 tree.collect_from_ctx(ctx, source);
1724
1725 assert!(tree.active_drag.is_some(), "drag session should be active");
1726 assert!(
1727 tree.active_drag
1728 .as_ref()
1729 .unwrap()
1730 .preview_overlay_id
1731 .is_some(),
1732 "preview overlay id should be recorded"
1733 );
1734 assert_eq!(
1735 tree.overlay_manager().len(),
1736 overlay_count_before + 1,
1737 "overlay count should increase by one for the preview"
1738 );
1739
1740 tree.dispatch_event(WidgetEvent::pointer_up(
1742 Point::new(999.0, 999.0),
1743 PointerButton::Primary,
1744 Modifiers::NONE,
1745 ));
1746
1747 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1748 assert_eq!(
1749 tree.overlay_manager().len(),
1750 overlay_count_before,
1751 "preview overlay should be dismissed on drop"
1752 );
1753 }
1754
1755 #[test]
1756 fn drag_preview_teardown_releases_what_its_content_registered() {
1757 #[derive(Debug)]
1758 struct SpinningPreview;
1759 impl Widget for SpinningPreview {
1760 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1761 let _ = ctx.animated_quad(crate::animated_quad::AnimatedQuadKind::SpinnerArc {
1762 period: std::time::Duration::from_secs(1),
1763 arc_fraction: 0.25,
1764 stroke_fraction: 0.12,
1765 color: teksilo_tokens::Color::RED.into(),
1766 });
1767 Vec::new()
1768 }
1769 fn layout_response(
1770 &self,
1771 _: SizeProposal,
1772 _: &crate::widget::LayoutContext,
1773 ) -> crate::widget::LayoutResponse {
1774 teksilo_canvas::Size::new(20.0, 20.0).into()
1775 }
1776 }
1777
1778 let mut tree = WidgetTree::new();
1779 let source = tree.add(FillWidget::new());
1780 tree.layout(SizeProposal::exact(100.0, 50.0));
1781 let mut ctx = crate::widget::EventContext::new();
1782 ctx.start_drag_with_preview(
1783 source,
1784 crate::drag_payload::DragPayload::typed(0_u32),
1785 Box::new(SpinningPreview),
1786 );
1787 tree.collect_from_ctx(ctx, source);
1788 assert_eq!(tree.animated_quad_count(), 1, "the preview's build ran");
1789
1790 tree.dispatch_event(WidgetEvent::pointer_up(
1791 Point::new(999.0, 999.0),
1792 PointerButton::Primary,
1793 Modifiers::NONE,
1794 ));
1795 assert!(tree.active_drag.is_none());
1796 assert_eq!(
1797 tree.animated_quad_count(),
1798 0,
1799 "tearing the preview down must release its animated-quad slot"
1800 );
1801 }
1802
1803 #[test]
1804 fn drag_preview_follows_pointer_position() {
1805 let mut tree = WidgetTree::new();
1806 let source = tree.add(FillWidget::new());
1807 tree.layout(SizeProposal::exact(200.0, 100.0));
1808
1809 let mut ctx = crate::widget::EventContext::new();
1810 ctx.start_drag_with_preview(
1811 source,
1812 crate::drag_payload::DragPayload::typed("p"),
1813 Box::new(FillWidget::new()),
1814 );
1815 tree.collect_from_ctx(ctx, source);
1816
1817 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(73.0, 41.0)));
1818
1819 let drag = tree.active_drag.as_ref().expect("active drag");
1820 assert!(
1821 (drag.current_position.x - 73.0).abs() < 0.01
1822 && (drag.current_position.y - 41.0).abs() < 0.01,
1823 "drag session position should track the pointer"
1824 );
1825
1826 let overlay_id = drag.preview_overlay_id.expect("preview overlay");
1827 let overlay = tree
1828 .overlay_manager()
1829 .overlay(overlay_id)
1830 .expect("overlay looked up by id");
1831 match &overlay.placement {
1832 crate::overlay::OverlayPlacement::AtPointer(p) => {
1833 assert!(
1834 (p.x - 73.0).abs() < 0.01 && (p.y - 41.0).abs() < 0.01,
1835 "preview overlay placement should follow pointer"
1836 );
1837 }
1838 other => panic!("expected AtPointer placement, got {:?}", other),
1839 }
1840 }
1841
1842 #[test]
1843 fn escape_during_hover_dismisses_preview() {
1844 let mut tree = WidgetTree::new();
1845 let source = tree.add(FillWidget::new());
1846 let _target = tree.add(
1847 FillWidget::new()
1848 .on_drag_hover(|_payload, _pos, _ctx| {
1849 crate::drag_state::DropFeedback::InsertionLine {
1850 y: 0.0,
1851 width: 100.0,
1852 }
1853 })
1854 .on_drop(|_, _, _| true),
1855 );
1856 tree.layout(SizeProposal::exact(200.0, 100.0));
1857
1858 let overlay_count_before = tree.overlay_manager().len();
1859
1860 let mut ctx = crate::widget::EventContext::new();
1861 ctx.start_drag_with_preview(
1862 source,
1863 crate::drag_payload::DragPayload::typed(0_u32),
1864 Box::new(FillWidget::new()),
1865 );
1866 tree.collect_from_ctx(ctx, source);
1867
1868 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1870
1871 assert!(tree.active_drag.is_some());
1872 assert_eq!(tree.overlay_manager().len(), overlay_count_before + 1);
1873
1874 tree.press_key(Key::Escape, Modifiers::NONE);
1876
1877 assert!(tree.active_drag.is_none(), "drag must be cancelled");
1878 assert_eq!(
1879 tree.overlay_manager().len(),
1880 overlay_count_before,
1881 "preview overlay must be dismissed after Escape"
1882 );
1883 }
1884
1885 #[test]
1886 fn active_drag_blocks_on_tap_on_other_widgets() {
1887 use std::cell::Cell;
1888 use std::rc::Rc;
1889
1890 let tap_fired = Rc::new(Cell::new(false));
1895 let tf = tap_fired.clone();
1896
1897 let mut tree = WidgetTree::new();
1898 let source = tree.add(FillWidget::new());
1899 let _other = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
1900 tf.set(true);
1901 }));
1902 tree.layout(SizeProposal::exact(200.0, 100.0));
1903
1904 let mut ctx = crate::widget::EventContext::new();
1905 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1906 tree.collect_from_ctx(ctx, source);
1907
1908 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1911 tree.dispatch_event(WidgetEvent::pointer_up(
1912 Point::new(150.0, 50.0),
1913 PointerButton::Primary,
1914 Modifiers::NONE,
1915 ));
1916
1917 assert!(
1918 !tap_fired.get(),
1919 "on_tap must not fire during an active drag"
1920 );
1921 }
1922
1923 #[test]
1926 fn on_drag_leave_fires_when_pointer_leaves_target_bounds() {
1927 use crate::test_widgets::InsetWidget;
1933 use std::cell::Cell;
1934 use std::rc::Rc;
1935
1936 let leave = Rc::new(Cell::new(0_u32));
1937 let l = leave.clone();
1938
1939 let mut tree = WidgetTree::new();
1940 let source = tree.add(FillWidget::new());
1941 let target = tree.add(
1942 FillWidget::new()
1943 .on_drag_hover(
1944 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1945 y: 0.0,
1946 width: 10.0,
1947 },
1948 )
1949 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1950 .on_drop(|_, _, _| true),
1951 );
1952 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
1953 tree.layout(SizeProposal::exact(200.0, 100.0));
1954
1955 let mut ctx = crate::widget::EventContext::new();
1956 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1957 tree.collect_from_ctx(ctx, source);
1958
1959 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1961 assert_eq!(leave.get(), 0, "no leave yet — target just became active");
1962
1963 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
1968 assert_eq!(
1969 leave.get(),
1970 1,
1971 "on_drag_leave fires when pointer exits the target's bounds"
1972 );
1973
1974 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1976 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1977 assert_eq!(
1978 leave.get(),
1979 1,
1980 "leave fires at most once per leave transition"
1981 );
1982
1983 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
1985 assert_eq!(leave.get(), 2);
1986 }
1987
1988 #[test]
1989 fn on_drag_leave_fires_on_drop() {
1990 use std::cell::Cell;
1991 use std::rc::Rc;
1992
1993 let leave = Rc::new(Cell::new(0_u32));
1994 let l = leave.clone();
1995
1996 let mut tree = WidgetTree::new();
1997 let source = tree.add(FillWidget::new());
1998 let _target = tree.add(
1999 FillWidget::new()
2000 .on_drag_hover(
2001 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2002 y: 0.0,
2003 width: 10.0,
2004 },
2005 )
2006 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2007 .on_drop(|_, _, _| true),
2008 );
2009 tree.layout(SizeProposal::exact(200.0, 100.0));
2010
2011 let mut ctx = crate::widget::EventContext::new();
2012 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2013 tree.collect_from_ctx(ctx, source);
2014 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2015 tree.dispatch_event(WidgetEvent::pointer_up(
2016 Point::new(100.0, 50.0),
2017 PointerButton::Primary,
2018 Modifiers::NONE,
2019 ));
2020
2021 assert_eq!(leave.get(), 1, "on_drag_leave fires exactly once on drop");
2022 }
2023
2024 #[test]
2025 fn on_drag_leave_fires_on_escape_cancel() {
2026 use std::cell::Cell;
2027 use std::rc::Rc;
2028
2029 let leave = Rc::new(Cell::new(0_u32));
2030 let l = leave.clone();
2031
2032 let mut tree = WidgetTree::new();
2033 let source = tree.add(FillWidget::new());
2034 let _target = tree.add(
2035 FillWidget::new()
2036 .on_drag_hover(
2037 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2038 y: 0.0,
2039 width: 10.0,
2040 },
2041 )
2042 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2043 .on_drop(|_, _, _| true),
2044 );
2045 tree.layout(SizeProposal::exact(200.0, 100.0));
2046
2047 let mut ctx = crate::widget::EventContext::new();
2048 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2049 tree.collect_from_ctx(ctx, source);
2050 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2051 tree.press_key(Key::Escape, Modifiers::NONE);
2052
2053 assert_eq!(
2054 leave.get(),
2055 1,
2056 "Escape cancel must fire on_drag_leave on the current target"
2057 );
2058 }
2059
2060 #[test]
2061 fn on_drag_leave_fires_when_source_destroyed_mid_drag() {
2062 use std::cell::Cell;
2063 use std::rc::Rc;
2064
2065 let leave = Rc::new(Cell::new(0_u32));
2066 let l = leave.clone();
2067
2068 let mut tree = WidgetTree::new();
2069 let source = tree.add(FillWidget::new());
2070 let _target = tree.add(
2071 FillWidget::new()
2072 .on_drag_hover(
2073 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2074 y: 0.0,
2075 width: 10.0,
2076 },
2077 )
2078 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2079 .on_drop(|_, _, _| true),
2080 );
2081 tree.layout(SizeProposal::exact(200.0, 100.0));
2082
2083 let mut ctx = crate::widget::EventContext::new();
2084 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2085 tree.collect_from_ctx(ctx, source);
2086 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2087
2088 tree.arena.destroy(source);
2089 tree.layout(SizeProposal::exact(200.0, 100.0));
2092
2093 assert!(
2094 tree.active_drag.is_none(),
2095 "active drag should have been cancelled"
2096 );
2097 assert_eq!(
2098 leave.get(),
2099 1,
2100 "on_drag_leave fires on the drop target when the source is torn down"
2101 );
2102 }
2103
2104 #[test]
2105 fn on_drag_tick_fires_per_layout_pass() {
2106 use std::cell::Cell;
2107 use std::rc::Rc;
2108
2109 let ticks = Rc::new(Cell::new(0_u32));
2110 let t = ticks.clone();
2111
2112 let mut tree = WidgetTree::new();
2113 let source = tree.add(FillWidget::new());
2114 let _target = tree.add(
2115 FillWidget::new()
2116 .on_drag_hover(
2117 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2118 y: 0.0,
2119 width: 10.0,
2120 },
2121 )
2122 .on_drag_tick(move |_pos, _ctx| t.set(t.get() + 1))
2123 .on_drop(|_, _, _| true),
2124 );
2125 tree.layout(SizeProposal::exact(200.0, 100.0));
2126
2127 let mut ctx = crate::widget::EventContext::new();
2128 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2129 tree.collect_from_ctx(ctx, source);
2130 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2132 assert_eq!(ticks.get(), 0, "tick shouldn't have fired yet");
2133
2134 tree.layout(SizeProposal::exact(200.0, 100.0));
2135 assert_eq!(ticks.get(), 1);
2136 tree.layout(SizeProposal::exact(200.0, 100.0));
2137 tree.layout(SizeProposal::exact(200.0, 100.0));
2138 assert_eq!(ticks.get(), 3);
2139
2140 tree.dispatch_event(WidgetEvent::pointer_up(
2142 Point::new(100.0, 50.0),
2143 PointerButton::Primary,
2144 Modifiers::NONE,
2145 ));
2146 let after_drop = ticks.get();
2147 tree.layout(SizeProposal::exact(200.0, 100.0));
2148 tree.layout(SizeProposal::exact(200.0, 100.0));
2149 assert_eq!(
2150 ticks.get(),
2151 after_drop,
2152 "on_drag_tick must not fire after drag ends"
2153 );
2154 }
2155
2156 #[test]
2157 fn on_drag_hover_and_on_drop_receive_widget_local_coordinates() {
2158 use crate::test_widgets::InsetWidget;
2165 use std::cell::Cell;
2166 use std::rc::Rc;
2167
2168 let hover_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2169 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2170 let h = hover_local.clone();
2171 let d = drop_local.clone();
2172
2173 let mut tree = WidgetTree::new();
2174 let source = tree.add(FillWidget::new());
2175 let target = tree.add(
2177 FillWidget::new()
2178 .on_drag_hover(move |_p, pos, _ctx| {
2179 h.set(pos);
2180 crate::drag_state::DropFeedback::InsertionLine {
2181 y: 0.0,
2182 width: 10.0,
2183 }
2184 })
2185 .on_drop(move |_payload, pos, _ctx| {
2186 d.set(pos);
2187 true
2188 }),
2189 );
2190 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2191 tree.layout(SizeProposal::exact(200.0, 100.0));
2192
2193 let mut ctx = crate::widget::EventContext::new();
2194 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2195 tree.collect_from_ctx(ctx, source);
2196
2197 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 60.0)));
2201 let hov = hover_local.get();
2202 assert!(
2203 (hov.x - 60.0).abs() < 0.01 && (hov.y - 20.0).abs() < 0.01,
2204 "on_drag_hover should receive local coords, got {:?}",
2205 hov,
2206 );
2207
2208 tree.dispatch_event(WidgetEvent::pointer_up(
2210 Point::new(110.0, 55.0),
2211 PointerButton::Primary,
2212 Modifiers::NONE,
2213 ));
2214 let drp = drop_local.get();
2215 assert!(
2216 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2217 "on_drop should receive local coords, got {:?}",
2218 drp,
2219 );
2220 }
2221
2222 #[test]
2223 fn active_drag_sets_grabbing_cursor() {
2224 let mut tree = WidgetTree::new();
2229 let source = tree.add(FillWidget::new());
2230 tree.layout(SizeProposal::exact(100.0, 50.0));
2231
2232 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2233
2234 let mut ctx = crate::widget::EventContext::new();
2235 ctx.start_drag_with_preview(
2236 source,
2237 crate::drag_payload::DragPayload::typed(0_u32),
2238 Box::new(FillWidget::new()),
2239 );
2240 tree.collect_from_ctx(ctx, source);
2241 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2242
2243 tree.dispatch_event(WidgetEvent::pointer_up(
2245 Point::new(50.0, 25.0),
2246 PointerButton::Primary,
2247 Modifiers::NONE,
2248 ));
2249 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2250 }
2251
2252 #[test]
2253 fn escape_cancel_resets_cursor() {
2254 let mut tree = WidgetTree::new();
2255 let source = tree.add(FillWidget::new());
2256 tree.layout(SizeProposal::exact(100.0, 50.0));
2257
2258 let mut ctx = crate::widget::EventContext::new();
2259 ctx.start_drag_with_preview(
2260 source,
2261 crate::drag_payload::DragPayload::typed(0_u32),
2262 Box::new(FillWidget::new()),
2263 );
2264 tree.collect_from_ctx(ctx, source);
2265 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2266
2267 tree.press_key(Key::Escape, Modifiers::NONE);
2268 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2269 }
2270
2271 #[test]
2272 fn drag_preview_composite_gets_built() {
2273 use std::cell::Cell;
2279 use std::rc::Rc;
2280
2281 let built = Rc::new(Cell::new(false));
2282 let b = built.clone();
2283
2284 #[derive(Debug)]
2285 struct CheckingWidget {
2286 built: Rc<Cell<bool>>,
2287 }
2288 impl Widget for CheckingWidget {
2289 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2290 self.built.set(true);
2291 Vec::new()
2292 }
2293 fn layout_response(
2294 &self,
2295 _: SizeProposal,
2296 _: &crate::widget::LayoutContext,
2297 ) -> crate::widget::LayoutResponse {
2298 teksilo_canvas::Size::new(50.0, 20.0).into()
2299 }
2300 }
2301
2302 let mut tree = WidgetTree::new();
2303 let source = tree.add(FillWidget::new());
2304 tree.layout(SizeProposal::exact(200.0, 100.0));
2305
2306 let mut ctx = crate::widget::EventContext::new();
2307 ctx.start_drag_with_preview(
2308 source,
2309 crate::drag_payload::DragPayload::typed(0_u32),
2310 Box::new(CheckingWidget { built: b }),
2311 );
2312 tree.collect_from_ctx(ctx, source);
2313
2314 assert!(built.get(), "preview's build() must fire on drag start");
2315 }
2316
2317 #[test]
2318 fn preview_placement_drives_layout_needs() {
2319 let mut tree = WidgetTree::new();
2327 let source = tree.add(FillWidget::new());
2328 tree.layout(SizeProposal::exact(200.0, 200.0));
2329
2330 let mut ctx = crate::widget::EventContext::new();
2331 ctx.start_drag_with_preview(
2332 source,
2333 crate::drag_payload::DragPayload::typed(0_u32),
2334 Box::new(FillWidget::new()),
2335 );
2336 tree.collect_from_ctx(ctx, source);
2337
2338 assert!(
2341 tree.needs_layout(),
2342 "drag start must mark preview content for layout"
2343 );
2344 tree.layout(SizeProposal::exact(200.0, 200.0));
2345 assert!(
2346 !tree.needs_layout(),
2347 "layout should have cleared dirty flag"
2348 );
2349
2350 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(75.0, 120.0)));
2353 assert!(
2354 tree.needs_layout(),
2355 "PointerMove during drag must mark preview for layout"
2356 );
2357 }
2358
2359 #[test]
2360 fn scroll_during_drag_routes_to_drop_target() {
2361 use std::cell::Cell;
2362 use std::rc::Rc;
2363
2364 let scroll_count = Rc::new(Cell::new(0_u32));
2365 let sc = scroll_count.clone();
2366
2367 let mut tree = WidgetTree::new();
2368 let source = tree.add(FillWidget::new());
2369 let _target = tree.add(
2370 FillWidget::new()
2371 .on_drag_hover(
2372 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2373 y: 0.0,
2374 width: 10.0,
2375 },
2376 )
2377 .on_scroll(move |event, _ctx| match event {
2378 WidgetEvent::Scroll { .. } => {
2379 sc.set(sc.get() + 1);
2380 EventResponse::Handled
2381 }
2382 _ => EventResponse::Ignored,
2383 })
2384 .on_drop(|_, _, _| true),
2385 );
2386 tree.layout(SizeProposal::exact(200.0, 100.0));
2387
2388 let mut ctx = crate::widget::EventContext::new();
2389 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2390 tree.collect_from_ctx(ctx, source);
2391 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2393
2394 tree.dispatch_event(WidgetEvent::scroll(
2397 crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
2398 Default::default(),
2399 ));
2400 assert_eq!(
2401 scroll_count.get(),
2402 1,
2403 "Scroll during drag must route to the current drop target"
2404 );
2405 }
2406
2407 #[test]
2410 fn external_drop_delivers_files_and_marks_external() {
2411 use crate::drag_payload::ExternalDropData;
2412 use std::cell::RefCell;
2413 use std::path::PathBuf;
2414 use std::rc::Rc;
2415
2416 let dropped_files: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
2417 let was_external = Rc::new(std::cell::Cell::new(false));
2418 let df = dropped_files.clone();
2419 let we = was_external.clone();
2420
2421 let mut tree = WidgetTree::new();
2422 let _target = tree.add(
2423 FillWidget::new()
2424 .on_drag_hover(|payload, _pos, _ctx| {
2425 if payload.is_external() && !payload.files().is_empty() {
2427 crate::drag_state::DropFeedback::HighlightRect {
2428 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2429 color: teksilo_tokens::Color::WHITE,
2430 }
2431 } else {
2432 crate::drag_state::DropFeedback::NoFeedback
2433 }
2434 })
2435 .on_drop(move |payload, _pos, _ctx| {
2436 we.set(payload.is_external());
2437 *df.borrow_mut() = payload.files().to_vec();
2438 true
2439 }),
2440 );
2441 tree.layout(SizeProposal::exact(200.0, 100.0));
2442
2443 let mut noop = crate::window::NoopWindowOps;
2444 let data = ExternalDropData {
2445 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2446 ..Default::default()
2447 };
2448 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2449 assert!(tree.active_drag.is_some());
2450 assert!(tree.active_drag.as_ref().unwrap().is_external);
2451
2452 tree.update_external_drag(Point::new(110.0, 55.0), &mut noop);
2453 let drop_data = ExternalDropData {
2455 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2456 ..Default::default()
2457 };
2458 tree.end_external_drag(Point::new(110.0, 55.0), drop_data, &mut noop);
2459
2460 assert!(
2461 tree.active_drag.is_none(),
2462 "external drag must clear on drop"
2463 );
2464 assert!(was_external.get(), "payload should report external origin");
2465 assert_eq!(
2466 *dropped_files.borrow(),
2467 vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2468 );
2469 }
2470
2471 #[test]
2472 fn external_drop_passes_local_coordinates() {
2473 use crate::drag_payload::ExternalDropData;
2474 use crate::test_widgets::InsetWidget;
2475 use std::cell::Cell;
2476 use std::path::PathBuf;
2477 use std::rc::Rc;
2478
2479 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2480 let d = drop_local.clone();
2481
2482 let mut tree = WidgetTree::new();
2483 let target = tree.add(
2485 FillWidget::new()
2486 .on_drag_hover(
2487 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2488 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2489 color: teksilo_tokens::Color::WHITE,
2490 },
2491 )
2492 .on_drop(move |_payload, pos, _ctx| {
2493 d.set(pos);
2494 true
2495 }),
2496 );
2497 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2498 tree.layout(SizeProposal::exact(200.0, 100.0));
2499
2500 let mut noop = crate::window::NoopWindowOps;
2501 let data = ExternalDropData {
2502 files: vec![PathBuf::from("/tmp/x")],
2503 ..Default::default()
2504 };
2505 tree.begin_external_drag(Point::new(110.0, 55.0), data, &mut noop);
2507 tree.end_external_drag(
2508 Point::new(110.0, 55.0),
2509 ExternalDropData::default(),
2510 &mut noop,
2511 );
2512
2513 let drp = drop_local.get();
2514 assert!(
2515 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2516 "external on_drop should receive local coords, got {:?}",
2517 drp,
2518 );
2519 }
2520
2521 #[test]
2522 fn cancel_external_drag_clears_session_and_fires_leave() {
2523 use crate::drag_payload::ExternalDropData;
2524 use std::cell::Cell;
2525 use std::path::PathBuf;
2526 use std::rc::Rc;
2527
2528 let left = Rc::new(Cell::new(0_u32));
2529 let l = left.clone();
2530
2531 let mut tree = WidgetTree::new();
2532 let _target = tree.add(
2533 FillWidget::new()
2534 .on_drag_hover(
2535 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2536 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2537 color: teksilo_tokens::Color::WHITE,
2538 },
2539 )
2540 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2541 .on_drop(|_, _, _| true),
2542 );
2543 tree.layout(SizeProposal::exact(200.0, 100.0));
2544
2545 let mut noop = crate::window::NoopWindowOps;
2546 let data = ExternalDropData {
2547 files: vec![PathBuf::from("/tmp/x")],
2548 ..Default::default()
2549 };
2550 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2551 assert!(tree.active_drag.is_some());
2552
2553 tree.cancel_external_drag(&mut noop);
2554 assert!(tree.active_drag.is_none(), "cancel must clear the session");
2555 assert_eq!(
2556 left.get(),
2557 1,
2558 "cancel must fire on_drag_leave on the target"
2559 );
2560 }
2561
2562 #[test]
2563 fn external_drag_helpers_noop_without_session() {
2564 let mut tree = WidgetTree::new();
2566 let _t = tree.add(FillWidget::new());
2567 tree.layout(SizeProposal::exact(100.0, 50.0));
2568
2569 let mut noop = crate::window::NoopWindowOps;
2570 tree.update_external_drag(Point::new(10.0, 10.0), &mut noop);
2571 tree.end_external_drag(
2572 Point::new(10.0, 10.0),
2573 crate::drag_payload::ExternalDropData::default(),
2574 &mut noop,
2575 );
2576 tree.cancel_external_drag(&mut noop);
2577 assert!(tree.active_drag.is_none());
2578 }
2579
2580 struct RecordingWindowOps {
2585 started: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OutboundDragData>>>,
2586 started_kinds: std::rc::Rc<std::cell::RefCell<Vec<teksilo_tokens::PointerKind>>>,
2588 succeed: bool,
2589 cancels: std::rc::Rc<std::cell::Cell<usize>>,
2590 accepts: std::rc::Rc<std::cell::RefCell<Vec<bool>>>,
2592 }
2593
2594 impl RecordingWindowOps {
2595 fn new(succeed: bool) -> Self {
2596 Self {
2597 started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2598 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2599 succeed,
2600 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2601 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2602 }
2603 }
2604 }
2605 impl crate::window::WindowOps for RecordingWindowOps {
2606 fn open_window(
2607 &mut self,
2608 _c: crate::window::WindowConfig,
2609 ) -> crate::window::TeksiloWindowId {
2610 panic!("not used in these tests")
2611 }
2612 fn find_window(&self, _s: &str) -> Option<crate::window::TeksiloWindowId> {
2613 None
2614 }
2615 fn window_state(
2616 &self,
2617 _id: crate::window::TeksiloWindowId,
2618 ) -> Option<crate::window::WindowState> {
2619 None
2620 }
2621 fn windows(&self) -> Vec<crate::window::WindowState> {
2622 Vec::new()
2623 }
2624 fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
2625 fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
2626 fn begin_os_drag(
2627 &mut self,
2628 data: crate::drag_payload::OutboundDragData,
2629 _image: Option<crate::drag_payload::DragImageData>,
2630 pointer: teksilo_tokens::PointerKind,
2631 ) -> bool {
2632 self.started.borrow_mut().push(data);
2633 self.started_kinds.borrow_mut().push(pointer);
2634 self.succeed
2635 }
2636 fn cancel_os_drag(&mut self) {
2637 self.cancels.set(self.cancels.get() + 1);
2638 }
2639 fn set_drop_accepted(&mut self, accepted: bool) {
2640 self.accepts.borrow_mut().push(accepted);
2641 }
2642 }
2643
2644 fn exportable_payload() -> crate::drag_payload::DragPayload {
2645 crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"hi".to_vec())
2646 }
2647
2648 #[test]
2649 fn internal_exportable_drag_escalates_when_leaving_window() {
2650 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2651 let mut ops = RecordingWindowOps {
2652 started: started.clone(),
2653 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2654 succeed: true,
2655 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2656 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2657 };
2658
2659 let mut tree = WidgetTree::new();
2660 let source = tree.add(FillWidget::new());
2661 tree.layout(SizeProposal::exact(200.0, 100.0));
2662
2663 let mut ctx = crate::widget::EventContext::new();
2664 ctx.start_drag(source, exportable_payload());
2665 tree.collect_from_ctx(ctx, source);
2666 assert!(tree.active_drag.is_some());
2667
2668 tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
2670 assert!(started.borrow().is_empty());
2671 assert!(tree.active_drag.is_some());
2672
2673 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2675 assert_eq!(started.borrow().len(), 1, "begin_os_drag called once");
2676 assert!(
2677 started.borrow()[0].mime.contains_key("text/plain"),
2678 "outbound data carries the payload's mime"
2679 );
2680 assert!(tree.active_drag.is_none(), "in-app session torn down");
2681 assert_eq!(tree.outbound_drag_source, Some(source));
2682 }
2683
2684 #[test]
2685 fn os_drag_ended_fires_source_on_drag_ended() {
2686 use crate::drag_payload::DropOutcome;
2687 use std::cell::Cell;
2688 use std::rc::Rc;
2689
2690 let outcome = Rc::new(Cell::new(None));
2691 let o = outcome.clone();
2692
2693 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2694 let mut ops = RecordingWindowOps {
2695 started,
2696 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2697 succeed: true,
2698 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2699 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2700 };
2701
2702 let mut tree = WidgetTree::new();
2703 let source =
2704 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2705 tree.layout(SizeProposal::exact(200.0, 100.0));
2706
2707 let mut ctx = crate::widget::EventContext::new();
2708 ctx.start_drag(source, exportable_payload());
2709 tree.collect_from_ctx(ctx, source);
2710 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2712
2713 tree.handle_os_drag_ended(DropOutcome::OsMove, &mut ops);
2714 assert_eq!(outcome.get(), Some(DropOutcome::OsMove));
2715 assert!(
2716 tree.outbound_drag_source.is_none(),
2717 "cleared after delivery"
2718 );
2719 }
2720
2721 #[test]
2722 fn escape_during_an_escalated_drag_asks_the_platform_to_cancel() {
2723 let mut ops = RecordingWindowOps::new(true);
2728 let cancels = ops.cancels.clone();
2729
2730 let mut tree = WidgetTree::new();
2731 let source = tree.add(FillWidget::new());
2732 tree.layout(SizeProposal::exact(200.0, 100.0));
2733
2734 let mut ctx = crate::widget::EventContext::new();
2735 ctx.start_drag(source, exportable_payload());
2736 tree.collect_from_ctx(ctx, source);
2737 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2739
2740 tree.dispatch_event_with_ops(
2741 crate::event::WidgetEvent::KeyDown {
2742 key: crate::event::Key::Escape,
2743 modifiers: crate::event::Modifiers::NONE,
2744 text: None,
2745 },
2746 &mut ops,
2747 );
2748 assert_eq!(cancels.get(), 1, "the platform must be asked to cancel");
2749 assert_eq!(
2750 tree.outbound_drag_source,
2751 Some(source),
2752 "the session stays until the backend reports its terminal outcome — \
2753 tearing it down here would drop the source's on_drag_ended"
2754 );
2755 }
2756
2757 #[test]
2758 fn escape_without_an_os_drag_does_not_touch_the_platform() {
2759 let mut ops = RecordingWindowOps::new(true);
2760 let cancels = ops.cancels.clone();
2761
2762 let mut tree = WidgetTree::new();
2763 tree.add(FillWidget::new());
2764 tree.layout(SizeProposal::exact(200.0, 100.0));
2765
2766 tree.dispatch_event_with_ops(
2767 crate::event::WidgetEvent::KeyDown {
2768 key: crate::event::Key::Escape,
2769 modifiers: crate::event::Modifiers::NONE,
2770 text: None,
2771 },
2772 &mut ops,
2773 );
2774 assert_eq!(cancels.get(), 0);
2775 }
2776
2777 #[test]
2778 fn no_backend_keeps_session_active_on_leave() {
2779 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2782 let mut ops = RecordingWindowOps {
2783 started: started.clone(),
2784 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2785 succeed: false,
2786 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2787 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2788 };
2789
2790 let mut tree = WidgetTree::new();
2791 let source = tree.add(FillWidget::new());
2792 tree.layout(SizeProposal::exact(200.0, 100.0));
2793
2794 let mut ctx = crate::widget::EventContext::new();
2795 ctx.start_drag(source, exportable_payload());
2796 tree.collect_from_ctx(ctx, source);
2797 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2798
2799 assert_eq!(started.borrow().len(), 1, "escalation was attempted");
2800 assert!(tree.active_drag.is_some(), "session kept (no backend)");
2801 assert!(tree.outbound_drag_source.is_none());
2802 }
2803
2804 #[test]
2805 fn non_exportable_drag_does_not_escalate() {
2806 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2807 let mut ops = RecordingWindowOps {
2808 started: started.clone(),
2809 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2810 succeed: true,
2811 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2812 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2813 };
2814
2815 let mut tree = WidgetTree::new();
2816 let source = tree.add(FillWidget::new());
2817 tree.layout(SizeProposal::exact(200.0, 100.0));
2818
2819 let mut ctx = crate::widget::EventContext::new();
2821 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u32));
2822 tree.collect_from_ctx(ctx, source);
2823 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2824
2825 assert!(started.borrow().is_empty(), "no escalation attempt");
2826 assert!(tree.active_drag.is_some(), "session unaffected");
2827 }
2828
2829 #[test]
2830 fn in_app_drop_fires_source_on_drag_ended_with_accepted() {
2831 use crate::drag_payload::DropOutcome;
2832 use std::cell::Cell;
2833 use std::rc::Rc;
2834
2835 let outcome = Rc::new(Cell::new(None));
2836 let o = outcome.clone();
2837
2838 let mut tree = WidgetTree::new();
2839 let source =
2840 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2841 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
2842 tree.layout(SizeProposal::exact(200.0, 100.0));
2843
2844 let mut ctx = crate::widget::EventContext::new();
2845 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
2846 tree.collect_from_ctx(ctx, source);
2847
2848 tree.dispatch_event(WidgetEvent::pointer_up(
2849 Point::new(150.0, 50.0),
2850 PointerButton::Primary,
2851 Modifiers::NONE,
2852 ));
2853
2854 assert_eq!(outcome.get(), Some(DropOutcome::InApp { accepted: true }));
2855 }
2856
2857 #[test]
2858 fn escape_fires_source_on_drag_ended_cancelled() {
2859 use crate::drag_payload::DropOutcome;
2860 use std::cell::Cell;
2861 use std::rc::Rc;
2862
2863 let outcome = Rc::new(Cell::new(None));
2864 let o = outcome.clone();
2865
2866 let mut tree = WidgetTree::new();
2867 let source =
2868 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2869 tree.layout(SizeProposal::exact(200.0, 100.0));
2870
2871 let mut ctx = crate::widget::EventContext::new();
2872 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2873 tree.collect_from_ctx(ctx, source);
2874
2875 tree.press_key(Key::Escape, Modifiers::NONE);
2876 assert_eq!(outcome.get(), Some(DropOutcome::Cancelled));
2877 }
2878
2879 #[test]
2884 fn os_drag_reentry_recovers_typed_payload_for_in_app_drop() {
2885 use crate::drag_payload::{DragPayload, DropOutcome};
2886 use std::cell::Cell;
2887 use std::rc::Rc;
2888
2889 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2890 let mut ops = RecordingWindowOps {
2891 started,
2892 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2893 succeed: true,
2894 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2895 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2896 };
2897
2898 let got_typed = Rc::new(Cell::new(0_u32));
2899 let ended = Rc::new(Cell::new(0_u32));
2900 let last_outcome = Rc::new(Cell::new(None));
2901 let g = got_typed.clone();
2902 let e = ended.clone();
2903 let lo = last_outcome.clone();
2904
2905 let mut tree = WidgetTree::new();
2906 let source = tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| {
2907 e.set(e.get() + 1);
2908 lo.set(Some(outcome));
2909 }));
2910 let _target =
2911 tree.add(
2912 FillWidget::new().on_drop(move |mut p, _, _| match p.take_typed::<u32>() {
2913 Some(v) => {
2914 g.set(v);
2915 true
2916 }
2917 None => false,
2918 }),
2919 );
2920 tree.layout(SizeProposal::exact(200.0, 100.0));
2921
2922 let mut ctx = crate::widget::EventContext::new();
2924 ctx.start_drag(
2925 source,
2926 DragPayload::typed(123_u32).with_mime("text/plain", b"x".to_vec()),
2927 );
2928 tree.collect_from_ctx(ctx, source);
2929
2930 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2932 assert!(tree.active_drag.is_none());
2933 assert!(
2934 super::has_outbound_typed(),
2935 "typed payload stashed globally"
2936 );
2937
2938 tree.begin_external_drag(
2941 Point::new(100.0, 50.0),
2942 crate::drag_payload::ExternalDropData::default(),
2943 &mut ops,
2944 );
2945 let d = tree.active_drag.as_ref().expect("re-entered session");
2946 assert!(!d.is_external, "re-entry is an internal session");
2947 assert!(d.payload.has_typed::<u32>(), "typed payload recovered");
2948 assert_eq!(
2949 d.payload.text(),
2950 Some("x"),
2951 "external view enriched from MIME so DropZone-style targets also accept"
2952 );
2953 assert!(!super::has_outbound_typed(), "stash taken by the re-entry");
2954
2955 tree.end_external_drag(
2957 Point::new(150.0, 50.0),
2958 crate::drag_payload::ExternalDropData::default(),
2959 &mut ops,
2960 );
2961 assert_eq!(got_typed.get(), 123, "target received the typed payload");
2962 assert_eq!(
2963 ended.get(),
2964 0,
2965 "source on_drag_ended not fired by the drop itself"
2966 );
2967
2968 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2971 assert_eq!(ended.get(), 1, "on_drag_ended fired exactly once");
2972 assert_eq!(last_outcome.get(), Some(DropOutcome::OsCopy));
2973 }
2974
2975 #[test]
2979 fn os_drag_reentry_recovers_typed_payload_across_windows() {
2980 use crate::drag_payload::{DragPayload, DropOutcome};
2981 use std::cell::Cell;
2982 use std::rc::Rc;
2983
2984 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2985 let mut ops = RecordingWindowOps {
2986 started,
2987 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2988 succeed: true,
2989 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2990 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2991 };
2992
2993 let mut tree_a = WidgetTree::new();
2995 let src = tree_a.add(FillWidget::new());
2996 tree_a.layout(SizeProposal::exact(200.0, 100.0));
2997 let mut ctx = crate::widget::EventContext::new();
2998 ctx.start_drag(
2999 src,
3000 DragPayload::typed(77_u32).with_mime("text/plain", b"x".to_vec()),
3001 );
3002 tree_a.collect_from_ctx(ctx, src);
3003 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3004 assert!(super::has_outbound_typed());
3005 assert_eq!(tree_a.outbound_drag_source, Some(src));
3006
3007 let got = Rc::new(Cell::new(0_u32));
3010 let g = got.clone();
3011 let mut tree_b = WidgetTree::new();
3012 let _t = tree_b.add(FillWidget::new().on_drop(
3013 move |mut p, _, _| match p.take_typed::<u32>() {
3014 Some(v) => {
3015 g.set(v);
3016 true
3017 }
3018 None => false,
3019 },
3020 ));
3021 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3022
3023 tree_b.begin_external_drag(
3024 Point::new(50.0, 50.0),
3025 crate::drag_payload::ExternalDropData::default(),
3026 &mut ops,
3027 );
3028 assert!(
3029 tree_b
3030 .active_drag
3031 .as_ref()
3032 .is_some_and(|d| d.payload.has_typed::<u32>()),
3033 "window B recovered the typed payload"
3034 );
3035 tree_b.end_external_drag(
3036 Point::new(50.0, 50.0),
3037 crate::drag_payload::ExternalDropData::default(),
3038 &mut ops,
3039 );
3040 assert_eq!(
3041 got.get(),
3042 77,
3043 "window B's target received the typed payload"
3044 );
3045
3046 tree_a.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
3048 }
3049
3050 #[test]
3054 fn os_drag_reexit_restashes_payload() {
3055 use crate::drag_payload::DragPayload;
3056 use std::rc::Rc;
3057
3058 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3059 let mut ops = RecordingWindowOps {
3060 started: started.clone(),
3061 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3062 succeed: true,
3063 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3064 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3065 };
3066
3067 let mut tree = WidgetTree::new();
3068 let source = tree.add(FillWidget::new());
3069 tree.layout(SizeProposal::exact(200.0, 100.0));
3070 let mut ctx = crate::widget::EventContext::new();
3071 ctx.start_drag(
3072 source,
3073 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3074 );
3075 tree.collect_from_ctx(ctx, source);
3076
3077 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3079 assert_eq!(started.borrow().len(), 1, "OS drag started once");
3080 tree.begin_external_drag(
3081 Point::new(100.0, 50.0),
3082 crate::drag_payload::ExternalDropData::default(),
3083 &mut ops,
3084 );
3085 assert!(tree.os_drag_reentered);
3086 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(!tree.os_drag_reentered, "re-exited");
3089 assert!(tree.active_drag.is_none(), "session torn down on re-exit");
3090 assert!(super::has_outbound_typed(), "payload re-stashed");
3091 assert_eq!(
3092 started.borrow().len(),
3093 1,
3094 "no second OS drag started on re-exit"
3095 );
3096 }
3097
3098 #[test]
3102 fn source_window_close_clears_stash_no_hijack() {
3103 use crate::drag_payload::{DragPayload, ExternalDropData};
3104 use std::path::PathBuf;
3105 use std::rc::Rc;
3106
3107 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3108 let mut ops = RecordingWindowOps {
3109 started,
3110 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3111 succeed: true,
3112 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3113 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3114 };
3115
3116 let mut tree = WidgetTree::new();
3117 let source = tree.add(FillWidget::new());
3118 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
3119 tree.layout(SizeProposal::exact(200.0, 100.0));
3120 let mut ctx = crate::widget::EventContext::new();
3121 ctx.start_drag(
3122 source,
3123 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3124 );
3125 tree.collect_from_ctx(ctx, source);
3126 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(super::has_outbound_typed());
3128 assert_eq!(tree.outbound_drag_source, Some(source));
3129
3130 tree.abort_outbound_drag();
3132 assert!(
3133 !super::has_outbound_typed(),
3134 "stash cleared when the source window closes"
3135 );
3136 assert!(tree.outbound_drag_source.is_none());
3137
3138 tree.begin_external_drag(
3141 Point::new(50.0, 50.0),
3142 ExternalDropData {
3143 files: vec![PathBuf::from("/tmp/real")],
3144 ..Default::default()
3145 },
3146 &mut ops,
3147 );
3148 let d = tree.active_drag.as_ref().expect("external session");
3149 assert!(
3150 d.is_external,
3151 "stale stash did not hijack the new external drag"
3152 );
3153 assert!(
3154 !d.payload.has_typed::<u32>(),
3155 "no stale typed payload leaked in"
3156 );
3157 assert_eq!(d.payload.files(), &[PathBuf::from("/tmp/real")]);
3158 }
3159
3160 use std::cell::{Cell, RefCell};
3165 use std::rc::Rc;
3166
3167 fn drag_arming_leaf(
3175 slot: Rc<Cell<Option<WidgetId>>>,
3176 payload: impl Fn() -> crate::drag_payload::DragPayload + 'static,
3177 ) -> impl crate::widget::Widget {
3178 let armed = Cell::new(false);
3179 FillWidget::new().on_pointer_event(move |event, ctx| {
3180 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3181 && !armed.replace(true)
3182 && let Some(id) = slot.get()
3183 {
3184 ctx.start_drag(id, payload());
3185 }
3186 crate::event::EventResponse::Ignored
3187 })
3188 }
3189
3190 fn touch_press(
3192 tree: &mut WidgetTree,
3193 at: Point,
3194 ops: &mut dyn crate::window::WindowOps,
3195 ) -> crate::pointer::PointerId {
3196 use crate::pointer::{
3197 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
3198 };
3199 use std::sync::atomic::{AtomicU64, Ordering};
3200 static NEXT: AtomicU64 = AtomicU64::new(91_000);
3201 let contact = PointerIdAllocator::global().begin(
3202 BackendDeviceKey::DEFAULT,
3203 NEXT.fetch_add(1, Ordering::Relaxed),
3204 );
3205 tree.dispatch_pointer_with_ops(
3206 PointerSample {
3207 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
3208 phase: PointerPhase::Down,
3209 position: at,
3210 button: None,
3211 modifiers: Modifiers::NONE,
3212 coalesced: Vec::new(),
3213 },
3214 ops,
3215 );
3216 contact
3217 }
3218
3219 fn placement_of(
3221 tree: &WidgetTree,
3222 id: crate::overlay::OverlayId,
3223 ) -> crate::overlay::OverlayPlacement {
3224 tree.overlay_manager
3225 .stack
3226 .iter()
3227 .find(|o| o.id == id)
3228 .map(|o| o.placement.clone())
3229 .expect("the overlay is in the stack")
3230 }
3231
3232 #[test]
3240 fn a_drag_tick_reports_the_device_that_started_the_drag() {
3241 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3242 let s = seen.clone();
3243
3244 let mut tree = WidgetTree::new();
3245 let target = tree.add(
3246 FillWidget::new()
3247 .on_drop(|_, _, _| true)
3248 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3249 );
3250 tree.layout(SizeProposal::exact(200.0, 100.0));
3251
3252 let mut ctx = crate::widget::EventContext::new();
3254 ctx.start_drag(target, crate::drag_payload::DragPayload::typed(1_u8));
3255 tree.collect_from_ctx(ctx, target);
3256 tree.handle_drag_move(Point::new(50.0, 50.0), &mut crate::window::NoopWindowOps);
3257 tree.layout(SizeProposal::exact(200.0, 100.0));
3258 assert_eq!(
3259 *seen.borrow(),
3260 vec![teksilo_tokens::PointerKind::Mouse],
3261 "a mouse drag still reports the mouse"
3262 );
3263 tree.cancel_active_drag(&mut crate::window::NoopWindowOps);
3264 seen.borrow_mut().clear();
3265
3266 let mut ops = crate::window::NoopWindowOps;
3269 let s = seen.clone();
3270 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3271 let armed = Cell::new(false);
3272 let s2 = slot.clone();
3273 let mut tree = WidgetTree::new();
3274 let row = tree.add(
3275 FillWidget::new()
3276 .on_pointer_event(move |event, ctx| {
3277 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3278 && !armed.replace(true)
3279 && let Some(id) = s2.get()
3280 {
3281 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(2_u8));
3282 }
3283 crate::event::EventResponse::Ignored
3284 })
3285 .on_drop(|_, _, _| true)
3286 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3287 );
3288 slot.set(Some(row));
3289 tree.layout(SizeProposal::exact(200.0, 100.0));
3290 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3291 assert!(tree.active_drag.is_some(), "the finger armed a drag");
3292 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3293 tree.layout(SizeProposal::exact(200.0, 100.0));
3294 assert_eq!(
3295 *seen.borrow(),
3296 vec![teksilo_tokens::PointerKind::Touch],
3297 "and a finger drag reports the finger, from a tick that has no sample"
3298 );
3299 }
3300
3301 #[test]
3309 fn the_preview_avoids_a_coarse_contact_and_still_pins_a_cursor() {
3310 use crate::overlay::OverlayPlacement;
3311
3312 let mut ops = crate::window::NoopWindowOps;
3313 let at = Point::new(60.0, 40.0);
3314
3315 let mut tree = WidgetTree::new();
3317 let source = tree.add(FillWidget::new());
3318 tree.layout(SizeProposal::exact(200.0, 100.0));
3319 let mut ctx = crate::widget::EventContext::new();
3320 ctx.start_drag_with_preview(
3321 source,
3322 crate::drag_payload::DragPayload::typed(1_u8),
3323 Box::new(FillWidget::new()),
3324 );
3325 tree.collect_from_ctx(ctx, source);
3326 tree.handle_drag_move(at, &mut ops);
3327 let overlay = tree
3328 .active_drag
3329 .as_ref()
3330 .and_then(|d| d.preview_overlay_id)
3331 .expect("the preview overlay exists");
3332 assert!(
3333 matches!(placement_of(&tree, overlay), OverlayPlacement::AtPointer(p) if p == at),
3334 "a mouse keeps AtPointer at the reported point, unchanged",
3335 );
3336
3337 let mut tree = WidgetTree::new();
3340 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3341 let source = tree.add(drag_arming_leaf_with_preview(slot.clone()));
3342 slot.set(Some(source));
3343 tree.layout(SizeProposal::exact(200.0, 100.0));
3344 touch_press(&mut tree, at, &mut ops);
3345 assert_eq!(
3346 tree.active_drag.as_ref().map(|d| d.pointer.kind),
3347 Some(teksilo_tokens::PointerKind::Touch),
3348 "the session recorded the finger",
3349 );
3350 tree.handle_drag_move(at, &mut ops);
3351 let overlay = tree
3352 .active_drag
3353 .as_ref()
3354 .and_then(|d| d.preview_overlay_id)
3355 .expect("the preview overlay exists");
3356 match placement_of(&tree, overlay) {
3357 OverlayPlacement::AtPointerAvoiding { point, avoid } => {
3358 assert_eq!(point, at);
3359 assert!(
3360 avoid.contains(at),
3361 "the rectangle to clear is centred on the contact, so no \
3362 placement that honours it can put the preview under the hand",
3363 );
3364 }
3365 other => panic!("a coarse pointer must avoid its own contact, got {other:?}"),
3366 }
3367 }
3368
3369 fn drag_arming_leaf_with_preview(
3372 slot: Rc<Cell<Option<WidgetId>>>,
3373 ) -> impl crate::widget::Widget {
3374 let armed = Cell::new(false);
3375 FillWidget::new().on_pointer_event(move |event, ctx| {
3376 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3377 && !armed.replace(true)
3378 && let Some(id) = slot.get()
3379 {
3380 ctx.start_drag_with_preview(
3381 id,
3382 crate::drag_payload::DragPayload::typed(1_u8),
3383 Box::new(FillWidget::new()),
3384 );
3385 }
3386 crate::event::EventResponse::Ignored
3387 })
3388 }
3389
3390 #[test]
3399 fn an_os_drag_started_by_a_finger_cancels_that_finger() {
3400 let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
3401 Rc::new(RefCell::new(Vec::new()));
3402 let c = cancelled.clone();
3403
3404 let mut ops = RecordingWindowOps::new(true);
3405 let kinds = ops.started_kinds.clone();
3406
3407 let mut tree = WidgetTree::new();
3408 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3409 let armed = Cell::new(false);
3410 let s2 = slot.clone();
3411 let source = tree.add(
3412 FillWidget::new()
3413 .on_pointer_event(move |event, ctx| {
3414 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3415 && !armed.replace(true)
3416 && let Some(id) = s2.get()
3417 {
3418 ctx.start_drag(id, exportable_payload());
3419 }
3420 crate::event::EventResponse::Ignored
3421 })
3422 .on_pointer_cancel(move |pointer, reason, _ctx| {
3423 c.borrow_mut().push((pointer.id, reason));
3424 }),
3425 );
3426 slot.set(Some(source));
3427 tree.layout(SizeProposal::exact(200.0, 100.0));
3428
3429 let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
3430 assert!(tree.active_drag.is_some());
3431
3432 tree.handle_drag_move(Point::new(-40.0, 50.0), &mut ops);
3434 assert_eq!(tree.outbound_drag_source, Some(source));
3435 assert_eq!(
3436 *kinds.borrow(),
3437 vec![teksilo_tokens::PointerKind::Touch],
3438 "the platform is told which device is dragging — Wayland needs the \
3439 touch-down serial, not a button serial"
3440 );
3441 assert_eq!(
3442 *cancelled.borrow(),
3443 vec![(contact, crate::pointer::CancelReason::OsDragStarted)],
3444 "the finger is the pointer revoked"
3445 );
3446 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3447 }
3448
3449 #[test]
3456 fn an_escape_cancelled_finger_drag_reports_the_finger_to_its_source() {
3457 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3458 let s = seen.clone();
3459
3460 let mut ops = crate::window::NoopWindowOps;
3461 let mut tree = WidgetTree::new();
3462 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3463 let armed = Cell::new(false);
3464 let s2 = slot.clone();
3465 let source = tree.add(
3466 FillWidget::new()
3467 .on_pointer_event(move |event, ctx| {
3468 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3469 && !armed.replace(true)
3470 && let Some(id) = s2.get()
3471 {
3472 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(1_u8));
3473 }
3474 crate::event::EventResponse::Ignored
3475 })
3476 .on_drag_ended(move |_outcome, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3477 );
3478 slot.set(Some(source));
3479 tree.layout(SizeProposal::exact(200.0, 100.0));
3480
3481 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3482 assert!(tree.active_drag.is_some());
3483 tree.dispatch_event_with_ops(
3484 crate::event::WidgetEvent::KeyDown {
3485 key: crate::event::Key::Escape,
3486 modifiers: crate::event::Modifiers::NONE,
3487 text: None,
3488 },
3489 &mut ops,
3490 );
3491 assert_eq!(*seen.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
3492 }
3493
3494 #[test]
3500 fn the_accept_setter_receives_the_widget_verdict() {
3501 use crate::drag_state::DropFeedback;
3502
3503 let verdict = Rc::new(Cell::new(false));
3504 let v = verdict.clone();
3505 let mut ops = RecordingWindowOps::new(true);
3506 let accepts = ops.accepts.clone();
3507
3508 let mut tree = WidgetTree::new();
3509 tree.add(
3510 FillWidget::new()
3511 .on_drag_hover(move |_p, _pos, _ctx| {
3512 if v.get() {
3513 DropFeedback::Accept
3514 } else {
3515 DropFeedback::NoFeedback
3516 }
3517 })
3518 .on_drop(|_, _, _| true),
3519 );
3520 tree.layout(SizeProposal::exact(200.0, 100.0));
3521
3522 tree.begin_external_drag(
3523 Point::new(50.0, 50.0),
3524 crate::drag_payload::ExternalDropData {
3525 files: vec![std::path::PathBuf::from("/tmp/a.png")],
3526 ..Default::default()
3527 },
3528 &mut ops,
3529 );
3530 assert_eq!(
3531 *accepts.borrow(),
3532 vec![false],
3533 "a refusing target is reported to the OS as a refusal"
3534 );
3535
3536 tree.update_external_drag(Point::new(52.0, 50.0), &mut ops);
3539 assert_eq!(
3540 *accepts.borrow(),
3541 vec![false],
3542 "an unchanged answer is not re-sent"
3543 );
3544
3545 verdict.set(true);
3547 tree.update_external_drag(Point::new(54.0, 50.0), &mut ops);
3548 assert_eq!(
3549 *accepts.borrow(),
3550 vec![false, true],
3551 "and a change is pushed once"
3552 );
3553 }
3554
3555 #[test]
3558 fn an_in_app_drag_pushes_no_os_accept_state() {
3559 let mut ops = RecordingWindowOps::new(true);
3560 let accepts = ops.accepts.clone();
3561
3562 let mut tree = WidgetTree::new();
3563 let source = tree.add(FillWidget::new());
3564 tree.add(FillWidget::new().on_drop(|_, _, _| true));
3565 tree.layout(SizeProposal::exact(200.0, 100.0));
3566
3567 let mut ctx = crate::widget::EventContext::new();
3568 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
3569 tree.collect_from_ctx(ctx, source);
3570 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3571
3572 assert!(accepts.borrow().is_empty());
3573 }
3574
3575 #[test]
3584 fn a_cancelled_os_drag_tears_the_source_down_exactly_once() {
3585 use crate::drag_payload::{DragPayload, DropOutcome};
3586
3587 let ended = Rc::new(RefCell::new(Vec::new()));
3588 let e = ended.clone();
3589 let mut ops = RecordingWindowOps::new(true);
3590
3591 let mut tree_a = WidgetTree::new();
3593 let src = tree_a.add(
3594 FillWidget::new().on_drag_ended(move |outcome, _ctx| e.borrow_mut().push(outcome)),
3595 );
3596 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3597 let mut ctx = crate::widget::EventContext::new();
3598 ctx.start_drag(
3599 src,
3600 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3601 );
3602 tree_a.collect_from_ctx(ctx, src);
3603 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3604 assert!(super::outbound_is_live());
3605
3606 let left = Rc::new(Cell::new(0_u32));
3608 let l = left.clone();
3609 let mut tree_b = WidgetTree::new();
3610 tree_b.add(
3611 FillWidget::new()
3612 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3613 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3614 .on_drop(|_, _, _| true),
3615 );
3616 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3617 let accepts = ops.accepts.clone();
3618 accepts.borrow_mut().clear();
3619 tree_b.begin_external_drag(
3620 Point::new(50.0, 50.0),
3621 crate::drag_payload::ExternalDropData::default(),
3622 &mut ops,
3623 );
3624 assert!(tree_b.os_drag_reentered);
3625 assert_eq!(
3626 *accepts.borrow(),
3627 vec![true],
3628 "a re-entered app drag still negotiates with the OS: its offer is \
3629 live and a refusal must still reach the compositor's cursor",
3630 );
3631
3632 tree_b.abort_external_drag(&mut ops);
3635 assert!(tree_b.active_drag.is_none(), "B's session is gone");
3636 assert!(!tree_b.os_drag_reentered);
3637 assert_eq!(left.get(), 1, "B's highlighted target was cleared");
3638 assert!(
3639 ended.borrow().is_empty(),
3640 "the abort must not fire the source's on_drag_ended — the source \
3641 window owns that"
3642 );
3643
3644 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3646 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
3647
3648 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3650 assert_eq!(
3651 *ended.borrow(),
3652 vec![DropOutcome::Cancelled],
3653 "exactly once"
3654 );
3655 }
3656
3657 #[test]
3667 fn a_reentered_session_is_reaped_when_the_os_drag_ends_elsewhere() {
3668 use crate::drag_payload::{DragPayload, DropOutcome};
3669
3670 let mut ops = RecordingWindowOps::new(true);
3671
3672 let mut tree_a = WidgetTree::new();
3673 let src = tree_a.add(FillWidget::new());
3674 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3675 let mut ctx = crate::widget::EventContext::new();
3676 ctx.start_drag(
3677 src,
3678 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3679 );
3680 tree_a.collect_from_ctx(ctx, src);
3681 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3682
3683 let left = Rc::new(Cell::new(0_u32));
3684 let l = left.clone();
3685 let mut tree_b = WidgetTree::new();
3686 tree_b.add(
3687 FillWidget::new()
3688 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3689 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3690 .on_drop(|_, _, _| true),
3691 );
3692 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3693 tree_b.begin_external_drag(
3694 Point::new(50.0, 50.0),
3695 crate::drag_payload::ExternalDropData::default(),
3696 &mut ops,
3697 );
3698 assert!(tree_b.active_drag.is_some() && tree_b.os_drag_reentered);
3699
3700 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3702 assert!(
3703 tree_b.active_drag.is_some(),
3704 "B has not been told anything yet"
3705 );
3706
3707 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3709 assert!(tree_b.active_drag.is_none(), "the dead session was reaped");
3710 assert!(!tree_b.os_drag_reentered);
3711 assert_eq!(left.get(), 1, "and its highlighted target was cleared");
3712 }
3713
3714 #[test]
3718 fn a_reentered_app_drag_recovers_the_device_that_started_it() {
3719 let mut ops = RecordingWindowOps::new(true);
3720
3721 let mut tree_a = WidgetTree::new();
3722 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3723 let src = tree_a.add(drag_arming_leaf(slot.clone(), exportable_payload));
3724 slot.set(Some(src));
3725 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3726 touch_press(&mut tree_a, Point::new(20.0, 50.0), &mut ops);
3727 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3728 assert!(super::outbound_is_live());
3729
3730 let mut tree_b = WidgetTree::new();
3731 tree_b.add(FillWidget::new().on_drop(|_, _, _| true));
3732 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3733 tree_b.begin_external_drag(
3734 Point::new(50.0, 50.0),
3735 crate::drag_payload::ExternalDropData::default(),
3736 &mut ops,
3737 );
3738 assert_eq!(
3739 tree_b.active_drag.as_ref().map(|d| d.pointer.kind),
3740 Some(teksilo_tokens::PointerKind::Touch),
3741 );
3742
3743 let mut tree_c = WidgetTree::new();
3745 tree_c.add(FillWidget::new().on_drop(|_, _, _| true));
3746 tree_c.layout(SizeProposal::exact(200.0, 100.0));
3747 tree_a.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3748 tree_c.begin_external_drag(
3749 Point::new(50.0, 50.0),
3750 crate::drag_payload::ExternalDropData::default(),
3751 &mut ops,
3752 );
3753 assert_eq!(
3754 tree_c.active_drag.as_ref().map(|d| d.pointer.kind),
3755 Some(teksilo_tokens::PointerKind::Unknown),
3756 "no OS names the source's device to a destination",
3757 );
3758 }
3759
3760 #[test]
3764 fn restash_after_drag_ended_is_noop() {
3765 use crate::drag_payload::DragPayload;
3766
3767 super::outbound_begin(
3768 DragPayload::typed(1_u32).with_mime("text/plain", b"x".to_vec()),
3769 crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO),
3770 );
3771 assert!(super::has_outbound_typed());
3772 let held = super::outbound_take_if_live().expect("payload taken while live");
3774 assert!(!super::has_outbound_typed());
3775 super::outbound_end();
3777 super::outbound_restash(held);
3779 assert!(
3780 !super::has_outbound_typed(),
3781 "ended drag is not resurrected by a racing re-stash"
3782 );
3783 assert!(super::outbound_take_if_live().is_none());
3785 }
3786}