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) {
178 if let Some(ref drag) = self.active_drag {
179 if let Some(overlay_id) = drag.preview_overlay_id {
180 self.overlay_manager.dismiss(overlay_id);
181 }
182 if let Some(content_id) = drag.preview_content_id {
183 self.arena.destroy(content_id);
184 }
185 }
186 }
187
188 pub(super) fn cancel_active_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
193 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
194 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
198 let saved = self.active_drag.as_ref().map(|d| d.pointer).map(|p| {
206 std::mem::replace(
207 &mut self.current_input,
208 crate::pointer::InputSnapshot::for_drag_session(p),
209 )
210 });
211 self.cleanup_drag_preview();
212 self.active_drag = None;
213 self.os_drop_accepted = None;
214 self.release_drag_capture(source);
215 self.current_cursor = crate::widget::CursorIcon::Default;
216 if let Some(prev) = prev_target {
217 self.fire_on_drag_leave(prev, &mut *ops);
218 }
219 if let Some(src) = source {
220 self.fire_on_drag_ended(src, crate::drag_payload::DropOutcome::Cancelled, &mut *ops);
221 }
222 if let Some(saved) = saved {
223 self.current_input = saved;
224 }
225 }
226
227 pub fn begin_external_drag(
259 &mut self,
260 position: teksilo_canvas::Point,
261 data: crate::drag_payload::ExternalDropData,
262 ops: &mut dyn crate::window::WindowOps,
263 ) {
264 if self.active_drag.is_some() {
268 self.cancel_active_drag(&mut *ops);
269 }
270
271 if let Some(mut payload) = outbound_take_if_live() {
281 payload.enrich_external_from_mime();
285 let pointer = outbound_pointer_if_live().unwrap_or_else(unknown_external_pointer);
288 self.active_drag = Some(crate::drag_state::DragSession {
289 payload,
290 pointer,
291 source_widget: None,
292 is_external: false,
293 current_position: position,
294 current_target: None,
295 feedback: crate::drag_state::DropFeedback::NoFeedback,
296 preview_content_id: None,
297 preview_overlay_id: None,
298 });
299 self.os_drag_reentered = true;
300 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
301 return;
302 }
303
304 self.active_drag = Some(crate::drag_state::DragSession {
305 payload: crate::drag_payload::DragPayload::external(data),
306 pointer: unknown_external_pointer(),
307 source_widget: None,
308 is_external: true,
309 current_position: position,
310 current_target: None,
311 feedback: crate::drag_state::DropFeedback::NoFeedback,
312 preview_content_id: None,
313 preview_overlay_id: None,
314 });
315 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
318 }
319
320 pub fn update_external_drag(
324 &mut self,
325 position: teksilo_canvas::Point,
326 ops: &mut dyn crate::window::WindowOps,
327 ) {
328 if self.active_drag.as_ref().is_some_and(|d| d.is_external) || self.os_drag_reentered {
332 self.drive_drag_session(position, DragDrive::Move, &mut *ops);
333 }
334 }
335
336 pub fn end_external_drag(
343 &mut self,
344 position: teksilo_canvas::Point,
345 data: crate::drag_payload::ExternalDropData,
346 ops: &mut dyn crate::window::WindowOps,
347 ) {
348 if self.os_drag_reentered {
355 self.os_drag_reentered = false;
356 self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
357 outbound_end();
358 return;
359 }
360 if !self.active_drag.as_ref().is_some_and(|d| d.is_external) {
361 return;
362 }
363 if !data.is_empty()
364 && let Some(drag) = self.active_drag.as_mut()
365 {
366 drag.payload = crate::drag_payload::DragPayload::external(data);
367 }
368 self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
369 }
370
371 pub fn abort_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
388 if self.active_drag.is_none() {
389 self.os_drag_reentered = false;
390 return;
391 }
392 self.os_drag_reentered = false;
393 self.cancel_active_drag(&mut *ops);
394 }
395
396 pub fn cancel_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
401 if self.os_drag_reentered {
406 self.reexit_outbound(&mut *ops);
407 } else if self.active_drag.as_ref().is_some_and(|d| d.is_external) {
408 self.cancel_active_drag(&mut *ops);
409 }
410 }
411
412 pub(super) fn process_drag_tick(&mut self, ops: &mut dyn crate::window::WindowOps) {
417 self.reap_dead_reentered_drag(&mut *ops);
419 let Some((target_id, position, pointer)) = self
420 .active_drag
421 .as_ref()
422 .and_then(|d| d.current_target.map(|t| (t, d.current_position, d.pointer)))
423 else {
424 return;
425 };
426 if !self.arena.is_active(target_id) {
427 return;
428 }
429 let bounds = self.arena.bounds(target_id);
430 let local = teksilo_canvas::Point::new(position.x - bounds.x, position.y - bounds.y);
431 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
432 Some(node) => (
433 node.external_handlers.on_drag_tick.take(),
434 node.handlers.on_drag_tick.take(),
435 ),
436 None => return,
437 };
438 if ext_handler.is_none() && own_handler.is_none() {
439 return;
440 }
441 let saved = std::mem::replace(
446 &mut self.current_input,
447 crate::pointer::InputSnapshot::for_drag_session(pointer),
448 );
449 let mut ctx = self.make_event_context(&mut *ops);
450 if let Some(h) = ext_handler.as_mut() {
451 h(local, &mut ctx);
452 }
453 if let Some(h) = own_handler.as_mut() {
454 h(local, &mut ctx);
455 }
456 if let Some(node) = self.arena.get_mut(target_id) {
457 node.external_handlers.on_drag_tick = ext_handler;
458 node.handlers.on_drag_tick = own_handler;
459 }
460 self.collect_from_ctx(ctx, target_id);
461 if self.active_drag.is_some() {
465 self.handle_drag_move(position, &mut *ops);
466 }
467 self.current_input = saved;
468 }
469
470 fn reap_dead_reentered_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
486 if !self.os_drag_reentered || outbound_is_live() {
487 return;
488 }
489 crate::trace_input!(
490 Gestures,
491 "the OS drag this window held as re-entered has ended elsewhere: dropping the session"
492 );
493 self.abort_external_drag(&mut *ops);
494 }
495
496 pub(super) fn fire_on_drag_leave(
502 &mut self,
503 target_id: WidgetId,
504 ops: &mut dyn crate::window::WindowOps,
505 ) {
506 if !self.arena.is_active(target_id) {
507 return;
508 }
509 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
510 Some(node) => (
511 node.external_handlers.on_drag_leave.take(),
512 node.handlers.on_drag_leave.take(),
513 ),
514 None => return,
515 };
516 if ext_handler.is_none() && own_handler.is_none() {
517 self.arena.mark_needs_paint(target_id);
520 return;
521 }
522 let mut ctx = self.make_event_context(&mut *ops);
523 if let Some(h) = ext_handler.as_mut() {
524 h(&mut ctx);
525 }
526 if let Some(h) = own_handler.as_mut() {
527 h(&mut ctx);
528 }
529 if let Some(node) = self.arena.get_mut(target_id) {
530 node.external_handlers.on_drag_leave = ext_handler;
531 node.handlers.on_drag_leave = own_handler;
532 }
533 self.collect_from_ctx(ctx, target_id);
534 self.arena.mark_needs_paint(target_id);
535 }
536
537 pub(super) fn fire_on_drag_ended(
541 &mut self,
542 source_id: WidgetId,
543 outcome: crate::drag_payload::DropOutcome,
544 ops: &mut dyn crate::window::WindowOps,
545 ) {
546 if !self.arena.is_active(source_id) {
547 return;
548 }
549 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(source_id) {
554 Some(node) => (
555 node.external_handlers.on_drag_ended.take(),
556 node.handlers.on_drag_ended.take(),
557 ),
558 None => return,
559 };
560 if ext_handler.is_none() && own_handler.is_none() {
561 return;
562 }
563 let mut ctx = self.make_event_context(&mut *ops);
564 if let Some(h) = ext_handler.as_mut() {
565 h(outcome, &mut ctx);
566 }
567 if let Some(h) = own_handler.as_mut() {
568 h(outcome, &mut ctx);
569 }
570 if let Some(node) = self.arena.get_mut(source_id) {
571 node.external_handlers.on_drag_ended = ext_handler;
572 node.handlers.on_drag_ended = own_handler;
573 }
574 self.collect_from_ctx(ctx, source_id);
575 }
576
577 fn window_content_size(&self) -> Option<(f32, f32)> {
580 Some((self.last_proposal.width?, self.last_proposal.height?))
581 }
582
583 fn is_outside_window(&self, position: teksilo_canvas::Point) -> bool {
586 match self.window_content_size() {
587 Some((w, h)) => {
588 position.x < 0.0 || position.y < 0.0 || position.x > w || position.y > h
589 }
590 None => false,
591 }
592 }
593
594 fn try_escalate_to_os_drag(
600 &mut self,
601 position: teksilo_canvas::Point,
602 ops: &mut dyn crate::window::WindowOps,
603 ) -> bool {
604 if self.os_drag_reentered {
609 if self.is_outside_window(position) {
610 self.reexit_outbound(&mut *ops);
611 return true;
612 }
613 return false;
614 }
615
616 let data = match self.active_drag.as_ref() {
618 Some(d)
619 if !d.is_external && d.source_widget.is_some() && d.payload.is_os_exportable() =>
620 {
621 d.payload.to_outbound()
622 }
623 _ => return false,
624 };
625 if !self.is_outside_window(position) {
626 return false;
627 }
628
629 let dragging = match self.active_drag.as_ref() {
633 Some(d) => d.pointer,
634 None => return false,
635 };
636 if !ops.begin_os_drag(data, None, dragging.kind) {
637 return false;
638 }
639
640 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
645 self.cleanup_drag_preview();
646 let drag = self
647 .active_drag
648 .take()
649 .expect("active_drag present (matched above)");
650 self.os_drop_accepted = None;
651 self.release_drag_capture(drag.source_widget);
652 self.current_cursor = crate::widget::CursorIcon::Default;
653 self.outbound_drag_source = drag.source_widget;
654 outbound_begin(drag.payload, dragging);
655 if let Some(prev) = prev_target {
656 self.fire_on_drag_leave(prev, &mut *ops);
657 }
658 self.cancel_pointer_to(
671 dragging.id,
672 crate::pointer::CancelReason::OsDragStarted,
673 self.outbound_drag_source,
674 &mut *ops,
675 );
676 true
677 }
678
679 fn reexit_outbound(&mut self, ops: &mut dyn crate::window::WindowOps) {
683 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
684 self.cleanup_drag_preview();
685 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
686 if let Some(drag) = self.active_drag.take() {
687 outbound_restash(drag.payload);
688 }
689 self.os_drag_reentered = false;
690 self.os_drop_accepted = None;
691 self.release_drag_capture(source);
692 self.current_cursor = crate::widget::CursorIcon::Default;
693 if let Some(prev) = prev_target {
694 self.fire_on_drag_leave(prev, &mut *ops);
695 }
696 }
697
698 pub fn handle_os_drag_ended(
703 &mut self,
704 outcome: crate::drag_payload::DropOutcome,
705 ops: &mut dyn crate::window::WindowOps,
706 ) {
707 outbound_end();
710 self.os_drag_reentered = false;
711 if let Some(source) = self.outbound_drag_source.take() {
712 self.fire_on_drag_ended(source, outcome, &mut *ops);
713 }
714 }
715
716 pub fn abort_outbound_drag(&mut self) {
724 if self.outbound_drag_source.take().is_some() {
725 outbound_end();
726 } else if self.os_drag_reentered
727 && let Some(drag) = self.active_drag.take()
728 {
729 outbound_restash(drag.payload);
730 }
731 self.os_drag_reentered = false;
732 self.os_drop_accepted = None;
733 }
734
735 pub(super) fn handle_drag_move(
738 &mut self,
739 position: teksilo_canvas::Point,
740 ops: &mut dyn crate::window::WindowOps,
741 ) {
742 if let Some(ref mut drag) = self.active_drag {
744 drag.current_position = position;
745 }
746
747 if self.try_escalate_to_os_drag(position, &mut *ops) {
750 return;
751 }
752
753 let preview_content = self
760 .active_drag
761 .as_ref()
762 .and_then(|d| Some((d.preview_overlay_id?, d.preview_content_id?)));
763 if let Some((overlay_id, content_id)) = preview_content {
764 let dragging = self.active_drag.as_ref().map(|d| d.pointer);
770 let placement = match dragging {
771 Some(pointer) => {
772 crate::overlay::OverlayPlacement::at_pointer_for(position, &pointer)
773 }
774 None => crate::overlay::OverlayPlacement::AtPointer(position),
775 };
776 self.overlay_manager.update_placement(overlay_id, placement);
777 self.arena.mark_needs_layout(content_id);
778 }
779
780 let exclude_overlay = self.active_drag.as_ref().and_then(|d| d.preview_overlay_id);
784 let exclude_widget = self.active_drag.as_ref().and_then(|d| d.preview_content_id);
785 let dragging = self.current_input.pointer;
789 let target =
790 self.hit_test_for_excluding(position, &dragging, exclude_overlay, exclude_widget);
791
792 let mut candidate = target.and_then(|t| self.find_drop_target_at_or_above(t));
800 let mut engaged: Option<WidgetId> = None;
801 let mut engaged_feedback = crate::drag_state::DropFeedback::NoFeedback;
802 let mut bubbled_past: Vec<WidgetId> = Vec::new();
803 while let Some(cand) = candidate {
804 let fb = self.fire_on_drag_hover(cand, position, &mut *ops);
805 if fb.is_engaged() {
806 engaged = Some(cand);
807 engaged_feedback = fb;
808 break;
809 }
810 bubbled_past.push(cand);
811 candidate = self.next_drop_target_above(cand);
812 }
813
814 let (new_target, new_feedback) = if engaged.is_some() {
822 for cand in &bubbled_past {
823 self.fire_on_drag_leave(*cand, &mut *ops);
824 }
825 (engaged, engaged_feedback)
826 } else if let Some((&deepest, rest)) = bubbled_past.split_first() {
827 for cand in rest {
828 self.fire_on_drag_leave(*cand, &mut *ops);
829 }
830 (Some(deepest), crate::drag_state::DropFeedback::NoFeedback)
831 } else {
832 (None, crate::drag_state::DropFeedback::NoFeedback)
833 };
834
835 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
841 if prev_target != new_target
842 && let Some(prev) = prev_target
843 && !bubbled_past.contains(&prev)
844 {
845 self.fire_on_drag_leave(prev, &mut *ops);
846 }
847 let engaged_now = new_feedback.is_engaged();
848 let is_os_drag =
854 self.os_drag_reentered || self.active_drag.as_ref().is_some_and(|d| d.is_external);
855 if let Some(ref mut drag) = self.active_drag {
856 drag.current_target = new_target;
857 drag.feedback = new_feedback;
858 }
859 if is_os_drag && self.os_drop_accepted != Some(engaged_now) {
870 self.os_drop_accepted = Some(engaged_now);
871 ops.set_drop_accepted(engaged_now);
872 }
873 }
874
875 fn fire_on_drag_hover(
880 &mut self,
881 target_id: WidgetId,
882 position: teksilo_canvas::Point,
883 ops: &mut dyn crate::window::WindowOps,
884 ) -> crate::drag_state::DropFeedback {
885 use crate::drag_state::DropFeedback;
886 let target_bounds = self.arena.bounds(target_id);
887 let local =
888 teksilo_canvas::Point::new(position.x - target_bounds.x, position.y - target_bounds.y);
889 let (mut ext_handler, mut own_handler, has_on_drop) = match self.arena.get_mut(target_id) {
890 Some(node) => {
891 let has_on_drop = node.any_handler(|h| h.on_drop.is_some());
892 let ext = node.external_handlers.on_drag_hover.take();
893 let own = node.handlers.on_drag_hover.take();
894 (ext, own, has_on_drop)
895 }
896 None => return DropFeedback::NoFeedback,
897 };
898 if ext_handler.is_none() && own_handler.is_none() {
900 return if has_on_drop {
901 DropFeedback::Accept
902 } else {
903 DropFeedback::NoFeedback
904 };
905 }
906 let mut feedback = DropFeedback::NoFeedback;
907 if self.active_drag.is_some() {
908 let mut ctx = self.make_event_context(&mut *ops);
909 if let Some(ref drag) = self.active_drag {
910 if let Some(h) = ext_handler.as_mut() {
911 feedback = h(&drag.payload, local, &mut ctx);
912 }
913 if let Some(h) = own_handler.as_mut() {
914 feedback = h(&drag.payload, local, &mut ctx);
915 }
916 }
917 if let Some(node) = self.arena.get_mut(target_id) {
918 node.external_handlers.on_drag_hover = ext_handler;
919 node.handlers.on_drag_hover = own_handler;
920 }
921 self.collect_from_ctx(ctx, target_id);
922 self.arena.mark_needs_paint(target_id);
923 } else if let Some(node) = self.arena.get_mut(target_id) {
924 node.external_handlers.on_drag_hover = ext_handler;
925 node.handlers.on_drag_hover = own_handler;
926 }
927 feedback
928 }
929
930 fn next_drop_target_above(&self, id: WidgetId) -> Option<WidgetId> {
933 let parent = self.arena.parent(id)?;
934 self.find_drop_target_at_or_above(parent)
935 }
936
937 pub(super) fn handle_drag_drop(
939 &mut self,
940 position: teksilo_canvas::Point,
941 ops: &mut dyn crate::window::WindowOps,
942 ) {
943 self.cleanup_drag_preview();
945
946 if self.active_drag.is_none() {
947 return;
948 }
949
950 let mut drop_target = self
961 .active_drag
962 .as_ref()
963 .and_then(|d| d.current_target)
964 .filter(|&t| self.arena.is_active(t));
965 if drop_target.is_none() {
966 let dropping = self.current_input.pointer;
967 let hit = self.hit_test_for(position, &dropping);
968 let mut candidate = hit.and_then(|t| self.find_drop_target_at_or_above(t));
969 while let Some(cand) = candidate {
970 if self
971 .fire_on_drag_hover(cand, position, &mut *ops)
972 .is_engaged()
973 {
974 drop_target = Some(cand);
975 break;
976 }
977 self.fire_on_drag_leave(cand, &mut *ops);
980 candidate = self.next_drop_target_above(cand);
981 }
982 }
983
984 let drag = match self.active_drag.take() {
986 Some(d) => d,
987 None => return,
988 };
989 self.os_drop_accepted = None;
990 self.release_drag_capture(drag.source_widget);
991 self.current_cursor = crate::widget::CursorIcon::Default;
992 let source = drag.source_widget;
995 let mut outcome = crate::drag_payload::DropOutcome::Cancelled;
998
999 if let Some(prev) = drop_target {
1003 self.fire_on_drag_leave(prev, &mut *ops);
1004 }
1005
1006 if let Some(target_id) = drop_target {
1011 let target_bounds = self.arena.bounds(target_id);
1012 let local = teksilo_canvas::Point::new(
1013 position.x - target_bounds.x,
1014 position.y - target_bounds.y,
1015 );
1016 let (taken_own, taken_ext) = match self.arena.get_mut(target_id) {
1017 Some(node) => {
1018 let own = node.handlers.on_drop.take();
1019 let ext = if own.is_none() {
1020 node.external_handlers.on_drop.take()
1021 } else {
1022 None
1023 };
1024 (own, ext)
1025 }
1026 None => (None, None),
1027 };
1028 let picked = if let Some(h) = taken_own {
1029 Some((h, true))
1030 } else {
1031 taken_ext.map(|h| (h, false))
1032 };
1033 if let Some((mut handler, is_own)) = picked {
1034 let mut ctx = self.make_event_context(&mut *ops);
1035 let accepted = handler(drag.payload, local, &mut ctx);
1036 outcome = crate::drag_payload::DropOutcome::InApp { accepted };
1037 if let Some(node) = self.arena.get_mut(target_id) {
1038 if is_own {
1039 node.handlers.on_drop = Some(handler);
1040 } else {
1041 node.external_handlers.on_drop = Some(handler);
1042 }
1043 }
1044 self.collect_from_ctx(ctx, target_id);
1045 self.arena.mark_needs_paint(target_id);
1046 }
1047 }
1048 if let Some(src) = source {
1052 self.fire_on_drag_ended(src, outcome, &mut *ops);
1053 }
1054 }
1055
1056 fn find_drop_target_at_or_above(&self, start: WidgetId) -> Option<WidgetId> {
1059 let mut current = Some(start);
1060 while let Some(id) = current {
1061 if let Some(node) = self.arena.get(id)
1062 && node.any_handler(|h| h.on_drop.is_some() || h.on_drag_hover.is_some())
1063 {
1064 return Some(id);
1065 }
1066 current = self.arena.parent(id);
1067 }
1068 None
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075 use crate::test_widgets::{FillWidget, StackWidget};
1076 use crate::widget::CursorIcon;
1077 use crate::widget_builder::WidgetBuilder;
1078
1079 #[test]
1080 fn start_drag_creates_session() {
1081 let mut tree = WidgetTree::new();
1082 let source = tree.add(FillWidget::new().on_tap({
1083 move |_pos, ctx: &mut crate::widget::EventContext| {
1084 ctx.start_drag(
1085 ctx.focus_requests.first().copied().unwrap_or_default(),
1086 crate::drag_payload::DragPayload::typed(42_u32),
1087 );
1088 }
1089 }));
1090 tree.layout(SizeProposal::exact(100.0, 50.0));
1091
1092 let mut ctx = crate::widget::EventContext::new();
1094 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1095 tree.collect_from_ctx(ctx, source);
1096
1097 assert!(tree.active_drag.is_some());
1098 let drag = tree.active_drag.as_ref().unwrap();
1099 assert_eq!(drag.source_widget, Some(source));
1100 assert!(!drag.is_external);
1101 assert!(drag.payload.has_typed::<u32>());
1102 }
1103
1104 #[test]
1105 fn drag_move_updates_position() {
1106 let mut tree = WidgetTree::new();
1107 let source = tree.add(FillWidget::new());
1108 tree.layout(SizeProposal::exact(200.0, 100.0));
1109
1110 let mut ctx = crate::widget::EventContext::new();
1112 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("hello"));
1113 tree.collect_from_ctx(ctx, source);
1114 assert!(tree.active_drag.is_some());
1115
1116 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 30.0)));
1118
1119 let drag = tree.active_drag.as_ref().unwrap();
1120 assert!((drag.current_position.x - 50.0).abs() < 0.01);
1121 assert!((drag.current_position.y - 30.0).abs() < 0.01);
1122 }
1123
1124 #[test]
1125 fn escape_cancels_drag() {
1126 let mut tree = WidgetTree::new();
1127 let source = tree.add(FillWidget::new());
1128 tree.layout(SizeProposal::exact(200.0, 100.0));
1129
1130 let mut ctx = crate::widget::EventContext::new();
1131 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(99_i32));
1132 tree.collect_from_ctx(ctx, source);
1133 assert!(tree.active_drag.is_some());
1134
1135 tree.press_key(Key::Escape, Modifiers::NONE);
1136 assert!(tree.active_drag.is_none(), "drag should be cancelled");
1137 }
1138
1139 #[test]
1140 fn drop_on_target_fires_handler() {
1141 use std::cell::Cell;
1142 use std::rc::Rc;
1143
1144 let dropped = Rc::new(Cell::new(false));
1145 let dropped_value = Rc::new(Cell::new(0_u32));
1146 let d = dropped.clone();
1147 let dv = dropped_value.clone();
1148
1149 let mut tree = WidgetTree::new();
1150 let source = tree.add(FillWidget::new());
1151 let _target = tree.add(FillWidget::new().on_drop(move |mut payload, _pos, _ctx| {
1153 d.set(true);
1154 if let Some(val) = payload.take_typed::<u32>() {
1155 dv.set(val);
1156 }
1157 true
1158 }));
1159 tree.layout(SizeProposal::exact(200.0, 100.0));
1160
1161 let mut ctx = crate::widget::EventContext::new();
1163 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1164 tree.collect_from_ctx(ctx, source);
1165
1166 tree.dispatch_event(WidgetEvent::pointer_up(
1168 Point::new(150.0, 50.0),
1169 PointerButton::Primary,
1170 Modifiers::NONE,
1171 ));
1172
1173 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1174 assert!(dropped.get(), "on_drop should have been called");
1175 assert_eq!(dropped_value.get(), 42);
1176 }
1177
1178 #[test]
1179 fn drop_on_no_target_cancels() {
1180 let mut tree = WidgetTree::new();
1181 let source = tree.add(FillWidget::new());
1182 tree.layout(SizeProposal::exact(100.0, 50.0));
1183
1184 let mut ctx = crate::widget::EventContext::new();
1185 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1186 tree.collect_from_ctx(ctx, source);
1187
1188 tree.dispatch_event(WidgetEvent::pointer_up(
1190 Point::new(999.0, 999.0),
1191 PointerButton::Primary,
1192 Modifiers::NONE,
1193 ));
1194
1195 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1196 }
1197
1198 #[test]
1199 fn drop_falls_through_a_destroyed_current_target() {
1200 use std::cell::Cell;
1205 use std::rc::Rc;
1206
1207 let bg_dropped = Rc::new(Cell::new(false));
1208 let bg_sink = bg_dropped.clone();
1209
1210 let mut tree = WidgetTree::new();
1211 let source = tree.add(FillWidget::new());
1215 let _bg = tree.add(FillWidget::new().on_drop(move |_p, _pos, _ctx| {
1216 bg_sink.set(true);
1217 true
1218 }));
1219 let fg = tree.add(
1222 FillWidget::new()
1223 .on_drag_hover(|_payload, _pos, _ctx| {
1224 crate::drag_state::DropFeedback::InsertionLine {
1225 y: 50.0,
1226 width: 200.0,
1227 }
1228 })
1229 .on_drop(|_, _, _| true),
1230 );
1231 tree.layout(SizeProposal::exact(200.0, 100.0));
1232
1233 let mut ctx = crate::widget::EventContext::new();
1234 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1235 tree.collect_from_ctx(ctx, source);
1236
1237 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1239 assert_eq!(
1240 tree.active_drag.as_ref().unwrap().current_target,
1241 Some(fg),
1242 "fg engaged as the current drop target"
1243 );
1244
1245 tree.arena.destroy(fg);
1247 assert!(!tree.arena.is_active(fg));
1248
1249 tree.dispatch_event(WidgetEvent::pointer_up(
1252 Point::new(100.0, 50.0),
1253 PointerButton::Primary,
1254 Modifiers::NONE,
1255 ));
1256
1257 assert!(tree.active_drag.is_none(), "drag session cleared");
1258 assert!(
1259 bg_dropped.get(),
1260 "drop landed on the live background target, not the destroyed one"
1261 );
1262 }
1263
1264 fn mouse_members(
1270 tree: &WidgetTree,
1271 ) -> Vec<(
1272 WidgetId,
1273 crate::gesture::MemberRole,
1274 crate::gesture::MemberState,
1275 )> {
1276 tree.sequence_members(crate::pointer::PointerId::MOUSE)
1277 }
1278
1279 #[test]
1280 fn drag_arming_walks_to_an_ancestor_without_a_dead_zone() {
1281 use crate::gesture::{MemberRole, MemberState};
1285 let mut tree = WidgetTree::new();
1286 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1287 let inner = tree.add(StackWidget::new().child(button));
1288 let ancestor = tree.add(StackWidget::new().child(inner).on_drag(|_phase, _ctx| {}));
1289 tree.layout(SizeProposal::exact(100.0, 100.0));
1290
1291 let b = tree.bounds(button);
1292 tree.pointer_down_button(
1293 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1294 PointerButton::Primary,
1295 );
1296 assert_eq!(
1297 mouse_members(&tree),
1298 vec![(ancestor, MemberRole::Gesture, MemberState::Possible)],
1299 "the draggable ancestor competes when the button press is not in a dead zone"
1300 );
1301 tree.pointer_up_button(
1302 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1303 PointerButton::Primary,
1304 );
1305 }
1306
1307 #[test]
1308 fn gesture_dead_zone_blocks_ancestor_drag_arming() {
1309 use crate::widget_builder::WidgetBuilder;
1315 let mut tree = WidgetTree::new();
1316 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1317 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1318 let _ancestor = tree.add(
1319 StackWidget::new()
1320 .child(dead_zone)
1321 .on_drag(|_phase, _ctx| {}),
1322 );
1323 tree.layout(SizeProposal::exact(100.0, 100.0));
1324
1325 let b = tree.bounds(button);
1326 tree.pointer_down_button(
1327 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1328 PointerButton::Primary,
1329 );
1330 assert!(
1331 mouse_members(&tree).is_empty(),
1332 "a dead zone blocks the draggable ancestor from competing"
1333 );
1334 tree.pointer_up_button(
1335 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1336 PointerButton::Primary,
1337 );
1338 }
1339
1340 #[test]
1341 fn a_dead_zone_boundary_blocks_a_mouse_exactly_as_it_blocks_a_finger() {
1342 use crate::pointer::{
1347 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
1348 };
1349 use crate::widget_builder::WidgetBuilder;
1350
1351 let mut tree = WidgetTree::new();
1352 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1353 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1354 tree.add(
1355 StackWidget::new()
1356 .child(dead_zone)
1357 .on_drag(|_phase, _ctx| {}),
1358 );
1359 tree.layout(SizeProposal::exact(100.0, 100.0));
1360 let b = tree.bounds(button);
1361 let at = Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
1362
1363 tree.pointer_down_button(at, PointerButton::Primary);
1364 assert!(
1365 mouse_members(&tree).is_empty(),
1366 "the mouse enrols no ancestor across the dead zone"
1367 );
1368 tree.pointer_up_button(at, PointerButton::Primary);
1369
1370 let contact = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 41);
1371 let sample = |phase| PointerSample {
1372 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
1373 phase,
1374 position: at,
1375 button: None,
1376 modifiers: Modifiers::NONE,
1377 coalesced: Vec::new(),
1378 };
1379 tree.dispatch_pointer(sample(PointerPhase::Down));
1380 assert!(
1381 tree.sequence_members(contact).is_empty(),
1382 "and neither does a finger"
1383 );
1384 tree.dispatch_pointer(sample(PointerPhase::Up));
1385 }
1386
1387 #[test]
1388 fn drag_hover_calls_on_drag_hover() {
1389 use std::cell::Cell;
1390 use std::rc::Rc;
1391
1392 let hover_count = Rc::new(Cell::new(0));
1393 let hc = hover_count.clone();
1394
1395 let mut tree = WidgetTree::new();
1396 let source = tree.add(FillWidget::new());
1397 let _target = tree.add(
1398 FillWidget::new()
1399 .on_drag_hover(move |_payload, _pos, _ctx| {
1400 hc.set(hc.get() + 1);
1401 crate::drag_state::DropFeedback::InsertionLine {
1402 y: 50.0,
1403 width: 200.0,
1404 }
1405 })
1406 .on_drop(|_, _, _| true),
1407 );
1408 tree.layout(SizeProposal::exact(200.0, 100.0));
1409
1410 let mut ctx = crate::widget::EventContext::new();
1412 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("test"));
1413 tree.collect_from_ctx(ctx, source);
1414
1415 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1417
1418 assert!(
1419 hover_count.get() > 0,
1420 "on_drag_hover should have been called"
1421 );
1422 }
1423
1424 #[test]
1430 fn rejecter_under_engaging_ancestor_leaves_once() {
1431 use std::cell::Cell;
1432 use std::rc::Rc;
1433
1434 let leaves = Rc::new(Cell::new(0));
1435 let lv = leaves.clone();
1436 let engage = Rc::new(Cell::new(false));
1439 let eg = engage.clone();
1440
1441 let mut tree = WidgetTree::new();
1442 let source = tree.add(FillWidget::new());
1443
1444 let child = tree.add(
1446 FillWidget::new()
1447 .on_drag_hover(|_payload, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1448 .on_drag_leave(move |_ctx| lv.set(lv.get() + 1)),
1449 );
1450 let _ancestor = tree.add(StackWidget::new().child(child).on_drag_hover(
1452 move |_payload, _pos, _ctx| {
1453 if eg.get() {
1454 crate::drag_state::DropFeedback::Accept
1455 } else {
1456 crate::drag_state::DropFeedback::NoFeedback
1457 }
1458 },
1459 ));
1460 tree.layout(SizeProposal::exact(200.0, 100.0));
1461
1462 let mut ctx = crate::widget::EventContext::new();
1463 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1464 tree.collect_from_ctx(ctx, source);
1465
1466 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1468 assert_eq!(
1469 tree.active_drag.as_ref().unwrap().current_target,
1470 Some(child)
1471 );
1472 assert_eq!(leaves.get(), 0, "no leave yet — child is freshly tracked");
1473
1474 engage.set(true);
1476 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(101.0, 50.0)));
1477
1478 assert_eq!(
1479 leaves.get(),
1480 1,
1481 "child must receive exactly one on_drag_leave, not two"
1482 );
1483 }
1484
1485 #[test]
1486 fn drop_outside_window_cancels() {
1487 let mut tree = WidgetTree::new();
1488 let source = tree.add(FillWidget::new());
1489 tree.layout(SizeProposal::exact(100.0, 50.0));
1490
1491 let mut ctx = crate::widget::EventContext::new();
1492 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1493 tree.collect_from_ctx(ctx, source);
1494 assert!(tree.active_drag.is_some());
1495
1496 tree.dispatch_event(WidgetEvent::pointer_up(
1498 Point::new(-100.0, -100.0),
1499 PointerButton::Primary,
1500 Modifiers::NONE,
1501 ));
1502
1503 assert!(tree.active_drag.is_none(), "drag should be cleared");
1504 }
1505
1506 #[test]
1507 fn drop_target_rejects_wrong_type() {
1508 use std::cell::Cell;
1509 use std::rc::Rc;
1510
1511 let accepted = Rc::new(Cell::new(false));
1512 let a = accepted.clone();
1513
1514 let mut tree = WidgetTree::new();
1515 let source = tree.add(FillWidget::new());
1516 let _target = tree.add(
1518 FillWidget::new()
1519 .on_drag_hover(|payload, _pos, _ctx| {
1520 if payload.has_typed::<String>() {
1521 crate::drag_state::DropFeedback::InsertionLine {
1522 y: 0.0,
1523 width: 100.0,
1524 }
1525 } else {
1526 crate::drag_state::DropFeedback::NoFeedback
1527 }
1528 })
1529 .on_drop(move |payload, _pos, _ctx| {
1530 if payload.has_typed::<String>() {
1531 a.set(true);
1532 true
1533 } else {
1534 false
1535 }
1536 }),
1537 );
1538 tree.layout(SizeProposal::exact(200.0, 100.0));
1539
1540 let mut ctx = crate::widget::EventContext::new();
1542 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1543 tree.collect_from_ctx(ctx, source);
1544
1545 tree.dispatch_event(WidgetEvent::pointer_up(
1546 Point::new(150.0, 50.0),
1547 PointerButton::Primary,
1548 Modifiers::NONE,
1549 ));
1550
1551 assert!(!accepted.get(), "on_drop should reject wrong payload type");
1552 }
1553
1554 #[test]
1555 fn inter_widget_drop_transfers_payload() {
1556 use std::cell::Cell;
1557 use std::rc::Rc;
1558
1559 let received_value = Rc::new(Cell::new(0_u32));
1560 let rv = received_value.clone();
1561
1562 let mut tree = WidgetTree::new();
1563 let source = tree.add(FillWidget::new());
1564 let _target = tree.add(
1565 FillWidget::new()
1566 .on_drag_hover(|_payload, _pos, _ctx| {
1567 crate::drag_state::DropFeedback::InsertionLine {
1568 y: 0.0,
1569 width: 100.0,
1570 }
1571 })
1572 .on_drop(move |mut payload, _pos, _ctx| {
1573 if let Some(val) = payload.take_typed::<u32>() {
1574 rv.set(val);
1575 true
1576 } else {
1577 false
1578 }
1579 }),
1580 );
1581 tree.layout(SizeProposal::exact(200.0, 100.0));
1582
1583 let mut ctx = crate::widget::EventContext::new();
1585 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(777_u32));
1586 tree.collect_from_ctx(ctx, source);
1587
1588 tree.dispatch_event(WidgetEvent::pointer_up(
1590 Point::new(150.0, 50.0),
1591 PointerButton::Primary,
1592 Modifiers::NONE,
1593 ));
1594
1595 assert_eq!(
1596 received_value.get(),
1597 777,
1598 "Target should receive the typed payload from source"
1599 );
1600 }
1601
1602 #[test]
1603 fn drop_on_child_walks_up_to_ancestor_drop_target() {
1604 use crate::test_widgets::StackWidget;
1605 use std::cell::Cell;
1606 use std::rc::Rc;
1607
1608 let parent_fired = Rc::new(Cell::new(false));
1611 let pf = parent_fired.clone();
1612
1613 let mut tree = WidgetTree::new();
1614 let source = tree.add(FillWidget::new());
1615 let child = tree.add(FillWidget::new());
1616 let _parent = tree.add(StackWidget::new().child(child).on_drop(
1617 move |_payload, _pos, _ctx| {
1618 pf.set(true);
1619 true
1620 },
1621 ));
1622 tree.layout(SizeProposal::exact(200.0, 100.0));
1623
1624 let mut ctx = crate::widget::EventContext::new();
1626 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
1627 tree.collect_from_ctx(ctx, source);
1628
1629 tree.dispatch_event(WidgetEvent::pointer_up(
1632 Point::new(100.0, 50.0),
1633 PointerButton::Primary,
1634 Modifiers::NONE,
1635 ));
1636
1637 assert!(
1638 parent_fired.get(),
1639 "Parent's on_drop should fire via ancestor walk"
1640 );
1641 }
1642
1643 #[test]
1644 fn drop_bubbles_past_a_rejecting_child_to_ancestor() {
1645 use crate::test_widgets::StackWidget;
1646 use std::cell::Cell;
1647 use std::rc::Rc;
1648
1649 let child_drop = Rc::new(Cell::new(false));
1654 let parent_drop = Rc::new(Cell::new(false));
1655 let cd = child_drop.clone();
1656 let pd = parent_drop.clone();
1657
1658 let mut tree = WidgetTree::new();
1659 let source = tree.add(FillWidget::new());
1660 let child = tree.add(
1661 FillWidget::new()
1662 .on_drag_hover(|_p, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1663 .on_drop(move |_p, _pos, _ctx| {
1664 cd.set(true);
1665 false }),
1667 );
1668 let _parent = tree.add(
1669 StackWidget::new()
1670 .child(child)
1671 .on_drop(move |_p, _pos, _ctx| {
1672 pd.set(true);
1673 true
1674 }),
1675 );
1676 tree.layout(SizeProposal::exact(200.0, 100.0));
1677
1678 let mut ctx = crate::widget::EventContext::new();
1679 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u8));
1680 tree.collect_from_ctx(ctx, source);
1681
1682 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1685 tree.dispatch_event(WidgetEvent::pointer_up(
1686 Point::new(100.0, 50.0),
1687 PointerButton::Primary,
1688 Modifiers::NONE,
1689 ));
1690
1691 assert!(parent_drop.get(), "drop bubbles to the accepting ancestor");
1692 assert!(
1693 !child_drop.get(),
1694 "the rejecting child must not receive the drop"
1695 );
1696 }
1697
1698 #[test]
1699 fn drag_preview_overlay_created_and_dismissed() {
1700 let mut tree = WidgetTree::new();
1701 let source = tree.add(FillWidget::new());
1702 tree.layout(SizeProposal::exact(100.0, 50.0));
1703
1704 let overlay_count_before = tree.overlay_manager().len();
1705
1706 let mut ctx = crate::widget::EventContext::new();
1708 ctx.start_drag_with_preview(
1709 source,
1710 crate::drag_payload::DragPayload::typed(0_u32),
1711 Box::new(FillWidget::new()),
1712 );
1713 tree.collect_from_ctx(ctx, source);
1714
1715 assert!(tree.active_drag.is_some(), "drag session should be active");
1716 assert!(
1717 tree.active_drag
1718 .as_ref()
1719 .unwrap()
1720 .preview_overlay_id
1721 .is_some(),
1722 "preview overlay id should be recorded"
1723 );
1724 assert_eq!(
1725 tree.overlay_manager().len(),
1726 overlay_count_before + 1,
1727 "overlay count should increase by one for the preview"
1728 );
1729
1730 tree.dispatch_event(WidgetEvent::pointer_up(
1732 Point::new(999.0, 999.0),
1733 PointerButton::Primary,
1734 Modifiers::NONE,
1735 ));
1736
1737 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1738 assert_eq!(
1739 tree.overlay_manager().len(),
1740 overlay_count_before,
1741 "preview overlay should be dismissed on drop"
1742 );
1743 }
1744
1745 #[test]
1746 fn drag_preview_follows_pointer_position() {
1747 let mut tree = WidgetTree::new();
1748 let source = tree.add(FillWidget::new());
1749 tree.layout(SizeProposal::exact(200.0, 100.0));
1750
1751 let mut ctx = crate::widget::EventContext::new();
1752 ctx.start_drag_with_preview(
1753 source,
1754 crate::drag_payload::DragPayload::typed("p"),
1755 Box::new(FillWidget::new()),
1756 );
1757 tree.collect_from_ctx(ctx, source);
1758
1759 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(73.0, 41.0)));
1760
1761 let drag = tree.active_drag.as_ref().expect("active drag");
1762 assert!(
1763 (drag.current_position.x - 73.0).abs() < 0.01
1764 && (drag.current_position.y - 41.0).abs() < 0.01,
1765 "drag session position should track the pointer"
1766 );
1767
1768 let overlay_id = drag.preview_overlay_id.expect("preview overlay");
1769 let overlay = tree
1770 .overlay_manager()
1771 .overlay(overlay_id)
1772 .expect("overlay looked up by id");
1773 match &overlay.placement {
1774 crate::overlay::OverlayPlacement::AtPointer(p) => {
1775 assert!(
1776 (p.x - 73.0).abs() < 0.01 && (p.y - 41.0).abs() < 0.01,
1777 "preview overlay placement should follow pointer"
1778 );
1779 }
1780 other => panic!("expected AtPointer placement, got {:?}", other),
1781 }
1782 }
1783
1784 #[test]
1785 fn escape_during_hover_dismisses_preview() {
1786 let mut tree = WidgetTree::new();
1787 let source = tree.add(FillWidget::new());
1788 let _target = tree.add(
1789 FillWidget::new()
1790 .on_drag_hover(|_payload, _pos, _ctx| {
1791 crate::drag_state::DropFeedback::InsertionLine {
1792 y: 0.0,
1793 width: 100.0,
1794 }
1795 })
1796 .on_drop(|_, _, _| true),
1797 );
1798 tree.layout(SizeProposal::exact(200.0, 100.0));
1799
1800 let overlay_count_before = tree.overlay_manager().len();
1801
1802 let mut ctx = crate::widget::EventContext::new();
1803 ctx.start_drag_with_preview(
1804 source,
1805 crate::drag_payload::DragPayload::typed(0_u32),
1806 Box::new(FillWidget::new()),
1807 );
1808 tree.collect_from_ctx(ctx, source);
1809
1810 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1812
1813 assert!(tree.active_drag.is_some());
1814 assert_eq!(tree.overlay_manager().len(), overlay_count_before + 1);
1815
1816 tree.press_key(Key::Escape, Modifiers::NONE);
1818
1819 assert!(tree.active_drag.is_none(), "drag must be cancelled");
1820 assert_eq!(
1821 tree.overlay_manager().len(),
1822 overlay_count_before,
1823 "preview overlay must be dismissed after Escape"
1824 );
1825 }
1826
1827 #[test]
1828 fn active_drag_blocks_on_tap_on_other_widgets() {
1829 use std::cell::Cell;
1830 use std::rc::Rc;
1831
1832 let tap_fired = Rc::new(Cell::new(false));
1837 let tf = tap_fired.clone();
1838
1839 let mut tree = WidgetTree::new();
1840 let source = tree.add(FillWidget::new());
1841 let _other = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
1842 tf.set(true);
1843 }));
1844 tree.layout(SizeProposal::exact(200.0, 100.0));
1845
1846 let mut ctx = crate::widget::EventContext::new();
1847 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1848 tree.collect_from_ctx(ctx, source);
1849
1850 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1853 tree.dispatch_event(WidgetEvent::pointer_up(
1854 Point::new(150.0, 50.0),
1855 PointerButton::Primary,
1856 Modifiers::NONE,
1857 ));
1858
1859 assert!(
1860 !tap_fired.get(),
1861 "on_tap must not fire during an active drag"
1862 );
1863 }
1864
1865 #[test]
1868 fn on_drag_leave_fires_when_pointer_leaves_target_bounds() {
1869 use crate::test_widgets::InsetWidget;
1875 use std::cell::Cell;
1876 use std::rc::Rc;
1877
1878 let leave = Rc::new(Cell::new(0_u32));
1879 let l = leave.clone();
1880
1881 let mut tree = WidgetTree::new();
1882 let source = tree.add(FillWidget::new());
1883 let target = tree.add(
1884 FillWidget::new()
1885 .on_drag_hover(
1886 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1887 y: 0.0,
1888 width: 10.0,
1889 },
1890 )
1891 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1892 .on_drop(|_, _, _| true),
1893 );
1894 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
1895 tree.layout(SizeProposal::exact(200.0, 100.0));
1896
1897 let mut ctx = crate::widget::EventContext::new();
1898 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1899 tree.collect_from_ctx(ctx, source);
1900
1901 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1903 assert_eq!(leave.get(), 0, "no leave yet — target just became active");
1904
1905 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
1910 assert_eq!(
1911 leave.get(),
1912 1,
1913 "on_drag_leave fires when pointer exits the target's bounds"
1914 );
1915
1916 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1918 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1919 assert_eq!(
1920 leave.get(),
1921 1,
1922 "leave fires at most once per leave transition"
1923 );
1924
1925 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
1927 assert_eq!(leave.get(), 2);
1928 }
1929
1930 #[test]
1931 fn on_drag_leave_fires_on_drop() {
1932 use std::cell::Cell;
1933 use std::rc::Rc;
1934
1935 let leave = Rc::new(Cell::new(0_u32));
1936 let l = leave.clone();
1937
1938 let mut tree = WidgetTree::new();
1939 let source = tree.add(FillWidget::new());
1940 let _target = tree.add(
1941 FillWidget::new()
1942 .on_drag_hover(
1943 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1944 y: 0.0,
1945 width: 10.0,
1946 },
1947 )
1948 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1949 .on_drop(|_, _, _| true),
1950 );
1951 tree.layout(SizeProposal::exact(200.0, 100.0));
1952
1953 let mut ctx = crate::widget::EventContext::new();
1954 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1955 tree.collect_from_ctx(ctx, source);
1956 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1957 tree.dispatch_event(WidgetEvent::pointer_up(
1958 Point::new(100.0, 50.0),
1959 PointerButton::Primary,
1960 Modifiers::NONE,
1961 ));
1962
1963 assert_eq!(leave.get(), 1, "on_drag_leave fires exactly once on drop");
1964 }
1965
1966 #[test]
1967 fn on_drag_leave_fires_on_escape_cancel() {
1968 use std::cell::Cell;
1969 use std::rc::Rc;
1970
1971 let leave = Rc::new(Cell::new(0_u32));
1972 let l = leave.clone();
1973
1974 let mut tree = WidgetTree::new();
1975 let source = tree.add(FillWidget::new());
1976 let _target = tree.add(
1977 FillWidget::new()
1978 .on_drag_hover(
1979 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1980 y: 0.0,
1981 width: 10.0,
1982 },
1983 )
1984 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1985 .on_drop(|_, _, _| true),
1986 );
1987 tree.layout(SizeProposal::exact(200.0, 100.0));
1988
1989 let mut ctx = crate::widget::EventContext::new();
1990 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1991 tree.collect_from_ctx(ctx, source);
1992 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1993 tree.press_key(Key::Escape, Modifiers::NONE);
1994
1995 assert_eq!(
1996 leave.get(),
1997 1,
1998 "Escape cancel must fire on_drag_leave on the current target"
1999 );
2000 }
2001
2002 #[test]
2003 fn on_drag_leave_fires_when_source_destroyed_mid_drag() {
2004 use std::cell::Cell;
2005 use std::rc::Rc;
2006
2007 let leave = Rc::new(Cell::new(0_u32));
2008 let l = leave.clone();
2009
2010 let mut tree = WidgetTree::new();
2011 let source = tree.add(FillWidget::new());
2012 let _target = tree.add(
2013 FillWidget::new()
2014 .on_drag_hover(
2015 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2016 y: 0.0,
2017 width: 10.0,
2018 },
2019 )
2020 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2021 .on_drop(|_, _, _| true),
2022 );
2023 tree.layout(SizeProposal::exact(200.0, 100.0));
2024
2025 let mut ctx = crate::widget::EventContext::new();
2026 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2027 tree.collect_from_ctx(ctx, source);
2028 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2029
2030 tree.arena.destroy(source);
2031 tree.layout(SizeProposal::exact(200.0, 100.0));
2034
2035 assert!(
2036 tree.active_drag.is_none(),
2037 "active drag should have been cancelled"
2038 );
2039 assert_eq!(
2040 leave.get(),
2041 1,
2042 "on_drag_leave fires on the drop target when the source is torn down"
2043 );
2044 }
2045
2046 #[test]
2047 fn on_drag_tick_fires_per_layout_pass() {
2048 use std::cell::Cell;
2049 use std::rc::Rc;
2050
2051 let ticks = Rc::new(Cell::new(0_u32));
2052 let t = ticks.clone();
2053
2054 let mut tree = WidgetTree::new();
2055 let source = tree.add(FillWidget::new());
2056 let _target = tree.add(
2057 FillWidget::new()
2058 .on_drag_hover(
2059 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2060 y: 0.0,
2061 width: 10.0,
2062 },
2063 )
2064 .on_drag_tick(move |_pos, _ctx| t.set(t.get() + 1))
2065 .on_drop(|_, _, _| true),
2066 );
2067 tree.layout(SizeProposal::exact(200.0, 100.0));
2068
2069 let mut ctx = crate::widget::EventContext::new();
2070 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2071 tree.collect_from_ctx(ctx, source);
2072 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2074 assert_eq!(ticks.get(), 0, "tick shouldn't have fired yet");
2075
2076 tree.layout(SizeProposal::exact(200.0, 100.0));
2077 assert_eq!(ticks.get(), 1);
2078 tree.layout(SizeProposal::exact(200.0, 100.0));
2079 tree.layout(SizeProposal::exact(200.0, 100.0));
2080 assert_eq!(ticks.get(), 3);
2081
2082 tree.dispatch_event(WidgetEvent::pointer_up(
2084 Point::new(100.0, 50.0),
2085 PointerButton::Primary,
2086 Modifiers::NONE,
2087 ));
2088 let after_drop = ticks.get();
2089 tree.layout(SizeProposal::exact(200.0, 100.0));
2090 tree.layout(SizeProposal::exact(200.0, 100.0));
2091 assert_eq!(
2092 ticks.get(),
2093 after_drop,
2094 "on_drag_tick must not fire after drag ends"
2095 );
2096 }
2097
2098 #[test]
2099 fn on_drag_hover_and_on_drop_receive_widget_local_coordinates() {
2100 use crate::test_widgets::InsetWidget;
2107 use std::cell::Cell;
2108 use std::rc::Rc;
2109
2110 let hover_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2111 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2112 let h = hover_local.clone();
2113 let d = drop_local.clone();
2114
2115 let mut tree = WidgetTree::new();
2116 let source = tree.add(FillWidget::new());
2117 let target = tree.add(
2119 FillWidget::new()
2120 .on_drag_hover(move |_p, pos, _ctx| {
2121 h.set(pos);
2122 crate::drag_state::DropFeedback::InsertionLine {
2123 y: 0.0,
2124 width: 10.0,
2125 }
2126 })
2127 .on_drop(move |_payload, pos, _ctx| {
2128 d.set(pos);
2129 true
2130 }),
2131 );
2132 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2133 tree.layout(SizeProposal::exact(200.0, 100.0));
2134
2135 let mut ctx = crate::widget::EventContext::new();
2136 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2137 tree.collect_from_ctx(ctx, source);
2138
2139 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 60.0)));
2143 let hov = hover_local.get();
2144 assert!(
2145 (hov.x - 60.0).abs() < 0.01 && (hov.y - 20.0).abs() < 0.01,
2146 "on_drag_hover should receive local coords, got {:?}",
2147 hov,
2148 );
2149
2150 tree.dispatch_event(WidgetEvent::pointer_up(
2152 Point::new(110.0, 55.0),
2153 PointerButton::Primary,
2154 Modifiers::NONE,
2155 ));
2156 let drp = drop_local.get();
2157 assert!(
2158 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2159 "on_drop should receive local coords, got {:?}",
2160 drp,
2161 );
2162 }
2163
2164 #[test]
2165 fn active_drag_sets_grabbing_cursor() {
2166 let mut tree = WidgetTree::new();
2171 let source = tree.add(FillWidget::new());
2172 tree.layout(SizeProposal::exact(100.0, 50.0));
2173
2174 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2175
2176 let mut ctx = crate::widget::EventContext::new();
2177 ctx.start_drag_with_preview(
2178 source,
2179 crate::drag_payload::DragPayload::typed(0_u32),
2180 Box::new(FillWidget::new()),
2181 );
2182 tree.collect_from_ctx(ctx, source);
2183 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2184
2185 tree.dispatch_event(WidgetEvent::pointer_up(
2187 Point::new(50.0, 25.0),
2188 PointerButton::Primary,
2189 Modifiers::NONE,
2190 ));
2191 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2192 }
2193
2194 #[test]
2195 fn escape_cancel_resets_cursor() {
2196 let mut tree = WidgetTree::new();
2197 let source = tree.add(FillWidget::new());
2198 tree.layout(SizeProposal::exact(100.0, 50.0));
2199
2200 let mut ctx = crate::widget::EventContext::new();
2201 ctx.start_drag_with_preview(
2202 source,
2203 crate::drag_payload::DragPayload::typed(0_u32),
2204 Box::new(FillWidget::new()),
2205 );
2206 tree.collect_from_ctx(ctx, source);
2207 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2208
2209 tree.press_key(Key::Escape, Modifiers::NONE);
2210 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2211 }
2212
2213 #[test]
2214 fn drag_preview_composite_gets_built() {
2215 use std::cell::Cell;
2221 use std::rc::Rc;
2222
2223 let built = Rc::new(Cell::new(false));
2224 let b = built.clone();
2225
2226 #[derive(Debug)]
2227 struct CheckingWidget {
2228 built: Rc<Cell<bool>>,
2229 }
2230 impl Widget for CheckingWidget {
2231 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2232 self.built.set(true);
2233 Vec::new()
2234 }
2235 fn layout_response(
2236 &self,
2237 _: SizeProposal,
2238 _: &crate::widget::LayoutContext,
2239 ) -> crate::widget::LayoutResponse {
2240 teksilo_canvas::Size::new(50.0, 20.0).into()
2241 }
2242 }
2243
2244 let mut tree = WidgetTree::new();
2245 let source = tree.add(FillWidget::new());
2246 tree.layout(SizeProposal::exact(200.0, 100.0));
2247
2248 let mut ctx = crate::widget::EventContext::new();
2249 ctx.start_drag_with_preview(
2250 source,
2251 crate::drag_payload::DragPayload::typed(0_u32),
2252 Box::new(CheckingWidget { built: b }),
2253 );
2254 tree.collect_from_ctx(ctx, source);
2255
2256 assert!(built.get(), "preview's build() must fire on drag start");
2257 }
2258
2259 #[test]
2260 fn preview_placement_drives_layout_needs() {
2261 let mut tree = WidgetTree::new();
2269 let source = tree.add(FillWidget::new());
2270 tree.layout(SizeProposal::exact(200.0, 200.0));
2271
2272 let mut ctx = crate::widget::EventContext::new();
2273 ctx.start_drag_with_preview(
2274 source,
2275 crate::drag_payload::DragPayload::typed(0_u32),
2276 Box::new(FillWidget::new()),
2277 );
2278 tree.collect_from_ctx(ctx, source);
2279
2280 assert!(
2283 tree.needs_layout(),
2284 "drag start must mark preview content for layout"
2285 );
2286 tree.layout(SizeProposal::exact(200.0, 200.0));
2287 assert!(
2288 !tree.needs_layout(),
2289 "layout should have cleared dirty flag"
2290 );
2291
2292 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(75.0, 120.0)));
2295 assert!(
2296 tree.needs_layout(),
2297 "PointerMove during drag must mark preview for layout"
2298 );
2299 }
2300
2301 #[test]
2302 fn scroll_during_drag_routes_to_drop_target() {
2303 use std::cell::Cell;
2304 use std::rc::Rc;
2305
2306 let scroll_count = Rc::new(Cell::new(0_u32));
2307 let sc = scroll_count.clone();
2308
2309 let mut tree = WidgetTree::new();
2310 let source = tree.add(FillWidget::new());
2311 let _target = tree.add(
2312 FillWidget::new()
2313 .on_drag_hover(
2314 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2315 y: 0.0,
2316 width: 10.0,
2317 },
2318 )
2319 .on_scroll(move |event, _ctx| match event {
2320 WidgetEvent::Scroll { .. } => {
2321 sc.set(sc.get() + 1);
2322 EventResponse::Handled
2323 }
2324 _ => EventResponse::Ignored,
2325 })
2326 .on_drop(|_, _, _| true),
2327 );
2328 tree.layout(SizeProposal::exact(200.0, 100.0));
2329
2330 let mut ctx = crate::widget::EventContext::new();
2331 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2332 tree.collect_from_ctx(ctx, source);
2333 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2335
2336 tree.dispatch_event(WidgetEvent::scroll(
2339 crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
2340 Default::default(),
2341 ));
2342 assert_eq!(
2343 scroll_count.get(),
2344 1,
2345 "Scroll during drag must route to the current drop target"
2346 );
2347 }
2348
2349 #[test]
2352 fn external_drop_delivers_files_and_marks_external() {
2353 use crate::drag_payload::ExternalDropData;
2354 use std::cell::RefCell;
2355 use std::path::PathBuf;
2356 use std::rc::Rc;
2357
2358 let dropped_files: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
2359 let was_external = Rc::new(std::cell::Cell::new(false));
2360 let df = dropped_files.clone();
2361 let we = was_external.clone();
2362
2363 let mut tree = WidgetTree::new();
2364 let _target = tree.add(
2365 FillWidget::new()
2366 .on_drag_hover(|payload, _pos, _ctx| {
2367 if payload.is_external() && !payload.files().is_empty() {
2369 crate::drag_state::DropFeedback::HighlightRect {
2370 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2371 color: teksilo_tokens::Color::WHITE,
2372 }
2373 } else {
2374 crate::drag_state::DropFeedback::NoFeedback
2375 }
2376 })
2377 .on_drop(move |payload, _pos, _ctx| {
2378 we.set(payload.is_external());
2379 *df.borrow_mut() = payload.files().to_vec();
2380 true
2381 }),
2382 );
2383 tree.layout(SizeProposal::exact(200.0, 100.0));
2384
2385 let mut noop = crate::window::NoopWindowOps;
2386 let data = ExternalDropData {
2387 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2388 ..Default::default()
2389 };
2390 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2391 assert!(tree.active_drag.is_some());
2392 assert!(tree.active_drag.as_ref().unwrap().is_external);
2393
2394 tree.update_external_drag(Point::new(110.0, 55.0), &mut noop);
2395 let drop_data = ExternalDropData {
2397 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2398 ..Default::default()
2399 };
2400 tree.end_external_drag(Point::new(110.0, 55.0), drop_data, &mut noop);
2401
2402 assert!(
2403 tree.active_drag.is_none(),
2404 "external drag must clear on drop"
2405 );
2406 assert!(was_external.get(), "payload should report external origin");
2407 assert_eq!(
2408 *dropped_files.borrow(),
2409 vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2410 );
2411 }
2412
2413 #[test]
2414 fn external_drop_passes_local_coordinates() {
2415 use crate::drag_payload::ExternalDropData;
2416 use crate::test_widgets::InsetWidget;
2417 use std::cell::Cell;
2418 use std::path::PathBuf;
2419 use std::rc::Rc;
2420
2421 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2422 let d = drop_local.clone();
2423
2424 let mut tree = WidgetTree::new();
2425 let target = tree.add(
2427 FillWidget::new()
2428 .on_drag_hover(
2429 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2430 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2431 color: teksilo_tokens::Color::WHITE,
2432 },
2433 )
2434 .on_drop(move |_payload, pos, _ctx| {
2435 d.set(pos);
2436 true
2437 }),
2438 );
2439 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2440 tree.layout(SizeProposal::exact(200.0, 100.0));
2441
2442 let mut noop = crate::window::NoopWindowOps;
2443 let data = ExternalDropData {
2444 files: vec![PathBuf::from("/tmp/x")],
2445 ..Default::default()
2446 };
2447 tree.begin_external_drag(Point::new(110.0, 55.0), data, &mut noop);
2449 tree.end_external_drag(
2450 Point::new(110.0, 55.0),
2451 ExternalDropData::default(),
2452 &mut noop,
2453 );
2454
2455 let drp = drop_local.get();
2456 assert!(
2457 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2458 "external on_drop should receive local coords, got {:?}",
2459 drp,
2460 );
2461 }
2462
2463 #[test]
2464 fn cancel_external_drag_clears_session_and_fires_leave() {
2465 use crate::drag_payload::ExternalDropData;
2466 use std::cell::Cell;
2467 use std::path::PathBuf;
2468 use std::rc::Rc;
2469
2470 let left = Rc::new(Cell::new(0_u32));
2471 let l = left.clone();
2472
2473 let mut tree = WidgetTree::new();
2474 let _target = tree.add(
2475 FillWidget::new()
2476 .on_drag_hover(
2477 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2478 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2479 color: teksilo_tokens::Color::WHITE,
2480 },
2481 )
2482 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2483 .on_drop(|_, _, _| true),
2484 );
2485 tree.layout(SizeProposal::exact(200.0, 100.0));
2486
2487 let mut noop = crate::window::NoopWindowOps;
2488 let data = ExternalDropData {
2489 files: vec![PathBuf::from("/tmp/x")],
2490 ..Default::default()
2491 };
2492 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2493 assert!(tree.active_drag.is_some());
2494
2495 tree.cancel_external_drag(&mut noop);
2496 assert!(tree.active_drag.is_none(), "cancel must clear the session");
2497 assert_eq!(
2498 left.get(),
2499 1,
2500 "cancel must fire on_drag_leave on the target"
2501 );
2502 }
2503
2504 #[test]
2505 fn external_drag_helpers_noop_without_session() {
2506 let mut tree = WidgetTree::new();
2508 let _t = tree.add(FillWidget::new());
2509 tree.layout(SizeProposal::exact(100.0, 50.0));
2510
2511 let mut noop = crate::window::NoopWindowOps;
2512 tree.update_external_drag(Point::new(10.0, 10.0), &mut noop);
2513 tree.end_external_drag(
2514 Point::new(10.0, 10.0),
2515 crate::drag_payload::ExternalDropData::default(),
2516 &mut noop,
2517 );
2518 tree.cancel_external_drag(&mut noop);
2519 assert!(tree.active_drag.is_none());
2520 }
2521
2522 struct RecordingWindowOps {
2527 started: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OutboundDragData>>>,
2528 started_kinds: std::rc::Rc<std::cell::RefCell<Vec<teksilo_tokens::PointerKind>>>,
2530 succeed: bool,
2531 cancels: std::rc::Rc<std::cell::Cell<usize>>,
2532 accepts: std::rc::Rc<std::cell::RefCell<Vec<bool>>>,
2534 }
2535
2536 impl RecordingWindowOps {
2537 fn new(succeed: bool) -> Self {
2538 Self {
2539 started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2540 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2541 succeed,
2542 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2543 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2544 }
2545 }
2546 }
2547 impl crate::window::WindowOps for RecordingWindowOps {
2548 fn open_window(
2549 &mut self,
2550 _c: crate::window::WindowConfig,
2551 ) -> crate::window::TeksiloWindowId {
2552 panic!("not used in these tests")
2553 }
2554 fn find_window(&self, _s: &str) -> Option<crate::window::TeksiloWindowId> {
2555 None
2556 }
2557 fn window_state(
2558 &self,
2559 _id: crate::window::TeksiloWindowId,
2560 ) -> Option<crate::window::WindowState> {
2561 None
2562 }
2563 fn windows(&self) -> Vec<crate::window::WindowState> {
2564 Vec::new()
2565 }
2566 fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
2567 fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
2568 fn begin_os_drag(
2569 &mut self,
2570 data: crate::drag_payload::OutboundDragData,
2571 _image: Option<crate::drag_payload::DragImageData>,
2572 pointer: teksilo_tokens::PointerKind,
2573 ) -> bool {
2574 self.started.borrow_mut().push(data);
2575 self.started_kinds.borrow_mut().push(pointer);
2576 self.succeed
2577 }
2578 fn cancel_os_drag(&mut self) {
2579 self.cancels.set(self.cancels.get() + 1);
2580 }
2581 fn set_drop_accepted(&mut self, accepted: bool) {
2582 self.accepts.borrow_mut().push(accepted);
2583 }
2584 }
2585
2586 fn exportable_payload() -> crate::drag_payload::DragPayload {
2587 crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"hi".to_vec())
2588 }
2589
2590 #[test]
2591 fn internal_exportable_drag_escalates_when_leaving_window() {
2592 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2593 let mut ops = RecordingWindowOps {
2594 started: started.clone(),
2595 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2596 succeed: true,
2597 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2598 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2599 };
2600
2601 let mut tree = WidgetTree::new();
2602 let source = tree.add(FillWidget::new());
2603 tree.layout(SizeProposal::exact(200.0, 100.0));
2604
2605 let mut ctx = crate::widget::EventContext::new();
2606 ctx.start_drag(source, exportable_payload());
2607 tree.collect_from_ctx(ctx, source);
2608 assert!(tree.active_drag.is_some());
2609
2610 tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
2612 assert!(started.borrow().is_empty());
2613 assert!(tree.active_drag.is_some());
2614
2615 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2617 assert_eq!(started.borrow().len(), 1, "begin_os_drag called once");
2618 assert!(
2619 started.borrow()[0].mime.contains_key("text/plain"),
2620 "outbound data carries the payload's mime"
2621 );
2622 assert!(tree.active_drag.is_none(), "in-app session torn down");
2623 assert_eq!(tree.outbound_drag_source, Some(source));
2624 }
2625
2626 #[test]
2627 fn os_drag_ended_fires_source_on_drag_ended() {
2628 use crate::drag_payload::DropOutcome;
2629 use std::cell::Cell;
2630 use std::rc::Rc;
2631
2632 let outcome = Rc::new(Cell::new(None));
2633 let o = outcome.clone();
2634
2635 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2636 let mut ops = RecordingWindowOps {
2637 started,
2638 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2639 succeed: true,
2640 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2641 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2642 };
2643
2644 let mut tree = WidgetTree::new();
2645 let source =
2646 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2647 tree.layout(SizeProposal::exact(200.0, 100.0));
2648
2649 let mut ctx = crate::widget::EventContext::new();
2650 ctx.start_drag(source, exportable_payload());
2651 tree.collect_from_ctx(ctx, source);
2652 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2654
2655 tree.handle_os_drag_ended(DropOutcome::OsMove, &mut ops);
2656 assert_eq!(outcome.get(), Some(DropOutcome::OsMove));
2657 assert!(
2658 tree.outbound_drag_source.is_none(),
2659 "cleared after delivery"
2660 );
2661 }
2662
2663 #[test]
2664 fn escape_during_an_escalated_drag_asks_the_platform_to_cancel() {
2665 let mut ops = RecordingWindowOps::new(true);
2670 let cancels = ops.cancels.clone();
2671
2672 let mut tree = WidgetTree::new();
2673 let source = tree.add(FillWidget::new());
2674 tree.layout(SizeProposal::exact(200.0, 100.0));
2675
2676 let mut ctx = crate::widget::EventContext::new();
2677 ctx.start_drag(source, exportable_payload());
2678 tree.collect_from_ctx(ctx, source);
2679 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2681
2682 tree.dispatch_event_with_ops(
2683 crate::event::WidgetEvent::KeyDown {
2684 key: crate::event::Key::Escape,
2685 modifiers: crate::event::Modifiers::NONE,
2686 text: None,
2687 },
2688 &mut ops,
2689 );
2690 assert_eq!(cancels.get(), 1, "the platform must be asked to cancel");
2691 assert_eq!(
2692 tree.outbound_drag_source,
2693 Some(source),
2694 "the session stays until the backend reports its terminal outcome — \
2695 tearing it down here would drop the source's on_drag_ended"
2696 );
2697 }
2698
2699 #[test]
2700 fn escape_without_an_os_drag_does_not_touch_the_platform() {
2701 let mut ops = RecordingWindowOps::new(true);
2702 let cancels = ops.cancels.clone();
2703
2704 let mut tree = WidgetTree::new();
2705 tree.add(FillWidget::new());
2706 tree.layout(SizeProposal::exact(200.0, 100.0));
2707
2708 tree.dispatch_event_with_ops(
2709 crate::event::WidgetEvent::KeyDown {
2710 key: crate::event::Key::Escape,
2711 modifiers: crate::event::Modifiers::NONE,
2712 text: None,
2713 },
2714 &mut ops,
2715 );
2716 assert_eq!(cancels.get(), 0);
2717 }
2718
2719 #[test]
2720 fn no_backend_keeps_session_active_on_leave() {
2721 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2724 let mut ops = RecordingWindowOps {
2725 started: started.clone(),
2726 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2727 succeed: false,
2728 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2729 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2730 };
2731
2732 let mut tree = WidgetTree::new();
2733 let source = tree.add(FillWidget::new());
2734 tree.layout(SizeProposal::exact(200.0, 100.0));
2735
2736 let mut ctx = crate::widget::EventContext::new();
2737 ctx.start_drag(source, exportable_payload());
2738 tree.collect_from_ctx(ctx, source);
2739 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2740
2741 assert_eq!(started.borrow().len(), 1, "escalation was attempted");
2742 assert!(tree.active_drag.is_some(), "session kept (no backend)");
2743 assert!(tree.outbound_drag_source.is_none());
2744 }
2745
2746 #[test]
2747 fn non_exportable_drag_does_not_escalate() {
2748 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2749 let mut ops = RecordingWindowOps {
2750 started: started.clone(),
2751 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2752 succeed: true,
2753 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2754 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2755 };
2756
2757 let mut tree = WidgetTree::new();
2758 let source = tree.add(FillWidget::new());
2759 tree.layout(SizeProposal::exact(200.0, 100.0));
2760
2761 let mut ctx = crate::widget::EventContext::new();
2763 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u32));
2764 tree.collect_from_ctx(ctx, source);
2765 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2766
2767 assert!(started.borrow().is_empty(), "no escalation attempt");
2768 assert!(tree.active_drag.is_some(), "session unaffected");
2769 }
2770
2771 #[test]
2772 fn in_app_drop_fires_source_on_drag_ended_with_accepted() {
2773 use crate::drag_payload::DropOutcome;
2774 use std::cell::Cell;
2775 use std::rc::Rc;
2776
2777 let outcome = Rc::new(Cell::new(None));
2778 let o = outcome.clone();
2779
2780 let mut tree = WidgetTree::new();
2781 let source =
2782 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2783 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
2784 tree.layout(SizeProposal::exact(200.0, 100.0));
2785
2786 let mut ctx = crate::widget::EventContext::new();
2787 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
2788 tree.collect_from_ctx(ctx, source);
2789
2790 tree.dispatch_event(WidgetEvent::pointer_up(
2791 Point::new(150.0, 50.0),
2792 PointerButton::Primary,
2793 Modifiers::NONE,
2794 ));
2795
2796 assert_eq!(outcome.get(), Some(DropOutcome::InApp { accepted: true }));
2797 }
2798
2799 #[test]
2800 fn escape_fires_source_on_drag_ended_cancelled() {
2801 use crate::drag_payload::DropOutcome;
2802 use std::cell::Cell;
2803 use std::rc::Rc;
2804
2805 let outcome = Rc::new(Cell::new(None));
2806 let o = outcome.clone();
2807
2808 let mut tree = WidgetTree::new();
2809 let source =
2810 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2811 tree.layout(SizeProposal::exact(200.0, 100.0));
2812
2813 let mut ctx = crate::widget::EventContext::new();
2814 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2815 tree.collect_from_ctx(ctx, source);
2816
2817 tree.press_key(Key::Escape, Modifiers::NONE);
2818 assert_eq!(outcome.get(), Some(DropOutcome::Cancelled));
2819 }
2820
2821 #[test]
2826 fn os_drag_reentry_recovers_typed_payload_for_in_app_drop() {
2827 use crate::drag_payload::{DragPayload, DropOutcome};
2828 use std::cell::Cell;
2829 use std::rc::Rc;
2830
2831 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2832 let mut ops = RecordingWindowOps {
2833 started,
2834 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2835 succeed: true,
2836 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2837 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2838 };
2839
2840 let got_typed = Rc::new(Cell::new(0_u32));
2841 let ended = Rc::new(Cell::new(0_u32));
2842 let last_outcome = Rc::new(Cell::new(None));
2843 let g = got_typed.clone();
2844 let e = ended.clone();
2845 let lo = last_outcome.clone();
2846
2847 let mut tree = WidgetTree::new();
2848 let source = tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| {
2849 e.set(e.get() + 1);
2850 lo.set(Some(outcome));
2851 }));
2852 let _target =
2853 tree.add(
2854 FillWidget::new().on_drop(move |mut p, _, _| match p.take_typed::<u32>() {
2855 Some(v) => {
2856 g.set(v);
2857 true
2858 }
2859 None => false,
2860 }),
2861 );
2862 tree.layout(SizeProposal::exact(200.0, 100.0));
2863
2864 let mut ctx = crate::widget::EventContext::new();
2866 ctx.start_drag(
2867 source,
2868 DragPayload::typed(123_u32).with_mime("text/plain", b"x".to_vec()),
2869 );
2870 tree.collect_from_ctx(ctx, source);
2871
2872 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2874 assert!(tree.active_drag.is_none());
2875 assert!(
2876 super::has_outbound_typed(),
2877 "typed payload stashed globally"
2878 );
2879
2880 tree.begin_external_drag(
2883 Point::new(100.0, 50.0),
2884 crate::drag_payload::ExternalDropData::default(),
2885 &mut ops,
2886 );
2887 let d = tree.active_drag.as_ref().expect("re-entered session");
2888 assert!(!d.is_external, "re-entry is an internal session");
2889 assert!(d.payload.has_typed::<u32>(), "typed payload recovered");
2890 assert_eq!(
2891 d.payload.text(),
2892 Some("x"),
2893 "external view enriched from MIME so DropZone-style targets also accept"
2894 );
2895 assert!(!super::has_outbound_typed(), "stash taken by the re-entry");
2896
2897 tree.end_external_drag(
2899 Point::new(150.0, 50.0),
2900 crate::drag_payload::ExternalDropData::default(),
2901 &mut ops,
2902 );
2903 assert_eq!(got_typed.get(), 123, "target received the typed payload");
2904 assert_eq!(
2905 ended.get(),
2906 0,
2907 "source on_drag_ended not fired by the drop itself"
2908 );
2909
2910 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2913 assert_eq!(ended.get(), 1, "on_drag_ended fired exactly once");
2914 assert_eq!(last_outcome.get(), Some(DropOutcome::OsCopy));
2915 }
2916
2917 #[test]
2921 fn os_drag_reentry_recovers_typed_payload_across_windows() {
2922 use crate::drag_payload::{DragPayload, DropOutcome};
2923 use std::cell::Cell;
2924 use std::rc::Rc;
2925
2926 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2927 let mut ops = RecordingWindowOps {
2928 started,
2929 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2930 succeed: true,
2931 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2932 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2933 };
2934
2935 let mut tree_a = WidgetTree::new();
2937 let src = tree_a.add(FillWidget::new());
2938 tree_a.layout(SizeProposal::exact(200.0, 100.0));
2939 let mut ctx = crate::widget::EventContext::new();
2940 ctx.start_drag(
2941 src,
2942 DragPayload::typed(77_u32).with_mime("text/plain", b"x".to_vec()),
2943 );
2944 tree_a.collect_from_ctx(ctx, src);
2945 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2946 assert!(super::has_outbound_typed());
2947 assert_eq!(tree_a.outbound_drag_source, Some(src));
2948
2949 let got = Rc::new(Cell::new(0_u32));
2952 let g = got.clone();
2953 let mut tree_b = WidgetTree::new();
2954 let _t = tree_b.add(FillWidget::new().on_drop(
2955 move |mut p, _, _| match p.take_typed::<u32>() {
2956 Some(v) => {
2957 g.set(v);
2958 true
2959 }
2960 None => false,
2961 },
2962 ));
2963 tree_b.layout(SizeProposal::exact(200.0, 100.0));
2964
2965 tree_b.begin_external_drag(
2966 Point::new(50.0, 50.0),
2967 crate::drag_payload::ExternalDropData::default(),
2968 &mut ops,
2969 );
2970 assert!(
2971 tree_b
2972 .active_drag
2973 .as_ref()
2974 .is_some_and(|d| d.payload.has_typed::<u32>()),
2975 "window B recovered the typed payload"
2976 );
2977 tree_b.end_external_drag(
2978 Point::new(50.0, 50.0),
2979 crate::drag_payload::ExternalDropData::default(),
2980 &mut ops,
2981 );
2982 assert_eq!(
2983 got.get(),
2984 77,
2985 "window B's target received the typed payload"
2986 );
2987
2988 tree_a.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2990 }
2991
2992 #[test]
2996 fn os_drag_reexit_restashes_payload() {
2997 use crate::drag_payload::DragPayload;
2998 use std::rc::Rc;
2999
3000 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3001 let mut ops = RecordingWindowOps {
3002 started: started.clone(),
3003 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3004 succeed: true,
3005 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3006 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3007 };
3008
3009 let mut tree = WidgetTree::new();
3010 let source = tree.add(FillWidget::new());
3011 tree.layout(SizeProposal::exact(200.0, 100.0));
3012 let mut ctx = crate::widget::EventContext::new();
3013 ctx.start_drag(
3014 source,
3015 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3016 );
3017 tree.collect_from_ctx(ctx, source);
3018
3019 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3021 assert_eq!(started.borrow().len(), 1, "OS drag started once");
3022 tree.begin_external_drag(
3023 Point::new(100.0, 50.0),
3024 crate::drag_payload::ExternalDropData::default(),
3025 &mut ops,
3026 );
3027 assert!(tree.os_drag_reentered);
3028 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(!tree.os_drag_reentered, "re-exited");
3031 assert!(tree.active_drag.is_none(), "session torn down on re-exit");
3032 assert!(super::has_outbound_typed(), "payload re-stashed");
3033 assert_eq!(
3034 started.borrow().len(),
3035 1,
3036 "no second OS drag started on re-exit"
3037 );
3038 }
3039
3040 #[test]
3044 fn source_window_close_clears_stash_no_hijack() {
3045 use crate::drag_payload::{DragPayload, ExternalDropData};
3046 use std::path::PathBuf;
3047 use std::rc::Rc;
3048
3049 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3050 let mut ops = RecordingWindowOps {
3051 started,
3052 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3053 succeed: true,
3054 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3055 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3056 };
3057
3058 let mut tree = WidgetTree::new();
3059 let source = tree.add(FillWidget::new());
3060 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
3061 tree.layout(SizeProposal::exact(200.0, 100.0));
3062 let mut ctx = crate::widget::EventContext::new();
3063 ctx.start_drag(
3064 source,
3065 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3066 );
3067 tree.collect_from_ctx(ctx, source);
3068 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(super::has_outbound_typed());
3070 assert_eq!(tree.outbound_drag_source, Some(source));
3071
3072 tree.abort_outbound_drag();
3074 assert!(
3075 !super::has_outbound_typed(),
3076 "stash cleared when the source window closes"
3077 );
3078 assert!(tree.outbound_drag_source.is_none());
3079
3080 tree.begin_external_drag(
3083 Point::new(50.0, 50.0),
3084 ExternalDropData {
3085 files: vec![PathBuf::from("/tmp/real")],
3086 ..Default::default()
3087 },
3088 &mut ops,
3089 );
3090 let d = tree.active_drag.as_ref().expect("external session");
3091 assert!(
3092 d.is_external,
3093 "stale stash did not hijack the new external drag"
3094 );
3095 assert!(
3096 !d.payload.has_typed::<u32>(),
3097 "no stale typed payload leaked in"
3098 );
3099 assert_eq!(d.payload.files(), &[PathBuf::from("/tmp/real")]);
3100 }
3101
3102 use std::cell::{Cell, RefCell};
3107 use std::rc::Rc;
3108
3109 fn drag_arming_leaf(
3117 slot: Rc<Cell<Option<WidgetId>>>,
3118 payload: impl Fn() -> crate::drag_payload::DragPayload + 'static,
3119 ) -> impl crate::widget::Widget {
3120 let armed = Cell::new(false);
3121 FillWidget::new().on_pointer_event(move |event, ctx| {
3122 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3123 && !armed.replace(true)
3124 && let Some(id) = slot.get()
3125 {
3126 ctx.start_drag(id, payload());
3127 }
3128 crate::event::EventResponse::Ignored
3129 })
3130 }
3131
3132 fn touch_press(
3134 tree: &mut WidgetTree,
3135 at: Point,
3136 ops: &mut dyn crate::window::WindowOps,
3137 ) -> crate::pointer::PointerId {
3138 use crate::pointer::{
3139 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
3140 };
3141 use std::sync::atomic::{AtomicU64, Ordering};
3142 static NEXT: AtomicU64 = AtomicU64::new(91_000);
3143 let contact = PointerIdAllocator::global().begin(
3144 BackendDeviceKey::DEFAULT,
3145 NEXT.fetch_add(1, Ordering::Relaxed),
3146 );
3147 tree.dispatch_pointer_with_ops(
3148 PointerSample {
3149 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
3150 phase: PointerPhase::Down,
3151 position: at,
3152 button: None,
3153 modifiers: Modifiers::NONE,
3154 coalesced: Vec::new(),
3155 },
3156 ops,
3157 );
3158 contact
3159 }
3160
3161 fn placement_of(
3163 tree: &WidgetTree,
3164 id: crate::overlay::OverlayId,
3165 ) -> crate::overlay::OverlayPlacement {
3166 tree.overlay_manager
3167 .stack
3168 .iter()
3169 .find(|o| o.id == id)
3170 .map(|o| o.placement.clone())
3171 .expect("the overlay is in the stack")
3172 }
3173
3174 #[test]
3182 fn a_drag_tick_reports_the_device_that_started_the_drag() {
3183 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3184 let s = seen.clone();
3185
3186 let mut tree = WidgetTree::new();
3187 let target = tree.add(
3188 FillWidget::new()
3189 .on_drop(|_, _, _| true)
3190 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3191 );
3192 tree.layout(SizeProposal::exact(200.0, 100.0));
3193
3194 let mut ctx = crate::widget::EventContext::new();
3196 ctx.start_drag(target, crate::drag_payload::DragPayload::typed(1_u8));
3197 tree.collect_from_ctx(ctx, target);
3198 tree.handle_drag_move(Point::new(50.0, 50.0), &mut crate::window::NoopWindowOps);
3199 tree.layout(SizeProposal::exact(200.0, 100.0));
3200 assert_eq!(
3201 *seen.borrow(),
3202 vec![teksilo_tokens::PointerKind::Mouse],
3203 "a mouse drag still reports the mouse"
3204 );
3205 tree.cancel_active_drag(&mut crate::window::NoopWindowOps);
3206 seen.borrow_mut().clear();
3207
3208 let mut ops = crate::window::NoopWindowOps;
3211 let s = seen.clone();
3212 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3213 let armed = Cell::new(false);
3214 let s2 = slot.clone();
3215 let mut tree = WidgetTree::new();
3216 let row = tree.add(
3217 FillWidget::new()
3218 .on_pointer_event(move |event, ctx| {
3219 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3220 && !armed.replace(true)
3221 && let Some(id) = s2.get()
3222 {
3223 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(2_u8));
3224 }
3225 crate::event::EventResponse::Ignored
3226 })
3227 .on_drop(|_, _, _| true)
3228 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3229 );
3230 slot.set(Some(row));
3231 tree.layout(SizeProposal::exact(200.0, 100.0));
3232 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3233 assert!(tree.active_drag.is_some(), "the finger armed a drag");
3234 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3235 tree.layout(SizeProposal::exact(200.0, 100.0));
3236 assert_eq!(
3237 *seen.borrow(),
3238 vec![teksilo_tokens::PointerKind::Touch],
3239 "and a finger drag reports the finger, from a tick that has no sample"
3240 );
3241 }
3242
3243 #[test]
3251 fn the_preview_avoids_a_coarse_contact_and_still_pins_a_cursor() {
3252 use crate::overlay::OverlayPlacement;
3253
3254 let mut ops = crate::window::NoopWindowOps;
3255 let at = Point::new(60.0, 40.0);
3256
3257 let mut tree = WidgetTree::new();
3259 let source = tree.add(FillWidget::new());
3260 tree.layout(SizeProposal::exact(200.0, 100.0));
3261 let mut ctx = crate::widget::EventContext::new();
3262 ctx.start_drag_with_preview(
3263 source,
3264 crate::drag_payload::DragPayload::typed(1_u8),
3265 Box::new(FillWidget::new()),
3266 );
3267 tree.collect_from_ctx(ctx, source);
3268 tree.handle_drag_move(at, &mut ops);
3269 let overlay = tree
3270 .active_drag
3271 .as_ref()
3272 .and_then(|d| d.preview_overlay_id)
3273 .expect("the preview overlay exists");
3274 assert!(
3275 matches!(placement_of(&tree, overlay), OverlayPlacement::AtPointer(p) if p == at),
3276 "a mouse keeps AtPointer at the reported point, unchanged",
3277 );
3278
3279 let mut tree = WidgetTree::new();
3282 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3283 let source = tree.add(drag_arming_leaf_with_preview(slot.clone()));
3284 slot.set(Some(source));
3285 tree.layout(SizeProposal::exact(200.0, 100.0));
3286 touch_press(&mut tree, at, &mut ops);
3287 assert_eq!(
3288 tree.active_drag.as_ref().map(|d| d.pointer.kind),
3289 Some(teksilo_tokens::PointerKind::Touch),
3290 "the session recorded the finger",
3291 );
3292 tree.handle_drag_move(at, &mut ops);
3293 let overlay = tree
3294 .active_drag
3295 .as_ref()
3296 .and_then(|d| d.preview_overlay_id)
3297 .expect("the preview overlay exists");
3298 match placement_of(&tree, overlay) {
3299 OverlayPlacement::AtPointerAvoiding { point, avoid } => {
3300 assert_eq!(point, at);
3301 assert!(
3302 avoid.contains(at),
3303 "the rectangle to clear is centred on the contact, so no \
3304 placement that honours it can put the preview under the hand",
3305 );
3306 }
3307 other => panic!("a coarse pointer must avoid its own contact, got {other:?}"),
3308 }
3309 }
3310
3311 fn drag_arming_leaf_with_preview(
3314 slot: Rc<Cell<Option<WidgetId>>>,
3315 ) -> impl crate::widget::Widget {
3316 let armed = Cell::new(false);
3317 FillWidget::new().on_pointer_event(move |event, ctx| {
3318 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3319 && !armed.replace(true)
3320 && let Some(id) = slot.get()
3321 {
3322 ctx.start_drag_with_preview(
3323 id,
3324 crate::drag_payload::DragPayload::typed(1_u8),
3325 Box::new(FillWidget::new()),
3326 );
3327 }
3328 crate::event::EventResponse::Ignored
3329 })
3330 }
3331
3332 #[test]
3341 fn an_os_drag_started_by_a_finger_cancels_that_finger() {
3342 let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
3343 Rc::new(RefCell::new(Vec::new()));
3344 let c = cancelled.clone();
3345
3346 let mut ops = RecordingWindowOps::new(true);
3347 let kinds = ops.started_kinds.clone();
3348
3349 let mut tree = WidgetTree::new();
3350 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3351 let armed = Cell::new(false);
3352 let s2 = slot.clone();
3353 let source = tree.add(
3354 FillWidget::new()
3355 .on_pointer_event(move |event, ctx| {
3356 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3357 && !armed.replace(true)
3358 && let Some(id) = s2.get()
3359 {
3360 ctx.start_drag(id, exportable_payload());
3361 }
3362 crate::event::EventResponse::Ignored
3363 })
3364 .on_pointer_cancel(move |pointer, reason, _ctx| {
3365 c.borrow_mut().push((pointer.id, reason));
3366 }),
3367 );
3368 slot.set(Some(source));
3369 tree.layout(SizeProposal::exact(200.0, 100.0));
3370
3371 let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
3372 assert!(tree.active_drag.is_some());
3373
3374 tree.handle_drag_move(Point::new(-40.0, 50.0), &mut ops);
3376 assert_eq!(tree.outbound_drag_source, Some(source));
3377 assert_eq!(
3378 *kinds.borrow(),
3379 vec![teksilo_tokens::PointerKind::Touch],
3380 "the platform is told which device is dragging — Wayland needs the \
3381 touch-down serial, not a button serial"
3382 );
3383 assert_eq!(
3384 *cancelled.borrow(),
3385 vec![(contact, crate::pointer::CancelReason::OsDragStarted)],
3386 "the finger is the pointer revoked"
3387 );
3388 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3389 }
3390
3391 #[test]
3398 fn an_escape_cancelled_finger_drag_reports_the_finger_to_its_source() {
3399 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3400 let s = seen.clone();
3401
3402 let mut ops = crate::window::NoopWindowOps;
3403 let mut tree = WidgetTree::new();
3404 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3405 let armed = Cell::new(false);
3406 let s2 = slot.clone();
3407 let source = tree.add(
3408 FillWidget::new()
3409 .on_pointer_event(move |event, ctx| {
3410 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3411 && !armed.replace(true)
3412 && let Some(id) = s2.get()
3413 {
3414 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(1_u8));
3415 }
3416 crate::event::EventResponse::Ignored
3417 })
3418 .on_drag_ended(move |_outcome, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3419 );
3420 slot.set(Some(source));
3421 tree.layout(SizeProposal::exact(200.0, 100.0));
3422
3423 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3424 assert!(tree.active_drag.is_some());
3425 tree.dispatch_event_with_ops(
3426 crate::event::WidgetEvent::KeyDown {
3427 key: crate::event::Key::Escape,
3428 modifiers: crate::event::Modifiers::NONE,
3429 text: None,
3430 },
3431 &mut ops,
3432 );
3433 assert_eq!(*seen.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
3434 }
3435
3436 #[test]
3442 fn the_accept_setter_receives_the_widget_verdict() {
3443 use crate::drag_state::DropFeedback;
3444
3445 let verdict = Rc::new(Cell::new(false));
3446 let v = verdict.clone();
3447 let mut ops = RecordingWindowOps::new(true);
3448 let accepts = ops.accepts.clone();
3449
3450 let mut tree = WidgetTree::new();
3451 tree.add(
3452 FillWidget::new()
3453 .on_drag_hover(move |_p, _pos, _ctx| {
3454 if v.get() {
3455 DropFeedback::Accept
3456 } else {
3457 DropFeedback::NoFeedback
3458 }
3459 })
3460 .on_drop(|_, _, _| true),
3461 );
3462 tree.layout(SizeProposal::exact(200.0, 100.0));
3463
3464 tree.begin_external_drag(
3465 Point::new(50.0, 50.0),
3466 crate::drag_payload::ExternalDropData {
3467 files: vec![std::path::PathBuf::from("/tmp/a.png")],
3468 ..Default::default()
3469 },
3470 &mut ops,
3471 );
3472 assert_eq!(
3473 *accepts.borrow(),
3474 vec![false],
3475 "a refusing target is reported to the OS as a refusal"
3476 );
3477
3478 tree.update_external_drag(Point::new(52.0, 50.0), &mut ops);
3481 assert_eq!(
3482 *accepts.borrow(),
3483 vec![false],
3484 "an unchanged answer is not re-sent"
3485 );
3486
3487 verdict.set(true);
3489 tree.update_external_drag(Point::new(54.0, 50.0), &mut ops);
3490 assert_eq!(
3491 *accepts.borrow(),
3492 vec![false, true],
3493 "and a change is pushed once"
3494 );
3495 }
3496
3497 #[test]
3500 fn an_in_app_drag_pushes_no_os_accept_state() {
3501 let mut ops = RecordingWindowOps::new(true);
3502 let accepts = ops.accepts.clone();
3503
3504 let mut tree = WidgetTree::new();
3505 let source = tree.add(FillWidget::new());
3506 tree.add(FillWidget::new().on_drop(|_, _, _| true));
3507 tree.layout(SizeProposal::exact(200.0, 100.0));
3508
3509 let mut ctx = crate::widget::EventContext::new();
3510 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
3511 tree.collect_from_ctx(ctx, source);
3512 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3513
3514 assert!(accepts.borrow().is_empty());
3515 }
3516
3517 #[test]
3526 fn a_cancelled_os_drag_tears_the_source_down_exactly_once() {
3527 use crate::drag_payload::{DragPayload, DropOutcome};
3528
3529 let ended = Rc::new(RefCell::new(Vec::new()));
3530 let e = ended.clone();
3531 let mut ops = RecordingWindowOps::new(true);
3532
3533 let mut tree_a = WidgetTree::new();
3535 let src = tree_a.add(
3536 FillWidget::new().on_drag_ended(move |outcome, _ctx| e.borrow_mut().push(outcome)),
3537 );
3538 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3539 let mut ctx = crate::widget::EventContext::new();
3540 ctx.start_drag(
3541 src,
3542 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3543 );
3544 tree_a.collect_from_ctx(ctx, src);
3545 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3546 assert!(super::outbound_is_live());
3547
3548 let left = Rc::new(Cell::new(0_u32));
3550 let l = left.clone();
3551 let mut tree_b = WidgetTree::new();
3552 tree_b.add(
3553 FillWidget::new()
3554 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3555 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3556 .on_drop(|_, _, _| true),
3557 );
3558 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3559 let accepts = ops.accepts.clone();
3560 accepts.borrow_mut().clear();
3561 tree_b.begin_external_drag(
3562 Point::new(50.0, 50.0),
3563 crate::drag_payload::ExternalDropData::default(),
3564 &mut ops,
3565 );
3566 assert!(tree_b.os_drag_reentered);
3567 assert_eq!(
3568 *accepts.borrow(),
3569 vec![true],
3570 "a re-entered app drag still negotiates with the OS: its offer is \
3571 live and a refusal must still reach the compositor's cursor",
3572 );
3573
3574 tree_b.abort_external_drag(&mut ops);
3577 assert!(tree_b.active_drag.is_none(), "B's session is gone");
3578 assert!(!tree_b.os_drag_reentered);
3579 assert_eq!(left.get(), 1, "B's highlighted target was cleared");
3580 assert!(
3581 ended.borrow().is_empty(),
3582 "the abort must not fire the source's on_drag_ended — the source \
3583 window owns that"
3584 );
3585
3586 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3588 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
3589
3590 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3592 assert_eq!(
3593 *ended.borrow(),
3594 vec![DropOutcome::Cancelled],
3595 "exactly once"
3596 );
3597 }
3598
3599 #[test]
3609 fn a_reentered_session_is_reaped_when_the_os_drag_ends_elsewhere() {
3610 use crate::drag_payload::{DragPayload, DropOutcome};
3611
3612 let mut ops = RecordingWindowOps::new(true);
3613
3614 let mut tree_a = WidgetTree::new();
3615 let src = tree_a.add(FillWidget::new());
3616 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3617 let mut ctx = crate::widget::EventContext::new();
3618 ctx.start_drag(
3619 src,
3620 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3621 );
3622 tree_a.collect_from_ctx(ctx, src);
3623 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3624
3625 let left = Rc::new(Cell::new(0_u32));
3626 let l = left.clone();
3627 let mut tree_b = WidgetTree::new();
3628 tree_b.add(
3629 FillWidget::new()
3630 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3631 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3632 .on_drop(|_, _, _| true),
3633 );
3634 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3635 tree_b.begin_external_drag(
3636 Point::new(50.0, 50.0),
3637 crate::drag_payload::ExternalDropData::default(),
3638 &mut ops,
3639 );
3640 assert!(tree_b.active_drag.is_some() && tree_b.os_drag_reentered);
3641
3642 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3644 assert!(
3645 tree_b.active_drag.is_some(),
3646 "B has not been told anything yet"
3647 );
3648
3649 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3651 assert!(tree_b.active_drag.is_none(), "the dead session was reaped");
3652 assert!(!tree_b.os_drag_reentered);
3653 assert_eq!(left.get(), 1, "and its highlighted target was cleared");
3654 }
3655
3656 #[test]
3660 fn a_reentered_app_drag_recovers_the_device_that_started_it() {
3661 let mut ops = RecordingWindowOps::new(true);
3662
3663 let mut tree_a = WidgetTree::new();
3664 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3665 let src = tree_a.add(drag_arming_leaf(slot.clone(), exportable_payload));
3666 slot.set(Some(src));
3667 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3668 touch_press(&mut tree_a, Point::new(20.0, 50.0), &mut ops);
3669 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3670 assert!(super::outbound_is_live());
3671
3672 let mut tree_b = WidgetTree::new();
3673 tree_b.add(FillWidget::new().on_drop(|_, _, _| true));
3674 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3675 tree_b.begin_external_drag(
3676 Point::new(50.0, 50.0),
3677 crate::drag_payload::ExternalDropData::default(),
3678 &mut ops,
3679 );
3680 assert_eq!(
3681 tree_b.active_drag.as_ref().map(|d| d.pointer.kind),
3682 Some(teksilo_tokens::PointerKind::Touch),
3683 );
3684
3685 let mut tree_c = WidgetTree::new();
3687 tree_c.add(FillWidget::new().on_drop(|_, _, _| true));
3688 tree_c.layout(SizeProposal::exact(200.0, 100.0));
3689 tree_a.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3690 tree_c.begin_external_drag(
3691 Point::new(50.0, 50.0),
3692 crate::drag_payload::ExternalDropData::default(),
3693 &mut ops,
3694 );
3695 assert_eq!(
3696 tree_c.active_drag.as_ref().map(|d| d.pointer.kind),
3697 Some(teksilo_tokens::PointerKind::Unknown),
3698 "no OS names the source's device to a destination",
3699 );
3700 }
3701
3702 #[test]
3706 fn restash_after_drag_ended_is_noop() {
3707 use crate::drag_payload::DragPayload;
3708
3709 super::outbound_begin(
3710 DragPayload::typed(1_u32).with_mime("text/plain", b"x".to_vec()),
3711 crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO),
3712 );
3713 assert!(super::has_outbound_typed());
3714 let held = super::outbound_take_if_live().expect("payload taken while live");
3716 assert!(!super::has_outbound_typed());
3717 super::outbound_end();
3719 super::outbound_restash(held);
3721 assert!(
3722 !super::has_outbound_typed(),
3723 "ended drag is not resurrected by a racing re-stash"
3724 );
3725 assert!(super::outbound_take_if_live().is_none());
3727 }
3728}