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().child(button));
1285 let ancestor = tree.add(StackWidget::new().child(inner).on_drag(|_phase, _ctx| {}));
1286 tree.layout(SizeProposal::exact(100.0, 100.0));
1287
1288 let b = tree.bounds(button);
1289 tree.pointer_down_button(
1290 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1291 PointerButton::Primary,
1292 );
1293 assert_eq!(
1294 mouse_members(&tree),
1295 vec![(ancestor, MemberRole::Gesture, MemberState::Possible)],
1296 "the draggable ancestor competes when the button press is not in a dead zone"
1297 );
1298 tree.pointer_up_button(
1299 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1300 PointerButton::Primary,
1301 );
1302 }
1303
1304 #[test]
1305 fn gesture_dead_zone_blocks_ancestor_drag_arming() {
1306 use crate::widget_builder::WidgetBuilder;
1312 let mut tree = WidgetTree::new();
1313 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1314 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1315 let _ancestor = tree.add(
1316 StackWidget::new()
1317 .child(dead_zone)
1318 .on_drag(|_phase, _ctx| {}),
1319 );
1320 tree.layout(SizeProposal::exact(100.0, 100.0));
1321
1322 let b = tree.bounds(button);
1323 tree.pointer_down_button(
1324 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1325 PointerButton::Primary,
1326 );
1327 assert!(
1328 mouse_members(&tree).is_empty(),
1329 "a dead zone blocks the draggable ancestor from competing"
1330 );
1331 tree.pointer_up_button(
1332 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1333 PointerButton::Primary,
1334 );
1335 }
1336
1337 #[test]
1338 fn a_dead_zone_boundary_blocks_a_mouse_exactly_as_it_blocks_a_finger() {
1339 use crate::pointer::{
1344 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
1345 };
1346 use crate::widget_builder::WidgetBuilder;
1347
1348 let mut tree = WidgetTree::new();
1349 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1350 let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
1351 tree.add(
1352 StackWidget::new()
1353 .child(dead_zone)
1354 .on_drag(|_phase, _ctx| {}),
1355 );
1356 tree.layout(SizeProposal::exact(100.0, 100.0));
1357 let b = tree.bounds(button);
1358 let at = Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
1359
1360 tree.pointer_down_button(at, PointerButton::Primary);
1361 assert!(
1362 mouse_members(&tree).is_empty(),
1363 "the mouse enrols no ancestor across the dead zone"
1364 );
1365 tree.pointer_up_button(at, PointerButton::Primary);
1366
1367 let contact = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 41);
1368 let sample = |phase| PointerSample {
1369 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
1370 phase,
1371 position: at,
1372 button: None,
1373 modifiers: Modifiers::NONE,
1374 coalesced: Vec::new(),
1375 };
1376 tree.dispatch_pointer(sample(PointerPhase::Down));
1377 assert!(
1378 tree.sequence_members(contact).is_empty(),
1379 "and neither does a finger"
1380 );
1381 tree.dispatch_pointer(sample(PointerPhase::Up));
1382 }
1383
1384 #[test]
1385 fn drag_hover_calls_on_drag_hover() {
1386 use std::cell::Cell;
1387 use std::rc::Rc;
1388
1389 let hover_count = Rc::new(Cell::new(0));
1390 let hc = hover_count.clone();
1391
1392 let mut tree = WidgetTree::new();
1393 let source = tree.add(FillWidget::new());
1394 let _target = tree.add(
1395 FillWidget::new()
1396 .on_drag_hover(move |_payload, _pos, _ctx| {
1397 hc.set(hc.get() + 1);
1398 crate::drag_state::DropFeedback::InsertionLine {
1399 y: 50.0,
1400 width: 200.0,
1401 }
1402 })
1403 .on_drop(|_, _, _| true),
1404 );
1405 tree.layout(SizeProposal::exact(200.0, 100.0));
1406
1407 let mut ctx = crate::widget::EventContext::new();
1409 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("test"));
1410 tree.collect_from_ctx(ctx, source);
1411
1412 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1414
1415 assert!(
1416 hover_count.get() > 0,
1417 "on_drag_hover should have been called"
1418 );
1419 }
1420
1421 #[test]
1427 fn rejecter_under_engaging_ancestor_leaves_once() {
1428 use std::cell::Cell;
1429 use std::rc::Rc;
1430
1431 let leaves = Rc::new(Cell::new(0));
1432 let lv = leaves.clone();
1433 let engage = Rc::new(Cell::new(false));
1436 let eg = engage.clone();
1437
1438 let mut tree = WidgetTree::new();
1439 let source = tree.add(FillWidget::new());
1440
1441 let child = tree.add(
1443 FillWidget::new()
1444 .on_drag_hover(|_payload, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1445 .on_drag_leave(move |_ctx| lv.set(lv.get() + 1)),
1446 );
1447 let _ancestor = tree.add(StackWidget::new().child(child).on_drag_hover(
1449 move |_payload, _pos, _ctx| {
1450 if eg.get() {
1451 crate::drag_state::DropFeedback::Accept
1452 } else {
1453 crate::drag_state::DropFeedback::NoFeedback
1454 }
1455 },
1456 ));
1457 tree.layout(SizeProposal::exact(200.0, 100.0));
1458
1459 let mut ctx = crate::widget::EventContext::new();
1460 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1461 tree.collect_from_ctx(ctx, source);
1462
1463 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1465 assert_eq!(
1466 tree.active_drag.as_ref().unwrap().current_target,
1467 Some(child)
1468 );
1469 assert_eq!(leaves.get(), 0, "no leave yet — child is freshly tracked");
1470
1471 engage.set(true);
1473 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(101.0, 50.0)));
1474
1475 assert_eq!(
1476 leaves.get(),
1477 1,
1478 "child must receive exactly one on_drag_leave, not two"
1479 );
1480 }
1481
1482 #[test]
1483 fn drop_outside_window_cancels() {
1484 let mut tree = WidgetTree::new();
1485 let source = tree.add(FillWidget::new());
1486 tree.layout(SizeProposal::exact(100.0, 50.0));
1487
1488 let mut ctx = crate::widget::EventContext::new();
1489 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1490 tree.collect_from_ctx(ctx, source);
1491 assert!(tree.active_drag.is_some());
1492
1493 tree.dispatch_event(WidgetEvent::pointer_up(
1495 Point::new(-100.0, -100.0),
1496 PointerButton::Primary,
1497 Modifiers::NONE,
1498 ));
1499
1500 assert!(tree.active_drag.is_none(), "drag should be cleared");
1501 }
1502
1503 #[test]
1504 fn drop_target_rejects_wrong_type() {
1505 use std::cell::Cell;
1506 use std::rc::Rc;
1507
1508 let accepted = Rc::new(Cell::new(false));
1509 let a = accepted.clone();
1510
1511 let mut tree = WidgetTree::new();
1512 let source = tree.add(FillWidget::new());
1513 let _target = tree.add(
1515 FillWidget::new()
1516 .on_drag_hover(|payload, _pos, _ctx| {
1517 if payload.has_typed::<String>() {
1518 crate::drag_state::DropFeedback::InsertionLine {
1519 y: 0.0,
1520 width: 100.0,
1521 }
1522 } else {
1523 crate::drag_state::DropFeedback::NoFeedback
1524 }
1525 })
1526 .on_drop(move |payload, _pos, _ctx| {
1527 if payload.has_typed::<String>() {
1528 a.set(true);
1529 true
1530 } else {
1531 false
1532 }
1533 }),
1534 );
1535 tree.layout(SizeProposal::exact(200.0, 100.0));
1536
1537 let mut ctx = crate::widget::EventContext::new();
1539 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1540 tree.collect_from_ctx(ctx, source);
1541
1542 tree.dispatch_event(WidgetEvent::pointer_up(
1543 Point::new(150.0, 50.0),
1544 PointerButton::Primary,
1545 Modifiers::NONE,
1546 ));
1547
1548 assert!(!accepted.get(), "on_drop should reject wrong payload type");
1549 }
1550
1551 #[test]
1552 fn inter_widget_drop_transfers_payload() {
1553 use std::cell::Cell;
1554 use std::rc::Rc;
1555
1556 let received_value = Rc::new(Cell::new(0_u32));
1557 let rv = received_value.clone();
1558
1559 let mut tree = WidgetTree::new();
1560 let source = tree.add(FillWidget::new());
1561 let _target = tree.add(
1562 FillWidget::new()
1563 .on_drag_hover(|_payload, _pos, _ctx| {
1564 crate::drag_state::DropFeedback::InsertionLine {
1565 y: 0.0,
1566 width: 100.0,
1567 }
1568 })
1569 .on_drop(move |mut payload, _pos, _ctx| {
1570 if let Some(val) = payload.take_typed::<u32>() {
1571 rv.set(val);
1572 true
1573 } else {
1574 false
1575 }
1576 }),
1577 );
1578 tree.layout(SizeProposal::exact(200.0, 100.0));
1579
1580 let mut ctx = crate::widget::EventContext::new();
1582 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(777_u32));
1583 tree.collect_from_ctx(ctx, source);
1584
1585 tree.dispatch_event(WidgetEvent::pointer_up(
1587 Point::new(150.0, 50.0),
1588 PointerButton::Primary,
1589 Modifiers::NONE,
1590 ));
1591
1592 assert_eq!(
1593 received_value.get(),
1594 777,
1595 "Target should receive the typed payload from source"
1596 );
1597 }
1598
1599 #[test]
1600 fn drop_on_child_walks_up_to_ancestor_drop_target() {
1601 use crate::test_widgets::StackWidget;
1602 use std::cell::Cell;
1603 use std::rc::Rc;
1604
1605 let parent_fired = Rc::new(Cell::new(false));
1608 let pf = parent_fired.clone();
1609
1610 let mut tree = WidgetTree::new();
1611 let source = tree.add(FillWidget::new());
1612 let child = tree.add(FillWidget::new());
1613 let _parent = tree.add(StackWidget::new().child(child).on_drop(
1614 move |_payload, _pos, _ctx| {
1615 pf.set(true);
1616 true
1617 },
1618 ));
1619 tree.layout(SizeProposal::exact(200.0, 100.0));
1620
1621 let mut ctx = crate::widget::EventContext::new();
1623 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
1624 tree.collect_from_ctx(ctx, source);
1625
1626 tree.dispatch_event(WidgetEvent::pointer_up(
1629 Point::new(100.0, 50.0),
1630 PointerButton::Primary,
1631 Modifiers::NONE,
1632 ));
1633
1634 assert!(
1635 parent_fired.get(),
1636 "Parent's on_drop should fire via ancestor walk"
1637 );
1638 }
1639
1640 #[test]
1641 fn drop_bubbles_past_a_rejecting_child_to_ancestor() {
1642 use crate::test_widgets::StackWidget;
1643 use std::cell::Cell;
1644 use std::rc::Rc;
1645
1646 let child_drop = Rc::new(Cell::new(false));
1651 let parent_drop = Rc::new(Cell::new(false));
1652 let cd = child_drop.clone();
1653 let pd = parent_drop.clone();
1654
1655 let mut tree = WidgetTree::new();
1656 let source = tree.add(FillWidget::new());
1657 let child = tree.add(
1658 FillWidget::new()
1659 .on_drag_hover(|_p, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1660 .on_drop(move |_p, _pos, _ctx| {
1661 cd.set(true);
1662 false }),
1664 );
1665 let _parent = tree.add(
1666 StackWidget::new()
1667 .child(child)
1668 .on_drop(move |_p, _pos, _ctx| {
1669 pd.set(true);
1670 true
1671 }),
1672 );
1673 tree.layout(SizeProposal::exact(200.0, 100.0));
1674
1675 let mut ctx = crate::widget::EventContext::new();
1676 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u8));
1677 tree.collect_from_ctx(ctx, source);
1678
1679 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1682 tree.dispatch_event(WidgetEvent::pointer_up(
1683 Point::new(100.0, 50.0),
1684 PointerButton::Primary,
1685 Modifiers::NONE,
1686 ));
1687
1688 assert!(parent_drop.get(), "drop bubbles to the accepting ancestor");
1689 assert!(
1690 !child_drop.get(),
1691 "the rejecting child must not receive the drop"
1692 );
1693 }
1694
1695 #[test]
1696 fn drag_preview_overlay_created_and_dismissed() {
1697 let mut tree = WidgetTree::new();
1698 let source = tree.add(FillWidget::new());
1699 tree.layout(SizeProposal::exact(100.0, 50.0));
1700
1701 let overlay_count_before = tree.overlay_manager().len();
1702
1703 let mut ctx = crate::widget::EventContext::new();
1705 ctx.start_drag_with_preview(
1706 source,
1707 crate::drag_payload::DragPayload::typed(0_u32),
1708 Box::new(FillWidget::new()),
1709 );
1710 tree.collect_from_ctx(ctx, source);
1711
1712 assert!(tree.active_drag.is_some(), "drag session should be active");
1713 assert!(
1714 tree.active_drag
1715 .as_ref()
1716 .unwrap()
1717 .preview_overlay_id
1718 .is_some(),
1719 "preview overlay id should be recorded"
1720 );
1721 assert_eq!(
1722 tree.overlay_manager().len(),
1723 overlay_count_before + 1,
1724 "overlay count should increase by one for the preview"
1725 );
1726
1727 tree.dispatch_event(WidgetEvent::pointer_up(
1729 Point::new(999.0, 999.0),
1730 PointerButton::Primary,
1731 Modifiers::NONE,
1732 ));
1733
1734 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1735 assert_eq!(
1736 tree.overlay_manager().len(),
1737 overlay_count_before,
1738 "preview overlay should be dismissed on drop"
1739 );
1740 }
1741
1742 #[test]
1743 fn drag_preview_follows_pointer_position() {
1744 let mut tree = WidgetTree::new();
1745 let source = tree.add(FillWidget::new());
1746 tree.layout(SizeProposal::exact(200.0, 100.0));
1747
1748 let mut ctx = crate::widget::EventContext::new();
1749 ctx.start_drag_with_preview(
1750 source,
1751 crate::drag_payload::DragPayload::typed("p"),
1752 Box::new(FillWidget::new()),
1753 );
1754 tree.collect_from_ctx(ctx, source);
1755
1756 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(73.0, 41.0)));
1757
1758 let drag = tree.active_drag.as_ref().expect("active drag");
1759 assert!(
1760 (drag.current_position.x - 73.0).abs() < 0.01
1761 && (drag.current_position.y - 41.0).abs() < 0.01,
1762 "drag session position should track the pointer"
1763 );
1764
1765 let overlay_id = drag.preview_overlay_id.expect("preview overlay");
1766 let overlay = tree
1767 .overlay_manager()
1768 .overlay(overlay_id)
1769 .expect("overlay looked up by id");
1770 match &overlay.placement {
1771 crate::overlay::OverlayPlacement::AtPointer(p) => {
1772 assert!(
1773 (p.x - 73.0).abs() < 0.01 && (p.y - 41.0).abs() < 0.01,
1774 "preview overlay placement should follow pointer"
1775 );
1776 }
1777 other => panic!("expected AtPointer placement, got {:?}", other),
1778 }
1779 }
1780
1781 #[test]
1782 fn escape_during_hover_dismisses_preview() {
1783 let mut tree = WidgetTree::new();
1784 let source = tree.add(FillWidget::new());
1785 let _target = tree.add(
1786 FillWidget::new()
1787 .on_drag_hover(|_payload, _pos, _ctx| {
1788 crate::drag_state::DropFeedback::InsertionLine {
1789 y: 0.0,
1790 width: 100.0,
1791 }
1792 })
1793 .on_drop(|_, _, _| true),
1794 );
1795 tree.layout(SizeProposal::exact(200.0, 100.0));
1796
1797 let overlay_count_before = tree.overlay_manager().len();
1798
1799 let mut ctx = crate::widget::EventContext::new();
1800 ctx.start_drag_with_preview(
1801 source,
1802 crate::drag_payload::DragPayload::typed(0_u32),
1803 Box::new(FillWidget::new()),
1804 );
1805 tree.collect_from_ctx(ctx, source);
1806
1807 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1809
1810 assert!(tree.active_drag.is_some());
1811 assert_eq!(tree.overlay_manager().len(), overlay_count_before + 1);
1812
1813 tree.press_key(Key::Escape, Modifiers::NONE);
1815
1816 assert!(tree.active_drag.is_none(), "drag must be cancelled");
1817 assert_eq!(
1818 tree.overlay_manager().len(),
1819 overlay_count_before,
1820 "preview overlay must be dismissed after Escape"
1821 );
1822 }
1823
1824 #[test]
1825 fn active_drag_blocks_on_tap_on_other_widgets() {
1826 use std::cell::Cell;
1827 use std::rc::Rc;
1828
1829 let tap_fired = Rc::new(Cell::new(false));
1834 let tf = tap_fired.clone();
1835
1836 let mut tree = WidgetTree::new();
1837 let source = tree.add(FillWidget::new());
1838 let _other = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
1839 tf.set(true);
1840 }));
1841 tree.layout(SizeProposal::exact(200.0, 100.0));
1842
1843 let mut ctx = crate::widget::EventContext::new();
1844 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1845 tree.collect_from_ctx(ctx, source);
1846
1847 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
1850 tree.dispatch_event(WidgetEvent::pointer_up(
1851 Point::new(150.0, 50.0),
1852 PointerButton::Primary,
1853 Modifiers::NONE,
1854 ));
1855
1856 assert!(
1857 !tap_fired.get(),
1858 "on_tap must not fire during an active drag"
1859 );
1860 }
1861
1862 #[test]
1865 fn on_drag_leave_fires_when_pointer_leaves_target_bounds() {
1866 use crate::test_widgets::InsetWidget;
1872 use std::cell::Cell;
1873 use std::rc::Rc;
1874
1875 let leave = Rc::new(Cell::new(0_u32));
1876 let l = leave.clone();
1877
1878 let mut tree = WidgetTree::new();
1879 let source = tree.add(FillWidget::new());
1880 let target = tree.add(
1881 FillWidget::new()
1882 .on_drag_hover(
1883 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1884 y: 0.0,
1885 width: 10.0,
1886 },
1887 )
1888 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1889 .on_drop(|_, _, _| true),
1890 );
1891 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
1892 tree.layout(SizeProposal::exact(200.0, 100.0));
1893
1894 let mut ctx = crate::widget::EventContext::new();
1895 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1896 tree.collect_from_ctx(ctx, source);
1897
1898 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1900 assert_eq!(leave.get(), 0, "no leave yet — target just became active");
1901
1902 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
1907 assert_eq!(
1908 leave.get(),
1909 1,
1910 "on_drag_leave fires when pointer exits the target's bounds"
1911 );
1912
1913 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1915 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1916 assert_eq!(
1917 leave.get(),
1918 1,
1919 "leave fires at most once per leave transition"
1920 );
1921
1922 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
1924 assert_eq!(leave.get(), 2);
1925 }
1926
1927 #[test]
1928 fn on_drag_leave_fires_on_drop() {
1929 use std::cell::Cell;
1930 use std::rc::Rc;
1931
1932 let leave = Rc::new(Cell::new(0_u32));
1933 let l = leave.clone();
1934
1935 let mut tree = WidgetTree::new();
1936 let source = tree.add(FillWidget::new());
1937 let _target = tree.add(
1938 FillWidget::new()
1939 .on_drag_hover(
1940 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1941 y: 0.0,
1942 width: 10.0,
1943 },
1944 )
1945 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1946 .on_drop(|_, _, _| true),
1947 );
1948 tree.layout(SizeProposal::exact(200.0, 100.0));
1949
1950 let mut ctx = crate::widget::EventContext::new();
1951 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1952 tree.collect_from_ctx(ctx, source);
1953 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1954 tree.dispatch_event(WidgetEvent::pointer_up(
1955 Point::new(100.0, 50.0),
1956 PointerButton::Primary,
1957 Modifiers::NONE,
1958 ));
1959
1960 assert_eq!(leave.get(), 1, "on_drag_leave fires exactly once on drop");
1961 }
1962
1963 #[test]
1964 fn on_drag_leave_fires_on_escape_cancel() {
1965 use std::cell::Cell;
1966 use std::rc::Rc;
1967
1968 let leave = Rc::new(Cell::new(0_u32));
1969 let l = leave.clone();
1970
1971 let mut tree = WidgetTree::new();
1972 let source = tree.add(FillWidget::new());
1973 let _target = tree.add(
1974 FillWidget::new()
1975 .on_drag_hover(
1976 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1977 y: 0.0,
1978 width: 10.0,
1979 },
1980 )
1981 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1982 .on_drop(|_, _, _| true),
1983 );
1984 tree.layout(SizeProposal::exact(200.0, 100.0));
1985
1986 let mut ctx = crate::widget::EventContext::new();
1987 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1988 tree.collect_from_ctx(ctx, source);
1989 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
1990 tree.press_key(Key::Escape, Modifiers::NONE);
1991
1992 assert_eq!(
1993 leave.get(),
1994 1,
1995 "Escape cancel must fire on_drag_leave on the current target"
1996 );
1997 }
1998
1999 #[test]
2000 fn on_drag_leave_fires_when_source_destroyed_mid_drag() {
2001 use std::cell::Cell;
2002 use std::rc::Rc;
2003
2004 let leave = Rc::new(Cell::new(0_u32));
2005 let l = leave.clone();
2006
2007 let mut tree = WidgetTree::new();
2008 let source = tree.add(FillWidget::new());
2009 let _target = tree.add(
2010 FillWidget::new()
2011 .on_drag_hover(
2012 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2013 y: 0.0,
2014 width: 10.0,
2015 },
2016 )
2017 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2018 .on_drop(|_, _, _| true),
2019 );
2020 tree.layout(SizeProposal::exact(200.0, 100.0));
2021
2022 let mut ctx = crate::widget::EventContext::new();
2023 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2024 tree.collect_from_ctx(ctx, source);
2025 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2026
2027 tree.arena.destroy(source);
2028 tree.layout(SizeProposal::exact(200.0, 100.0));
2031
2032 assert!(
2033 tree.active_drag.is_none(),
2034 "active drag should have been cancelled"
2035 );
2036 assert_eq!(
2037 leave.get(),
2038 1,
2039 "on_drag_leave fires on the drop target when the source is torn down"
2040 );
2041 }
2042
2043 #[test]
2044 fn on_drag_tick_fires_per_layout_pass() {
2045 use std::cell::Cell;
2046 use std::rc::Rc;
2047
2048 let ticks = Rc::new(Cell::new(0_u32));
2049 let t = ticks.clone();
2050
2051 let mut tree = WidgetTree::new();
2052 let source = tree.add(FillWidget::new());
2053 let _target = tree.add(
2054 FillWidget::new()
2055 .on_drag_hover(
2056 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2057 y: 0.0,
2058 width: 10.0,
2059 },
2060 )
2061 .on_drag_tick(move |_pos, _ctx| t.set(t.get() + 1))
2062 .on_drop(|_, _, _| true),
2063 );
2064 tree.layout(SizeProposal::exact(200.0, 100.0));
2065
2066 let mut ctx = crate::widget::EventContext::new();
2067 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2068 tree.collect_from_ctx(ctx, source);
2069 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2071 assert_eq!(ticks.get(), 0, "tick shouldn't have fired yet");
2072
2073 tree.layout(SizeProposal::exact(200.0, 100.0));
2074 assert_eq!(ticks.get(), 1);
2075 tree.layout(SizeProposal::exact(200.0, 100.0));
2076 tree.layout(SizeProposal::exact(200.0, 100.0));
2077 assert_eq!(ticks.get(), 3);
2078
2079 tree.dispatch_event(WidgetEvent::pointer_up(
2081 Point::new(100.0, 50.0),
2082 PointerButton::Primary,
2083 Modifiers::NONE,
2084 ));
2085 let after_drop = ticks.get();
2086 tree.layout(SizeProposal::exact(200.0, 100.0));
2087 tree.layout(SizeProposal::exact(200.0, 100.0));
2088 assert_eq!(
2089 ticks.get(),
2090 after_drop,
2091 "on_drag_tick must not fire after drag ends"
2092 );
2093 }
2094
2095 #[test]
2096 fn on_drag_hover_and_on_drop_receive_widget_local_coordinates() {
2097 use crate::test_widgets::InsetWidget;
2104 use std::cell::Cell;
2105 use std::rc::Rc;
2106
2107 let hover_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2108 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2109 let h = hover_local.clone();
2110 let d = drop_local.clone();
2111
2112 let mut tree = WidgetTree::new();
2113 let source = tree.add(FillWidget::new());
2114 let target = tree.add(
2116 FillWidget::new()
2117 .on_drag_hover(move |_p, pos, _ctx| {
2118 h.set(pos);
2119 crate::drag_state::DropFeedback::InsertionLine {
2120 y: 0.0,
2121 width: 10.0,
2122 }
2123 })
2124 .on_drop(move |_payload, pos, _ctx| {
2125 d.set(pos);
2126 true
2127 }),
2128 );
2129 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2130 tree.layout(SizeProposal::exact(200.0, 100.0));
2131
2132 let mut ctx = crate::widget::EventContext::new();
2133 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2134 tree.collect_from_ctx(ctx, source);
2135
2136 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 60.0)));
2140 let hov = hover_local.get();
2141 assert!(
2142 (hov.x - 60.0).abs() < 0.01 && (hov.y - 20.0).abs() < 0.01,
2143 "on_drag_hover should receive local coords, got {:?}",
2144 hov,
2145 );
2146
2147 tree.dispatch_event(WidgetEvent::pointer_up(
2149 Point::new(110.0, 55.0),
2150 PointerButton::Primary,
2151 Modifiers::NONE,
2152 ));
2153 let drp = drop_local.get();
2154 assert!(
2155 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2156 "on_drop should receive local coords, got {:?}",
2157 drp,
2158 );
2159 }
2160
2161 #[test]
2162 fn active_drag_sets_grabbing_cursor() {
2163 let mut tree = WidgetTree::new();
2168 let source = tree.add(FillWidget::new());
2169 tree.layout(SizeProposal::exact(100.0, 50.0));
2170
2171 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2172
2173 let mut ctx = crate::widget::EventContext::new();
2174 ctx.start_drag_with_preview(
2175 source,
2176 crate::drag_payload::DragPayload::typed(0_u32),
2177 Box::new(FillWidget::new()),
2178 );
2179 tree.collect_from_ctx(ctx, source);
2180 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2181
2182 tree.dispatch_event(WidgetEvent::pointer_up(
2184 Point::new(50.0, 25.0),
2185 PointerButton::Primary,
2186 Modifiers::NONE,
2187 ));
2188 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2189 }
2190
2191 #[test]
2192 fn escape_cancel_resets_cursor() {
2193 let mut tree = WidgetTree::new();
2194 let source = tree.add(FillWidget::new());
2195 tree.layout(SizeProposal::exact(100.0, 50.0));
2196
2197 let mut ctx = crate::widget::EventContext::new();
2198 ctx.start_drag_with_preview(
2199 source,
2200 crate::drag_payload::DragPayload::typed(0_u32),
2201 Box::new(FillWidget::new()),
2202 );
2203 tree.collect_from_ctx(ctx, source);
2204 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
2205
2206 tree.press_key(Key::Escape, Modifiers::NONE);
2207 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2208 }
2209
2210 #[test]
2211 fn drag_preview_composite_gets_built() {
2212 use std::cell::Cell;
2218 use std::rc::Rc;
2219
2220 let built = Rc::new(Cell::new(false));
2221 let b = built.clone();
2222
2223 #[derive(Debug)]
2224 struct CheckingWidget {
2225 built: Rc<Cell<bool>>,
2226 }
2227 impl Widget for CheckingWidget {
2228 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2229 self.built.set(true);
2230 Vec::new()
2231 }
2232 fn layout_response(
2233 &self,
2234 _: SizeProposal,
2235 _: &crate::widget::LayoutContext,
2236 ) -> crate::widget::LayoutResponse {
2237 teksilo_canvas::Size::new(50.0, 20.0).into()
2238 }
2239 }
2240
2241 let mut tree = WidgetTree::new();
2242 let source = tree.add(FillWidget::new());
2243 tree.layout(SizeProposal::exact(200.0, 100.0));
2244
2245 let mut ctx = crate::widget::EventContext::new();
2246 ctx.start_drag_with_preview(
2247 source,
2248 crate::drag_payload::DragPayload::typed(0_u32),
2249 Box::new(CheckingWidget { built: b }),
2250 );
2251 tree.collect_from_ctx(ctx, source);
2252
2253 assert!(built.get(), "preview's build() must fire on drag start");
2254 }
2255
2256 #[test]
2257 fn preview_placement_drives_layout_needs() {
2258 let mut tree = WidgetTree::new();
2266 let source = tree.add(FillWidget::new());
2267 tree.layout(SizeProposal::exact(200.0, 200.0));
2268
2269 let mut ctx = crate::widget::EventContext::new();
2270 ctx.start_drag_with_preview(
2271 source,
2272 crate::drag_payload::DragPayload::typed(0_u32),
2273 Box::new(FillWidget::new()),
2274 );
2275 tree.collect_from_ctx(ctx, source);
2276
2277 assert!(
2280 tree.needs_layout(),
2281 "drag start must mark preview content for layout"
2282 );
2283 tree.layout(SizeProposal::exact(200.0, 200.0));
2284 assert!(
2285 !tree.needs_layout(),
2286 "layout should have cleared dirty flag"
2287 );
2288
2289 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(75.0, 120.0)));
2292 assert!(
2293 tree.needs_layout(),
2294 "PointerMove during drag must mark preview for layout"
2295 );
2296 }
2297
2298 #[test]
2299 fn scroll_during_drag_routes_to_drop_target() {
2300 use std::cell::Cell;
2301 use std::rc::Rc;
2302
2303 let scroll_count = Rc::new(Cell::new(0_u32));
2304 let sc = scroll_count.clone();
2305
2306 let mut tree = WidgetTree::new();
2307 let source = tree.add(FillWidget::new());
2308 let _target = tree.add(
2309 FillWidget::new()
2310 .on_drag_hover(
2311 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2312 y: 0.0,
2313 width: 10.0,
2314 },
2315 )
2316 .on_scroll(move |event, _ctx| match event {
2317 WidgetEvent::Scroll { .. } => {
2318 sc.set(sc.get() + 1);
2319 EventResponse::Handled
2320 }
2321 _ => EventResponse::Ignored,
2322 })
2323 .on_drop(|_, _, _| true),
2324 );
2325 tree.layout(SizeProposal::exact(200.0, 100.0));
2326
2327 let mut ctx = crate::widget::EventContext::new();
2328 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2329 tree.collect_from_ctx(ctx, source);
2330 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
2332
2333 tree.dispatch_event(WidgetEvent::scroll(
2336 crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
2337 Default::default(),
2338 ));
2339 assert_eq!(
2340 scroll_count.get(),
2341 1,
2342 "Scroll during drag must route to the current drop target"
2343 );
2344 }
2345
2346 #[test]
2349 fn external_drop_delivers_files_and_marks_external() {
2350 use crate::drag_payload::ExternalDropData;
2351 use std::cell::RefCell;
2352 use std::path::PathBuf;
2353 use std::rc::Rc;
2354
2355 let dropped_files: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
2356 let was_external = Rc::new(std::cell::Cell::new(false));
2357 let df = dropped_files.clone();
2358 let we = was_external.clone();
2359
2360 let mut tree = WidgetTree::new();
2361 let _target = tree.add(
2362 FillWidget::new()
2363 .on_drag_hover(|payload, _pos, _ctx| {
2364 if payload.is_external() && !payload.files().is_empty() {
2366 crate::drag_state::DropFeedback::HighlightRect {
2367 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2368 color: teksilo_tokens::Color::WHITE,
2369 }
2370 } else {
2371 crate::drag_state::DropFeedback::NoFeedback
2372 }
2373 })
2374 .on_drop(move |payload, _pos, _ctx| {
2375 we.set(payload.is_external());
2376 *df.borrow_mut() = payload.files().to_vec();
2377 true
2378 }),
2379 );
2380 tree.layout(SizeProposal::exact(200.0, 100.0));
2381
2382 let mut noop = crate::window::NoopWindowOps;
2383 let data = ExternalDropData {
2384 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2385 ..Default::default()
2386 };
2387 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2388 assert!(tree.active_drag.is_some());
2389 assert!(tree.active_drag.as_ref().unwrap().is_external);
2390
2391 tree.update_external_drag(Point::new(110.0, 55.0), &mut noop);
2392 let drop_data = ExternalDropData {
2394 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2395 ..Default::default()
2396 };
2397 tree.end_external_drag(Point::new(110.0, 55.0), drop_data, &mut noop);
2398
2399 assert!(
2400 tree.active_drag.is_none(),
2401 "external drag must clear on drop"
2402 );
2403 assert!(was_external.get(), "payload should report external origin");
2404 assert_eq!(
2405 *dropped_files.borrow(),
2406 vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2407 );
2408 }
2409
2410 #[test]
2411 fn external_drop_passes_local_coordinates() {
2412 use crate::drag_payload::ExternalDropData;
2413 use crate::test_widgets::InsetWidget;
2414 use std::cell::Cell;
2415 use std::path::PathBuf;
2416 use std::rc::Rc;
2417
2418 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2419 let d = drop_local.clone();
2420
2421 let mut tree = WidgetTree::new();
2422 let target = tree.add(
2424 FillWidget::new()
2425 .on_drag_hover(
2426 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2427 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2428 color: teksilo_tokens::Color::WHITE,
2429 },
2430 )
2431 .on_drop(move |_payload, pos, _ctx| {
2432 d.set(pos);
2433 true
2434 }),
2435 );
2436 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2437 tree.layout(SizeProposal::exact(200.0, 100.0));
2438
2439 let mut noop = crate::window::NoopWindowOps;
2440 let data = ExternalDropData {
2441 files: vec![PathBuf::from("/tmp/x")],
2442 ..Default::default()
2443 };
2444 tree.begin_external_drag(Point::new(110.0, 55.0), data, &mut noop);
2446 tree.end_external_drag(
2447 Point::new(110.0, 55.0),
2448 ExternalDropData::default(),
2449 &mut noop,
2450 );
2451
2452 let drp = drop_local.get();
2453 assert!(
2454 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2455 "external on_drop should receive local coords, got {:?}",
2456 drp,
2457 );
2458 }
2459
2460 #[test]
2461 fn cancel_external_drag_clears_session_and_fires_leave() {
2462 use crate::drag_payload::ExternalDropData;
2463 use std::cell::Cell;
2464 use std::path::PathBuf;
2465 use std::rc::Rc;
2466
2467 let left = Rc::new(Cell::new(0_u32));
2468 let l = left.clone();
2469
2470 let mut tree = WidgetTree::new();
2471 let _target = tree.add(
2472 FillWidget::new()
2473 .on_drag_hover(
2474 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2475 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2476 color: teksilo_tokens::Color::WHITE,
2477 },
2478 )
2479 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2480 .on_drop(|_, _, _| true),
2481 );
2482 tree.layout(SizeProposal::exact(200.0, 100.0));
2483
2484 let mut noop = crate::window::NoopWindowOps;
2485 let data = ExternalDropData {
2486 files: vec![PathBuf::from("/tmp/x")],
2487 ..Default::default()
2488 };
2489 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2490 assert!(tree.active_drag.is_some());
2491
2492 tree.cancel_external_drag(&mut noop);
2493 assert!(tree.active_drag.is_none(), "cancel must clear the session");
2494 assert_eq!(
2495 left.get(),
2496 1,
2497 "cancel must fire on_drag_leave on the target"
2498 );
2499 }
2500
2501 #[test]
2502 fn external_drag_helpers_noop_without_session() {
2503 let mut tree = WidgetTree::new();
2505 let _t = tree.add(FillWidget::new());
2506 tree.layout(SizeProposal::exact(100.0, 50.0));
2507
2508 let mut noop = crate::window::NoopWindowOps;
2509 tree.update_external_drag(Point::new(10.0, 10.0), &mut noop);
2510 tree.end_external_drag(
2511 Point::new(10.0, 10.0),
2512 crate::drag_payload::ExternalDropData::default(),
2513 &mut noop,
2514 );
2515 tree.cancel_external_drag(&mut noop);
2516 assert!(tree.active_drag.is_none());
2517 }
2518
2519 struct RecordingWindowOps {
2524 started: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OutboundDragData>>>,
2525 started_kinds: std::rc::Rc<std::cell::RefCell<Vec<teksilo_tokens::PointerKind>>>,
2527 succeed: bool,
2528 cancels: std::rc::Rc<std::cell::Cell<usize>>,
2529 accepts: std::rc::Rc<std::cell::RefCell<Vec<bool>>>,
2531 }
2532
2533 impl RecordingWindowOps {
2534 fn new(succeed: bool) -> Self {
2535 Self {
2536 started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2537 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2538 succeed,
2539 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2540 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2541 }
2542 }
2543 }
2544 impl crate::window::WindowOps for RecordingWindowOps {
2545 fn open_window(
2546 &mut self,
2547 _c: crate::window::WindowConfig,
2548 ) -> crate::window::TeksiloWindowId {
2549 panic!("not used in these tests")
2550 }
2551 fn find_window(&self, _s: &str) -> Option<crate::window::TeksiloWindowId> {
2552 None
2553 }
2554 fn window_state(
2555 &self,
2556 _id: crate::window::TeksiloWindowId,
2557 ) -> Option<crate::window::WindowState> {
2558 None
2559 }
2560 fn windows(&self) -> Vec<crate::window::WindowState> {
2561 Vec::new()
2562 }
2563 fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
2564 fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
2565 fn begin_os_drag(
2566 &mut self,
2567 data: crate::drag_payload::OutboundDragData,
2568 _image: Option<crate::drag_payload::DragImageData>,
2569 pointer: teksilo_tokens::PointerKind,
2570 ) -> bool {
2571 self.started.borrow_mut().push(data);
2572 self.started_kinds.borrow_mut().push(pointer);
2573 self.succeed
2574 }
2575 fn cancel_os_drag(&mut self) {
2576 self.cancels.set(self.cancels.get() + 1);
2577 }
2578 fn set_drop_accepted(&mut self, accepted: bool) {
2579 self.accepts.borrow_mut().push(accepted);
2580 }
2581 }
2582
2583 fn exportable_payload() -> crate::drag_payload::DragPayload {
2584 crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"hi".to_vec())
2585 }
2586
2587 #[test]
2588 fn internal_exportable_drag_escalates_when_leaving_window() {
2589 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2590 let mut ops = RecordingWindowOps {
2591 started: started.clone(),
2592 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2593 succeed: true,
2594 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2595 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2596 };
2597
2598 let mut tree = WidgetTree::new();
2599 let source = tree.add(FillWidget::new());
2600 tree.layout(SizeProposal::exact(200.0, 100.0));
2601
2602 let mut ctx = crate::widget::EventContext::new();
2603 ctx.start_drag(source, exportable_payload());
2604 tree.collect_from_ctx(ctx, source);
2605 assert!(tree.active_drag.is_some());
2606
2607 tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
2609 assert!(started.borrow().is_empty());
2610 assert!(tree.active_drag.is_some());
2611
2612 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2614 assert_eq!(started.borrow().len(), 1, "begin_os_drag called once");
2615 assert!(
2616 started.borrow()[0].mime.contains_key("text/plain"),
2617 "outbound data carries the payload's mime"
2618 );
2619 assert!(tree.active_drag.is_none(), "in-app session torn down");
2620 assert_eq!(tree.outbound_drag_source, Some(source));
2621 }
2622
2623 #[test]
2624 fn os_drag_ended_fires_source_on_drag_ended() {
2625 use crate::drag_payload::DropOutcome;
2626 use std::cell::Cell;
2627 use std::rc::Rc;
2628
2629 let outcome = Rc::new(Cell::new(None));
2630 let o = outcome.clone();
2631
2632 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2633 let mut ops = RecordingWindowOps {
2634 started,
2635 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2636 succeed: true,
2637 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2638 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2639 };
2640
2641 let mut tree = WidgetTree::new();
2642 let source =
2643 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2644 tree.layout(SizeProposal::exact(200.0, 100.0));
2645
2646 let mut ctx = crate::widget::EventContext::new();
2647 ctx.start_drag(source, exportable_payload());
2648 tree.collect_from_ctx(ctx, source);
2649 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2651
2652 tree.handle_os_drag_ended(DropOutcome::OsMove, &mut ops);
2653 assert_eq!(outcome.get(), Some(DropOutcome::OsMove));
2654 assert!(
2655 tree.outbound_drag_source.is_none(),
2656 "cleared after delivery"
2657 );
2658 }
2659
2660 #[test]
2661 fn escape_during_an_escalated_drag_asks_the_platform_to_cancel() {
2662 let mut ops = RecordingWindowOps::new(true);
2667 let cancels = ops.cancels.clone();
2668
2669 let mut tree = WidgetTree::new();
2670 let source = tree.add(FillWidget::new());
2671 tree.layout(SizeProposal::exact(200.0, 100.0));
2672
2673 let mut ctx = crate::widget::EventContext::new();
2674 ctx.start_drag(source, exportable_payload());
2675 tree.collect_from_ctx(ctx, source);
2676 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2678
2679 tree.dispatch_event_with_ops(
2680 crate::event::WidgetEvent::KeyDown {
2681 key: crate::event::Key::Escape,
2682 modifiers: crate::event::Modifiers::NONE,
2683 text: None,
2684 },
2685 &mut ops,
2686 );
2687 assert_eq!(cancels.get(), 1, "the platform must be asked to cancel");
2688 assert_eq!(
2689 tree.outbound_drag_source,
2690 Some(source),
2691 "the session stays until the backend reports its terminal outcome — \
2692 tearing it down here would drop the source's on_drag_ended"
2693 );
2694 }
2695
2696 #[test]
2697 fn escape_without_an_os_drag_does_not_touch_the_platform() {
2698 let mut ops = RecordingWindowOps::new(true);
2699 let cancels = ops.cancels.clone();
2700
2701 let mut tree = WidgetTree::new();
2702 tree.add(FillWidget::new());
2703 tree.layout(SizeProposal::exact(200.0, 100.0));
2704
2705 tree.dispatch_event_with_ops(
2706 crate::event::WidgetEvent::KeyDown {
2707 key: crate::event::Key::Escape,
2708 modifiers: crate::event::Modifiers::NONE,
2709 text: None,
2710 },
2711 &mut ops,
2712 );
2713 assert_eq!(cancels.get(), 0);
2714 }
2715
2716 #[test]
2717 fn no_backend_keeps_session_active_on_leave() {
2718 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2721 let mut ops = RecordingWindowOps {
2722 started: started.clone(),
2723 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2724 succeed: false,
2725 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2726 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2727 };
2728
2729 let mut tree = WidgetTree::new();
2730 let source = tree.add(FillWidget::new());
2731 tree.layout(SizeProposal::exact(200.0, 100.0));
2732
2733 let mut ctx = crate::widget::EventContext::new();
2734 ctx.start_drag(source, exportable_payload());
2735 tree.collect_from_ctx(ctx, source);
2736 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2737
2738 assert_eq!(started.borrow().len(), 1, "escalation was attempted");
2739 assert!(tree.active_drag.is_some(), "session kept (no backend)");
2740 assert!(tree.outbound_drag_source.is_none());
2741 }
2742
2743 #[test]
2744 fn non_exportable_drag_does_not_escalate() {
2745 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2746 let mut ops = RecordingWindowOps {
2747 started: started.clone(),
2748 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2749 succeed: true,
2750 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2751 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2752 };
2753
2754 let mut tree = WidgetTree::new();
2755 let source = tree.add(FillWidget::new());
2756 tree.layout(SizeProposal::exact(200.0, 100.0));
2757
2758 let mut ctx = crate::widget::EventContext::new();
2760 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u32));
2761 tree.collect_from_ctx(ctx, source);
2762 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2763
2764 assert!(started.borrow().is_empty(), "no escalation attempt");
2765 assert!(tree.active_drag.is_some(), "session unaffected");
2766 }
2767
2768 #[test]
2769 fn in_app_drop_fires_source_on_drag_ended_with_accepted() {
2770 use crate::drag_payload::DropOutcome;
2771 use std::cell::Cell;
2772 use std::rc::Rc;
2773
2774 let outcome = Rc::new(Cell::new(None));
2775 let o = outcome.clone();
2776
2777 let mut tree = WidgetTree::new();
2778 let source =
2779 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2780 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
2781 tree.layout(SizeProposal::exact(200.0, 100.0));
2782
2783 let mut ctx = crate::widget::EventContext::new();
2784 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
2785 tree.collect_from_ctx(ctx, source);
2786
2787 tree.dispatch_event(WidgetEvent::pointer_up(
2788 Point::new(150.0, 50.0),
2789 PointerButton::Primary,
2790 Modifiers::NONE,
2791 ));
2792
2793 assert_eq!(outcome.get(), Some(DropOutcome::InApp { accepted: true }));
2794 }
2795
2796 #[test]
2797 fn escape_fires_source_on_drag_ended_cancelled() {
2798 use crate::drag_payload::DropOutcome;
2799 use std::cell::Cell;
2800 use std::rc::Rc;
2801
2802 let outcome = Rc::new(Cell::new(None));
2803 let o = outcome.clone();
2804
2805 let mut tree = WidgetTree::new();
2806 let source =
2807 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2808 tree.layout(SizeProposal::exact(200.0, 100.0));
2809
2810 let mut ctx = crate::widget::EventContext::new();
2811 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2812 tree.collect_from_ctx(ctx, source);
2813
2814 tree.press_key(Key::Escape, Modifiers::NONE);
2815 assert_eq!(outcome.get(), Some(DropOutcome::Cancelled));
2816 }
2817
2818 #[test]
2823 fn os_drag_reentry_recovers_typed_payload_for_in_app_drop() {
2824 use crate::drag_payload::{DragPayload, DropOutcome};
2825 use std::cell::Cell;
2826 use std::rc::Rc;
2827
2828 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2829 let mut ops = RecordingWindowOps {
2830 started,
2831 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2832 succeed: true,
2833 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2834 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2835 };
2836
2837 let got_typed = Rc::new(Cell::new(0_u32));
2838 let ended = Rc::new(Cell::new(0_u32));
2839 let last_outcome = Rc::new(Cell::new(None));
2840 let g = got_typed.clone();
2841 let e = ended.clone();
2842 let lo = last_outcome.clone();
2843
2844 let mut tree = WidgetTree::new();
2845 let source = tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| {
2846 e.set(e.get() + 1);
2847 lo.set(Some(outcome));
2848 }));
2849 let _target =
2850 tree.add(
2851 FillWidget::new().on_drop(move |mut p, _, _| match p.take_typed::<u32>() {
2852 Some(v) => {
2853 g.set(v);
2854 true
2855 }
2856 None => false,
2857 }),
2858 );
2859 tree.layout(SizeProposal::exact(200.0, 100.0));
2860
2861 let mut ctx = crate::widget::EventContext::new();
2863 ctx.start_drag(
2864 source,
2865 DragPayload::typed(123_u32).with_mime("text/plain", b"x".to_vec()),
2866 );
2867 tree.collect_from_ctx(ctx, source);
2868
2869 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2871 assert!(tree.active_drag.is_none());
2872 assert!(
2873 super::has_outbound_typed(),
2874 "typed payload stashed globally"
2875 );
2876
2877 tree.begin_external_drag(
2880 Point::new(100.0, 50.0),
2881 crate::drag_payload::ExternalDropData::default(),
2882 &mut ops,
2883 );
2884 let d = tree.active_drag.as_ref().expect("re-entered session");
2885 assert!(!d.is_external, "re-entry is an internal session");
2886 assert!(d.payload.has_typed::<u32>(), "typed payload recovered");
2887 assert_eq!(
2888 d.payload.text(),
2889 Some("x"),
2890 "external view enriched from MIME so DropZone-style targets also accept"
2891 );
2892 assert!(!super::has_outbound_typed(), "stash taken by the re-entry");
2893
2894 tree.end_external_drag(
2896 Point::new(150.0, 50.0),
2897 crate::drag_payload::ExternalDropData::default(),
2898 &mut ops,
2899 );
2900 assert_eq!(got_typed.get(), 123, "target received the typed payload");
2901 assert_eq!(
2902 ended.get(),
2903 0,
2904 "source on_drag_ended not fired by the drop itself"
2905 );
2906
2907 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2910 assert_eq!(ended.get(), 1, "on_drag_ended fired exactly once");
2911 assert_eq!(last_outcome.get(), Some(DropOutcome::OsCopy));
2912 }
2913
2914 #[test]
2918 fn os_drag_reentry_recovers_typed_payload_across_windows() {
2919 use crate::drag_payload::{DragPayload, DropOutcome};
2920 use std::cell::Cell;
2921 use std::rc::Rc;
2922
2923 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2924 let mut ops = RecordingWindowOps {
2925 started,
2926 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2927 succeed: true,
2928 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2929 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2930 };
2931
2932 let mut tree_a = WidgetTree::new();
2934 let src = tree_a.add(FillWidget::new());
2935 tree_a.layout(SizeProposal::exact(200.0, 100.0));
2936 let mut ctx = crate::widget::EventContext::new();
2937 ctx.start_drag(
2938 src,
2939 DragPayload::typed(77_u32).with_mime("text/plain", b"x".to_vec()),
2940 );
2941 tree_a.collect_from_ctx(ctx, src);
2942 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2943 assert!(super::has_outbound_typed());
2944 assert_eq!(tree_a.outbound_drag_source, Some(src));
2945
2946 let got = Rc::new(Cell::new(0_u32));
2949 let g = got.clone();
2950 let mut tree_b = WidgetTree::new();
2951 let _t = tree_b.add(FillWidget::new().on_drop(
2952 move |mut p, _, _| match p.take_typed::<u32>() {
2953 Some(v) => {
2954 g.set(v);
2955 true
2956 }
2957 None => false,
2958 },
2959 ));
2960 tree_b.layout(SizeProposal::exact(200.0, 100.0));
2961
2962 tree_b.begin_external_drag(
2963 Point::new(50.0, 50.0),
2964 crate::drag_payload::ExternalDropData::default(),
2965 &mut ops,
2966 );
2967 assert!(
2968 tree_b
2969 .active_drag
2970 .as_ref()
2971 .is_some_and(|d| d.payload.has_typed::<u32>()),
2972 "window B recovered the typed payload"
2973 );
2974 tree_b.end_external_drag(
2975 Point::new(50.0, 50.0),
2976 crate::drag_payload::ExternalDropData::default(),
2977 &mut ops,
2978 );
2979 assert_eq!(
2980 got.get(),
2981 77,
2982 "window B's target received the typed payload"
2983 );
2984
2985 tree_a.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2987 }
2988
2989 #[test]
2993 fn os_drag_reexit_restashes_payload() {
2994 use crate::drag_payload::DragPayload;
2995 use std::rc::Rc;
2996
2997 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2998 let mut ops = RecordingWindowOps {
2999 started: started.clone(),
3000 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3001 succeed: true,
3002 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3003 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3004 };
3005
3006 let mut tree = WidgetTree::new();
3007 let source = tree.add(FillWidget::new());
3008 tree.layout(SizeProposal::exact(200.0, 100.0));
3009 let mut ctx = crate::widget::EventContext::new();
3010 ctx.start_drag(
3011 source,
3012 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3013 );
3014 tree.collect_from_ctx(ctx, source);
3015
3016 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3018 assert_eq!(started.borrow().len(), 1, "OS drag started once");
3019 tree.begin_external_drag(
3020 Point::new(100.0, 50.0),
3021 crate::drag_payload::ExternalDropData::default(),
3022 &mut ops,
3023 );
3024 assert!(tree.os_drag_reentered);
3025 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(!tree.os_drag_reentered, "re-exited");
3028 assert!(tree.active_drag.is_none(), "session torn down on re-exit");
3029 assert!(super::has_outbound_typed(), "payload re-stashed");
3030 assert_eq!(
3031 started.borrow().len(),
3032 1,
3033 "no second OS drag started on re-exit"
3034 );
3035 }
3036
3037 #[test]
3041 fn source_window_close_clears_stash_no_hijack() {
3042 use crate::drag_payload::{DragPayload, ExternalDropData};
3043 use std::path::PathBuf;
3044 use std::rc::Rc;
3045
3046 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
3047 let mut ops = RecordingWindowOps {
3048 started,
3049 started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3050 succeed: true,
3051 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
3052 accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
3053 };
3054
3055 let mut tree = WidgetTree::new();
3056 let source = tree.add(FillWidget::new());
3057 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
3058 tree.layout(SizeProposal::exact(200.0, 100.0));
3059 let mut ctx = crate::widget::EventContext::new();
3060 ctx.start_drag(
3061 source,
3062 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3063 );
3064 tree.collect_from_ctx(ctx, source);
3065 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(super::has_outbound_typed());
3067 assert_eq!(tree.outbound_drag_source, Some(source));
3068
3069 tree.abort_outbound_drag();
3071 assert!(
3072 !super::has_outbound_typed(),
3073 "stash cleared when the source window closes"
3074 );
3075 assert!(tree.outbound_drag_source.is_none());
3076
3077 tree.begin_external_drag(
3080 Point::new(50.0, 50.0),
3081 ExternalDropData {
3082 files: vec![PathBuf::from("/tmp/real")],
3083 ..Default::default()
3084 },
3085 &mut ops,
3086 );
3087 let d = tree.active_drag.as_ref().expect("external session");
3088 assert!(
3089 d.is_external,
3090 "stale stash did not hijack the new external drag"
3091 );
3092 assert!(
3093 !d.payload.has_typed::<u32>(),
3094 "no stale typed payload leaked in"
3095 );
3096 assert_eq!(d.payload.files(), &[PathBuf::from("/tmp/real")]);
3097 }
3098
3099 use std::cell::{Cell, RefCell};
3104 use std::rc::Rc;
3105
3106 fn drag_arming_leaf(
3114 slot: Rc<Cell<Option<WidgetId>>>,
3115 payload: impl Fn() -> crate::drag_payload::DragPayload + 'static,
3116 ) -> impl crate::widget::Widget {
3117 let armed = Cell::new(false);
3118 FillWidget::new().on_pointer_event(move |event, ctx| {
3119 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3120 && !armed.replace(true)
3121 && let Some(id) = slot.get()
3122 {
3123 ctx.start_drag(id, payload());
3124 }
3125 crate::event::EventResponse::Ignored
3126 })
3127 }
3128
3129 fn touch_press(
3131 tree: &mut WidgetTree,
3132 at: Point,
3133 ops: &mut dyn crate::window::WindowOps,
3134 ) -> crate::pointer::PointerId {
3135 use crate::pointer::{
3136 BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
3137 };
3138 use std::sync::atomic::{AtomicU64, Ordering};
3139 static NEXT: AtomicU64 = AtomicU64::new(91_000);
3140 let contact = PointerIdAllocator::global().begin(
3141 BackendDeviceKey::DEFAULT,
3142 NEXT.fetch_add(1, Ordering::Relaxed),
3143 );
3144 tree.dispatch_pointer_with_ops(
3145 PointerSample {
3146 pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
3147 phase: PointerPhase::Down,
3148 position: at,
3149 button: None,
3150 modifiers: Modifiers::NONE,
3151 coalesced: Vec::new(),
3152 },
3153 ops,
3154 );
3155 contact
3156 }
3157
3158 fn placement_of(
3160 tree: &WidgetTree,
3161 id: crate::overlay::OverlayId,
3162 ) -> crate::overlay::OverlayPlacement {
3163 tree.overlay_manager
3164 .stack
3165 .iter()
3166 .find(|o| o.id == id)
3167 .map(|o| o.placement.clone())
3168 .expect("the overlay is in the stack")
3169 }
3170
3171 #[test]
3179 fn a_drag_tick_reports_the_device_that_started_the_drag() {
3180 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3181 let s = seen.clone();
3182
3183 let mut tree = WidgetTree::new();
3184 let target = tree.add(
3185 FillWidget::new()
3186 .on_drop(|_, _, _| true)
3187 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3188 );
3189 tree.layout(SizeProposal::exact(200.0, 100.0));
3190
3191 let mut ctx = crate::widget::EventContext::new();
3193 ctx.start_drag(target, crate::drag_payload::DragPayload::typed(1_u8));
3194 tree.collect_from_ctx(ctx, target);
3195 tree.handle_drag_move(Point::new(50.0, 50.0), &mut crate::window::NoopWindowOps);
3196 tree.layout(SizeProposal::exact(200.0, 100.0));
3197 assert_eq!(
3198 *seen.borrow(),
3199 vec![teksilo_tokens::PointerKind::Mouse],
3200 "a mouse drag still reports the mouse"
3201 );
3202 tree.cancel_active_drag(&mut crate::window::NoopWindowOps);
3203 seen.borrow_mut().clear();
3204
3205 let mut ops = crate::window::NoopWindowOps;
3208 let s = seen.clone();
3209 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3210 let armed = Cell::new(false);
3211 let s2 = slot.clone();
3212 let mut tree = WidgetTree::new();
3213 let row = tree.add(
3214 FillWidget::new()
3215 .on_pointer_event(move |event, ctx| {
3216 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3217 && !armed.replace(true)
3218 && let Some(id) = s2.get()
3219 {
3220 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(2_u8));
3221 }
3222 crate::event::EventResponse::Ignored
3223 })
3224 .on_drop(|_, _, _| true)
3225 .on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3226 );
3227 slot.set(Some(row));
3228 tree.layout(SizeProposal::exact(200.0, 100.0));
3229 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3230 assert!(tree.active_drag.is_some(), "the finger armed a drag");
3231 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3232 tree.layout(SizeProposal::exact(200.0, 100.0));
3233 assert_eq!(
3234 *seen.borrow(),
3235 vec![teksilo_tokens::PointerKind::Touch],
3236 "and a finger drag reports the finger, from a tick that has no sample"
3237 );
3238 }
3239
3240 #[test]
3248 fn the_preview_avoids_a_coarse_contact_and_still_pins_a_cursor() {
3249 use crate::overlay::OverlayPlacement;
3250
3251 let mut ops = crate::window::NoopWindowOps;
3252 let at = Point::new(60.0, 40.0);
3253
3254 let mut tree = WidgetTree::new();
3256 let source = tree.add(FillWidget::new());
3257 tree.layout(SizeProposal::exact(200.0, 100.0));
3258 let mut ctx = crate::widget::EventContext::new();
3259 ctx.start_drag_with_preview(
3260 source,
3261 crate::drag_payload::DragPayload::typed(1_u8),
3262 Box::new(FillWidget::new()),
3263 );
3264 tree.collect_from_ctx(ctx, source);
3265 tree.handle_drag_move(at, &mut ops);
3266 let overlay = tree
3267 .active_drag
3268 .as_ref()
3269 .and_then(|d| d.preview_overlay_id)
3270 .expect("the preview overlay exists");
3271 assert!(
3272 matches!(placement_of(&tree, overlay), OverlayPlacement::AtPointer(p) if p == at),
3273 "a mouse keeps AtPointer at the reported point, unchanged",
3274 );
3275
3276 let mut tree = WidgetTree::new();
3279 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3280 let source = tree.add(drag_arming_leaf_with_preview(slot.clone()));
3281 slot.set(Some(source));
3282 tree.layout(SizeProposal::exact(200.0, 100.0));
3283 touch_press(&mut tree, at, &mut ops);
3284 assert_eq!(
3285 tree.active_drag.as_ref().map(|d| d.pointer.kind),
3286 Some(teksilo_tokens::PointerKind::Touch),
3287 "the session recorded the finger",
3288 );
3289 tree.handle_drag_move(at, &mut ops);
3290 let overlay = tree
3291 .active_drag
3292 .as_ref()
3293 .and_then(|d| d.preview_overlay_id)
3294 .expect("the preview overlay exists");
3295 match placement_of(&tree, overlay) {
3296 OverlayPlacement::AtPointerAvoiding { point, avoid } => {
3297 assert_eq!(point, at);
3298 assert!(
3299 avoid.contains(at),
3300 "the rectangle to clear is centred on the contact, so no \
3301 placement that honours it can put the preview under the hand",
3302 );
3303 }
3304 other => panic!("a coarse pointer must avoid its own contact, got {other:?}"),
3305 }
3306 }
3307
3308 fn drag_arming_leaf_with_preview(
3311 slot: Rc<Cell<Option<WidgetId>>>,
3312 ) -> impl crate::widget::Widget {
3313 let armed = Cell::new(false);
3314 FillWidget::new().on_pointer_event(move |event, ctx| {
3315 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3316 && !armed.replace(true)
3317 && let Some(id) = slot.get()
3318 {
3319 ctx.start_drag_with_preview(
3320 id,
3321 crate::drag_payload::DragPayload::typed(1_u8),
3322 Box::new(FillWidget::new()),
3323 );
3324 }
3325 crate::event::EventResponse::Ignored
3326 })
3327 }
3328
3329 #[test]
3338 fn an_os_drag_started_by_a_finger_cancels_that_finger() {
3339 let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
3340 Rc::new(RefCell::new(Vec::new()));
3341 let c = cancelled.clone();
3342
3343 let mut ops = RecordingWindowOps::new(true);
3344 let kinds = ops.started_kinds.clone();
3345
3346 let mut tree = WidgetTree::new();
3347 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3348 let armed = Cell::new(false);
3349 let s2 = slot.clone();
3350 let source = tree.add(
3351 FillWidget::new()
3352 .on_pointer_event(move |event, ctx| {
3353 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3354 && !armed.replace(true)
3355 && let Some(id) = s2.get()
3356 {
3357 ctx.start_drag(id, exportable_payload());
3358 }
3359 crate::event::EventResponse::Ignored
3360 })
3361 .on_pointer_cancel(move |pointer, reason, _ctx| {
3362 c.borrow_mut().push((pointer.id, reason));
3363 }),
3364 );
3365 slot.set(Some(source));
3366 tree.layout(SizeProposal::exact(200.0, 100.0));
3367
3368 let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
3369 assert!(tree.active_drag.is_some());
3370
3371 tree.handle_drag_move(Point::new(-40.0, 50.0), &mut ops);
3373 assert_eq!(tree.outbound_drag_source, Some(source));
3374 assert_eq!(
3375 *kinds.borrow(),
3376 vec![teksilo_tokens::PointerKind::Touch],
3377 "the platform is told which device is dragging — Wayland needs the \
3378 touch-down serial, not a button serial"
3379 );
3380 assert_eq!(
3381 *cancelled.borrow(),
3382 vec![(contact, crate::pointer::CancelReason::OsDragStarted)],
3383 "the finger is the pointer revoked"
3384 );
3385 tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3386 }
3387
3388 #[test]
3395 fn an_escape_cancelled_finger_drag_reports_the_finger_to_its_source() {
3396 let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
3397 let s = seen.clone();
3398
3399 let mut ops = crate::window::NoopWindowOps;
3400 let mut tree = WidgetTree::new();
3401 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3402 let armed = Cell::new(false);
3403 let s2 = slot.clone();
3404 let source = tree.add(
3405 FillWidget::new()
3406 .on_pointer_event(move |event, ctx| {
3407 if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
3408 && !armed.replace(true)
3409 && let Some(id) = s2.get()
3410 {
3411 ctx.start_drag(id, crate::drag_payload::DragPayload::typed(1_u8));
3412 }
3413 crate::event::EventResponse::Ignored
3414 })
3415 .on_drag_ended(move |_outcome, ctx| s.borrow_mut().push(ctx.pointer_kind())),
3416 );
3417 slot.set(Some(source));
3418 tree.layout(SizeProposal::exact(200.0, 100.0));
3419
3420 touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
3421 assert!(tree.active_drag.is_some());
3422 tree.dispatch_event_with_ops(
3423 crate::event::WidgetEvent::KeyDown {
3424 key: crate::event::Key::Escape,
3425 modifiers: crate::event::Modifiers::NONE,
3426 text: None,
3427 },
3428 &mut ops,
3429 );
3430 assert_eq!(*seen.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
3431 }
3432
3433 #[test]
3439 fn the_accept_setter_receives_the_widget_verdict() {
3440 use crate::drag_state::DropFeedback;
3441
3442 let verdict = Rc::new(Cell::new(false));
3443 let v = verdict.clone();
3444 let mut ops = RecordingWindowOps::new(true);
3445 let accepts = ops.accepts.clone();
3446
3447 let mut tree = WidgetTree::new();
3448 tree.add(
3449 FillWidget::new()
3450 .on_drag_hover(move |_p, _pos, _ctx| {
3451 if v.get() {
3452 DropFeedback::Accept
3453 } else {
3454 DropFeedback::NoFeedback
3455 }
3456 })
3457 .on_drop(|_, _, _| true),
3458 );
3459 tree.layout(SizeProposal::exact(200.0, 100.0));
3460
3461 tree.begin_external_drag(
3462 Point::new(50.0, 50.0),
3463 crate::drag_payload::ExternalDropData {
3464 files: vec![std::path::PathBuf::from("/tmp/a.png")],
3465 ..Default::default()
3466 },
3467 &mut ops,
3468 );
3469 assert_eq!(
3470 *accepts.borrow(),
3471 vec![false],
3472 "a refusing target is reported to the OS as a refusal"
3473 );
3474
3475 tree.update_external_drag(Point::new(52.0, 50.0), &mut ops);
3478 assert_eq!(
3479 *accepts.borrow(),
3480 vec![false],
3481 "an unchanged answer is not re-sent"
3482 );
3483
3484 verdict.set(true);
3486 tree.update_external_drag(Point::new(54.0, 50.0), &mut ops);
3487 assert_eq!(
3488 *accepts.borrow(),
3489 vec![false, true],
3490 "and a change is pushed once"
3491 );
3492 }
3493
3494 #[test]
3497 fn an_in_app_drag_pushes_no_os_accept_state() {
3498 let mut ops = RecordingWindowOps::new(true);
3499 let accepts = ops.accepts.clone();
3500
3501 let mut tree = WidgetTree::new();
3502 let source = tree.add(FillWidget::new());
3503 tree.add(FillWidget::new().on_drop(|_, _, _| true));
3504 tree.layout(SizeProposal::exact(200.0, 100.0));
3505
3506 let mut ctx = crate::widget::EventContext::new();
3507 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
3508 tree.collect_from_ctx(ctx, source);
3509 tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
3510
3511 assert!(accepts.borrow().is_empty());
3512 }
3513
3514 #[test]
3523 fn a_cancelled_os_drag_tears_the_source_down_exactly_once() {
3524 use crate::drag_payload::{DragPayload, DropOutcome};
3525
3526 let ended = Rc::new(RefCell::new(Vec::new()));
3527 let e = ended.clone();
3528 let mut ops = RecordingWindowOps::new(true);
3529
3530 let mut tree_a = WidgetTree::new();
3532 let src = tree_a.add(
3533 FillWidget::new().on_drag_ended(move |outcome, _ctx| e.borrow_mut().push(outcome)),
3534 );
3535 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3536 let mut ctx = crate::widget::EventContext::new();
3537 ctx.start_drag(
3538 src,
3539 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
3540 );
3541 tree_a.collect_from_ctx(ctx, src);
3542 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3543 assert!(super::outbound_is_live());
3544
3545 let left = Rc::new(Cell::new(0_u32));
3547 let l = left.clone();
3548 let mut tree_b = WidgetTree::new();
3549 tree_b.add(
3550 FillWidget::new()
3551 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3552 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3553 .on_drop(|_, _, _| true),
3554 );
3555 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3556 let accepts = ops.accepts.clone();
3557 accepts.borrow_mut().clear();
3558 tree_b.begin_external_drag(
3559 Point::new(50.0, 50.0),
3560 crate::drag_payload::ExternalDropData::default(),
3561 &mut ops,
3562 );
3563 assert!(tree_b.os_drag_reentered);
3564 assert_eq!(
3565 *accepts.borrow(),
3566 vec![true],
3567 "a re-entered app drag still negotiates with the OS: its offer is \
3568 live and a refusal must still reach the compositor's cursor",
3569 );
3570
3571 tree_b.abort_external_drag(&mut ops);
3574 assert!(tree_b.active_drag.is_none(), "B's session is gone");
3575 assert!(!tree_b.os_drag_reentered);
3576 assert_eq!(left.get(), 1, "B's highlighted target was cleared");
3577 assert!(
3578 ended.borrow().is_empty(),
3579 "the abort must not fire the source's on_drag_ended — the source \
3580 window owns that"
3581 );
3582
3583 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3585 assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
3586
3587 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3589 assert_eq!(
3590 *ended.borrow(),
3591 vec![DropOutcome::Cancelled],
3592 "exactly once"
3593 );
3594 }
3595
3596 #[test]
3606 fn a_reentered_session_is_reaped_when_the_os_drag_ends_elsewhere() {
3607 use crate::drag_payload::{DragPayload, DropOutcome};
3608
3609 let mut ops = RecordingWindowOps::new(true);
3610
3611 let mut tree_a = WidgetTree::new();
3612 let src = tree_a.add(FillWidget::new());
3613 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3614 let mut ctx = crate::widget::EventContext::new();
3615 ctx.start_drag(
3616 src,
3617 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
3618 );
3619 tree_a.collect_from_ctx(ctx, src);
3620 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3621
3622 let left = Rc::new(Cell::new(0_u32));
3623 let l = left.clone();
3624 let mut tree_b = WidgetTree::new();
3625 tree_b.add(
3626 FillWidget::new()
3627 .on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
3628 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
3629 .on_drop(|_, _, _| true),
3630 );
3631 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3632 tree_b.begin_external_drag(
3633 Point::new(50.0, 50.0),
3634 crate::drag_payload::ExternalDropData::default(),
3635 &mut ops,
3636 );
3637 assert!(tree_b.active_drag.is_some() && tree_b.os_drag_reentered);
3638
3639 tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
3641 assert!(
3642 tree_b.active_drag.is_some(),
3643 "B has not been told anything yet"
3644 );
3645
3646 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3648 assert!(tree_b.active_drag.is_none(), "the dead session was reaped");
3649 assert!(!tree_b.os_drag_reentered);
3650 assert_eq!(left.get(), 1, "and its highlighted target was cleared");
3651 }
3652
3653 #[test]
3657 fn a_reentered_app_drag_recovers_the_device_that_started_it() {
3658 let mut ops = RecordingWindowOps::new(true);
3659
3660 let mut tree_a = WidgetTree::new();
3661 let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
3662 let src = tree_a.add(drag_arming_leaf(slot.clone(), exportable_payload));
3663 slot.set(Some(src));
3664 tree_a.layout(SizeProposal::exact(200.0, 100.0));
3665 touch_press(&mut tree_a, Point::new(20.0, 50.0), &mut ops);
3666 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
3667 assert!(super::outbound_is_live());
3668
3669 let mut tree_b = WidgetTree::new();
3670 tree_b.add(FillWidget::new().on_drop(|_, _, _| true));
3671 tree_b.layout(SizeProposal::exact(200.0, 100.0));
3672 tree_b.begin_external_drag(
3673 Point::new(50.0, 50.0),
3674 crate::drag_payload::ExternalDropData::default(),
3675 &mut ops,
3676 );
3677 assert_eq!(
3678 tree_b.active_drag.as_ref().map(|d| d.pointer.kind),
3679 Some(teksilo_tokens::PointerKind::Touch),
3680 );
3681
3682 let mut tree_c = WidgetTree::new();
3684 tree_c.add(FillWidget::new().on_drop(|_, _, _| true));
3685 tree_c.layout(SizeProposal::exact(200.0, 100.0));
3686 tree_a.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
3687 tree_c.begin_external_drag(
3688 Point::new(50.0, 50.0),
3689 crate::drag_payload::ExternalDropData::default(),
3690 &mut ops,
3691 );
3692 assert_eq!(
3693 tree_c.active_drag.as_ref().map(|d| d.pointer.kind),
3694 Some(teksilo_tokens::PointerKind::Unknown),
3695 "no OS names the source's device to a destination",
3696 );
3697 }
3698
3699 #[test]
3703 fn restash_after_drag_ended_is_noop() {
3704 use crate::drag_payload::DragPayload;
3705
3706 super::outbound_begin(
3707 DragPayload::typed(1_u32).with_mime("text/plain", b"x".to_vec()),
3708 crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO),
3709 );
3710 assert!(super::has_outbound_typed());
3711 let held = super::outbound_take_if_live().expect("payload taken while live");
3713 assert!(!super::has_outbound_typed());
3714 super::outbound_end();
3716 super::outbound_restash(held);
3718 assert!(
3719 !super::has_outbound_typed(),
3720 "ended drag is not resurrected by a racing re-stash"
3721 );
3722 assert!(super::outbound_take_if_live().is_none());
3724 }
3725}