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