1use super::*;
5
6struct OutboundStash {
27 live: bool,
29 payload: Option<crate::drag_payload::DragPayload>,
32}
33
34thread_local! {
35 static OUTBOUND: std::cell::RefCell<OutboundStash> =
36 const { std::cell::RefCell::new(OutboundStash { live: false, payload: None }) };
37}
38
39fn outbound_begin(payload: crate::drag_payload::DragPayload) {
41 OUTBOUND.with(|s| {
42 let mut s = s.borrow_mut();
43 s.live = true;
44 s.payload = Some(payload);
45 });
46}
47
48fn outbound_take_if_live() -> Option<crate::drag_payload::DragPayload> {
51 OUTBOUND.with(|s| {
52 let mut s = s.borrow_mut();
53 if s.live { s.payload.take() } else { None }
54 })
55}
56
57fn outbound_restash(payload: crate::drag_payload::DragPayload) {
61 OUTBOUND.with(|s| {
62 let mut s = s.borrow_mut();
63 if s.live {
64 s.payload = Some(payload);
65 }
66 });
67}
68
69#[cfg(test)]
71fn has_outbound_typed() -> bool {
72 OUTBOUND.with(|s| s.borrow().payload.is_some())
73}
74
75fn outbound_end() {
78 OUTBOUND.with(|s| {
79 let mut s = s.borrow_mut();
80 s.live = false;
81 s.payload = None;
82 });
83}
84
85impl WidgetTree {
86 pub(super) fn cleanup_drag_preview(&mut self) {
88 if let Some(ref drag) = self.active_drag {
89 if let Some(overlay_id) = drag.preview_overlay_id {
90 self.overlay_manager.dismiss(overlay_id);
91 }
92 if let Some(content_id) = drag.preview_content_id {
93 self.arena.destroy(content_id);
94 }
95 }
96 }
97
98 pub(super) fn cancel_active_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
103 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
104 let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
108 self.cleanup_drag_preview();
109 self.active_drag = None;
110 self.pointer_captured_by = None;
111 self.current_cursor = crate::widget::CursorIcon::Default;
112 if let Some(prev) = prev_target {
113 self.fire_on_drag_leave(prev, &mut *ops);
114 }
115 if let Some(src) = source {
116 self.fire_on_drag_ended(src, crate::drag_payload::DropOutcome::Cancelled, &mut *ops);
117 }
118 }
119
120 pub fn begin_external_drag(
139 &mut self,
140 position: teksilo_canvas::Point,
141 data: crate::drag_payload::ExternalDropData,
142 ops: &mut dyn crate::window::WindowOps,
143 ) {
144 if self.active_drag.is_some() {
148 self.cancel_active_drag(&mut *ops);
149 }
150
151 if let Some(mut payload) = outbound_take_if_live() {
161 payload.enrich_external_from_mime();
165 self.active_drag = Some(crate::drag_state::DragSession {
166 payload,
167 source_widget: None,
168 is_external: false,
169 current_position: position,
170 current_target: None,
171 feedback: crate::drag_state::DropFeedback::NoFeedback,
172 preview_content_id: None,
173 preview_overlay_id: None,
174 });
175 self.os_drag_reentered = true;
176 self.handle_drag_move(position, &mut *ops);
177 return;
178 }
179
180 self.active_drag = Some(crate::drag_state::DragSession {
181 payload: crate::drag_payload::DragPayload::external(data),
182 source_widget: None,
183 is_external: true,
184 current_position: position,
185 current_target: None,
186 feedback: crate::drag_state::DropFeedback::NoFeedback,
187 preview_content_id: None,
188 preview_overlay_id: None,
189 });
190 self.handle_drag_move(position, &mut *ops);
193 }
194
195 pub fn update_external_drag(
198 &mut self,
199 position: teksilo_canvas::Point,
200 ops: &mut dyn crate::window::WindowOps,
201 ) {
202 if self.active_drag.as_ref().is_some_and(|d| d.is_external) || self.os_drag_reentered {
206 self.handle_drag_move(position, &mut *ops);
207 }
208 }
209
210 pub fn end_external_drag(
216 &mut self,
217 position: teksilo_canvas::Point,
218 data: crate::drag_payload::ExternalDropData,
219 ops: &mut dyn crate::window::WindowOps,
220 ) {
221 if self.os_drag_reentered {
228 self.os_drag_reentered = false;
229 self.handle_drag_drop(position, &mut *ops);
230 outbound_end();
231 return;
232 }
233 if !self.active_drag.as_ref().is_some_and(|d| d.is_external) {
234 return;
235 }
236 if !data.is_empty()
237 && let Some(drag) = self.active_drag.as_mut()
238 {
239 drag.payload = crate::drag_payload::DragPayload::external(data);
240 }
241 self.handle_drag_drop(position, &mut *ops);
242 }
243
244 pub fn cancel_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
248 if self.os_drag_reentered {
253 self.reexit_outbound(&mut *ops);
254 } else if self.active_drag.as_ref().is_some_and(|d| d.is_external) {
255 self.cancel_active_drag(&mut *ops);
256 }
257 }
258
259 pub(super) fn process_drag_tick(&mut self, ops: &mut dyn crate::window::WindowOps) {
264 let Some((target_id, position)) = self
265 .active_drag
266 .as_ref()
267 .and_then(|d| d.current_target.map(|t| (t, d.current_position)))
268 else {
269 return;
270 };
271 if !self.arena.is_active(target_id) {
272 return;
273 }
274 let bounds = self.arena.bounds(target_id);
275 let local = teksilo_canvas::Point::new(position.x - bounds.x, position.y - bounds.y);
276 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
277 Some(node) => (
278 node.external_handlers.on_drag_tick.take(),
279 node.handlers.on_drag_tick.take(),
280 ),
281 None => return,
282 };
283 if ext_handler.is_none() && own_handler.is_none() {
284 return;
285 }
286 let mut ctx = self.make_event_context(&mut *ops);
287 if let Some(h) = ext_handler.as_mut() {
288 h(local, &mut ctx);
289 }
290 if let Some(h) = own_handler.as_mut() {
291 h(local, &mut ctx);
292 }
293 if let Some(node) = self.arena.get_mut(target_id) {
294 node.external_handlers.on_drag_tick = ext_handler;
295 node.handlers.on_drag_tick = own_handler;
296 }
297 self.collect_from_ctx(ctx, target_id);
298 if self.active_drag.is_some() {
302 self.handle_drag_move(position, &mut *ops);
303 }
304 }
305
306 pub(super) fn fire_on_drag_leave(
312 &mut self,
313 target_id: WidgetId,
314 ops: &mut dyn crate::window::WindowOps,
315 ) {
316 if !self.arena.is_active(target_id) {
317 return;
318 }
319 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
320 Some(node) => (
321 node.external_handlers.on_drag_leave.take(),
322 node.handlers.on_drag_leave.take(),
323 ),
324 None => return,
325 };
326 if ext_handler.is_none() && own_handler.is_none() {
327 self.arena.mark_needs_paint(target_id);
330 return;
331 }
332 let mut ctx = self.make_event_context(&mut *ops);
333 if let Some(h) = ext_handler.as_mut() {
334 h(&mut ctx);
335 }
336 if let Some(h) = own_handler.as_mut() {
337 h(&mut ctx);
338 }
339 if let Some(node) = self.arena.get_mut(target_id) {
340 node.external_handlers.on_drag_leave = ext_handler;
341 node.handlers.on_drag_leave = own_handler;
342 }
343 self.collect_from_ctx(ctx, target_id);
344 self.arena.mark_needs_paint(target_id);
345 }
346
347 pub(super) fn fire_on_drag_ended(
351 &mut self,
352 source_id: WidgetId,
353 outcome: crate::drag_payload::DropOutcome,
354 ops: &mut dyn crate::window::WindowOps,
355 ) {
356 if !self.arena.is_active(source_id) {
357 return;
358 }
359 let (mut ext_handler, mut own_handler) = match self.arena.get_mut(source_id) {
364 Some(node) => (
365 node.external_handlers.on_drag_ended.take(),
366 node.handlers.on_drag_ended.take(),
367 ),
368 None => return,
369 };
370 if ext_handler.is_none() && own_handler.is_none() {
371 return;
372 }
373 let mut ctx = self.make_event_context(&mut *ops);
374 if let Some(h) = ext_handler.as_mut() {
375 h(outcome, &mut ctx);
376 }
377 if let Some(h) = own_handler.as_mut() {
378 h(outcome, &mut ctx);
379 }
380 if let Some(node) = self.arena.get_mut(source_id) {
381 node.external_handlers.on_drag_ended = ext_handler;
382 node.handlers.on_drag_ended = own_handler;
383 }
384 self.collect_from_ctx(ctx, source_id);
385 }
386
387 fn window_content_size(&self) -> Option<(f32, f32)> {
390 Some((self.last_proposal.width?, self.last_proposal.height?))
391 }
392
393 fn is_outside_window(&self, position: teksilo_canvas::Point) -> bool {
396 match self.window_content_size() {
397 Some((w, h)) => {
398 position.x < 0.0 || position.y < 0.0 || position.x > w || position.y > h
399 }
400 None => false,
401 }
402 }
403
404 fn try_escalate_to_os_drag(
410 &mut self,
411 position: teksilo_canvas::Point,
412 ops: &mut dyn crate::window::WindowOps,
413 ) -> bool {
414 if self.os_drag_reentered {
419 if self.is_outside_window(position) {
420 self.reexit_outbound(&mut *ops);
421 return true;
422 }
423 return false;
424 }
425
426 let data = match self.active_drag.as_ref() {
428 Some(d)
429 if !d.is_external && d.source_widget.is_some() && d.payload.is_os_exportable() =>
430 {
431 d.payload.to_outbound()
432 }
433 _ => return false,
434 };
435 if !self.is_outside_window(position) {
436 return false;
437 }
438
439 if !ops.begin_os_drag(data, None) {
443 return false;
444 }
445
446 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
451 self.cleanup_drag_preview();
452 let drag = self
453 .active_drag
454 .take()
455 .expect("active_drag present (matched above)");
456 self.pointer_captured_by = None;
457 self.current_cursor = crate::widget::CursorIcon::Default;
458 self.outbound_drag_source = drag.source_widget;
459 outbound_begin(drag.payload);
460 if let Some(prev) = prev_target {
461 self.fire_on_drag_leave(prev, &mut *ops);
462 }
463 true
464 }
465
466 fn reexit_outbound(&mut self, ops: &mut dyn crate::window::WindowOps) {
470 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
471 self.cleanup_drag_preview();
472 if let Some(drag) = self.active_drag.take() {
473 outbound_restash(drag.payload);
474 }
475 self.os_drag_reentered = false;
476 self.pointer_captured_by = None;
477 self.current_cursor = crate::widget::CursorIcon::Default;
478 if let Some(prev) = prev_target {
479 self.fire_on_drag_leave(prev, &mut *ops);
480 }
481 }
482
483 pub fn handle_os_drag_ended(
488 &mut self,
489 outcome: crate::drag_payload::DropOutcome,
490 ops: &mut dyn crate::window::WindowOps,
491 ) {
492 outbound_end();
495 self.os_drag_reentered = false;
496 if let Some(source) = self.outbound_drag_source.take() {
497 self.fire_on_drag_ended(source, outcome, &mut *ops);
498 }
499 }
500
501 pub fn abort_outbound_drag(&mut self) {
509 if self.outbound_drag_source.take().is_some() {
510 outbound_end();
511 } else if self.os_drag_reentered
512 && let Some(drag) = self.active_drag.take()
513 {
514 outbound_restash(drag.payload);
515 }
516 self.os_drag_reentered = false;
517 }
518
519 pub(super) fn handle_drag_move(
522 &mut self,
523 position: teksilo_canvas::Point,
524 ops: &mut dyn crate::window::WindowOps,
525 ) {
526 if let Some(ref mut drag) = self.active_drag {
528 drag.current_position = position;
529 }
530
531 if self.try_escalate_to_os_drag(position, &mut *ops) {
534 return;
535 }
536
537 let preview_content = self
544 .active_drag
545 .as_ref()
546 .and_then(|d| Some((d.preview_overlay_id?, d.preview_content_id?)));
547 if let Some((overlay_id, content_id)) = preview_content {
548 self.overlay_manager.update_placement(
549 overlay_id,
550 crate::overlay::OverlayPlacement::AtPointer(position),
551 );
552 self.arena.mark_needs_layout(content_id);
553 }
554
555 let exclude_overlay = self.active_drag.as_ref().and_then(|d| d.preview_overlay_id);
559 let exclude_widget = self.active_drag.as_ref().and_then(|d| d.preview_content_id);
560 let target =
561 self.hit_test_excluding_overlay_and_widget(position, exclude_overlay, exclude_widget);
562
563 let mut candidate = target.and_then(|t| self.find_drop_target_at_or_above(t));
571 let mut engaged: Option<WidgetId> = None;
572 let mut engaged_feedback = crate::drag_state::DropFeedback::NoFeedback;
573 let mut bubbled_past: Vec<WidgetId> = Vec::new();
574 while let Some(cand) = candidate {
575 let fb = self.fire_on_drag_hover(cand, position, &mut *ops);
576 if fb.is_engaged() {
577 engaged = Some(cand);
578 engaged_feedback = fb;
579 break;
580 }
581 bubbled_past.push(cand);
582 candidate = self.next_drop_target_above(cand);
583 }
584
585 let (new_target, new_feedback) = if engaged.is_some() {
593 for cand in &bubbled_past {
594 self.fire_on_drag_leave(*cand, &mut *ops);
595 }
596 (engaged, engaged_feedback)
597 } else if let Some((&deepest, rest)) = bubbled_past.split_first() {
598 for cand in rest {
599 self.fire_on_drag_leave(*cand, &mut *ops);
600 }
601 (Some(deepest), crate::drag_state::DropFeedback::NoFeedback)
602 } else {
603 (None, crate::drag_state::DropFeedback::NoFeedback)
604 };
605
606 let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
612 if prev_target != new_target
613 && let Some(prev) = prev_target
614 && !bubbled_past.contains(&prev)
615 {
616 self.fire_on_drag_leave(prev, &mut *ops);
617 }
618 if let Some(ref mut drag) = self.active_drag {
619 drag.current_target = new_target;
620 drag.feedback = new_feedback;
621 }
622 }
623
624 fn fire_on_drag_hover(
629 &mut self,
630 target_id: WidgetId,
631 position: teksilo_canvas::Point,
632 ops: &mut dyn crate::window::WindowOps,
633 ) -> crate::drag_state::DropFeedback {
634 use crate::drag_state::DropFeedback;
635 let target_bounds = self.arena.bounds(target_id);
636 let local =
637 teksilo_canvas::Point::new(position.x - target_bounds.x, position.y - target_bounds.y);
638 let (mut ext_handler, mut own_handler, has_on_drop) = match self.arena.get_mut(target_id) {
639 Some(node) => {
640 let has_on_drop = node.any_handler(|h| h.on_drop.is_some());
641 let ext = node.external_handlers.on_drag_hover.take();
642 let own = node.handlers.on_drag_hover.take();
643 (ext, own, has_on_drop)
644 }
645 None => return DropFeedback::NoFeedback,
646 };
647 if ext_handler.is_none() && own_handler.is_none() {
649 return if has_on_drop {
650 DropFeedback::Accept
651 } else {
652 DropFeedback::NoFeedback
653 };
654 }
655 let mut feedback = DropFeedback::NoFeedback;
656 if self.active_drag.is_some() {
657 let mut ctx = self.make_event_context(&mut *ops);
658 if let Some(ref drag) = self.active_drag {
659 if let Some(h) = ext_handler.as_mut() {
660 feedback = h(&drag.payload, local, &mut ctx);
661 }
662 if let Some(h) = own_handler.as_mut() {
663 feedback = h(&drag.payload, local, &mut ctx);
664 }
665 }
666 if let Some(node) = self.arena.get_mut(target_id) {
667 node.external_handlers.on_drag_hover = ext_handler;
668 node.handlers.on_drag_hover = own_handler;
669 }
670 self.collect_from_ctx(ctx, target_id);
671 self.arena.mark_needs_paint(target_id);
672 } else if let Some(node) = self.arena.get_mut(target_id) {
673 node.external_handlers.on_drag_hover = ext_handler;
674 node.handlers.on_drag_hover = own_handler;
675 }
676 feedback
677 }
678
679 fn next_drop_target_above(&self, id: WidgetId) -> Option<WidgetId> {
682 let parent = self.arena.parent(id)?;
683 self.find_drop_target_at_or_above(parent)
684 }
685
686 pub(super) fn handle_drag_drop(
688 &mut self,
689 position: teksilo_canvas::Point,
690 ops: &mut dyn crate::window::WindowOps,
691 ) {
692 self.cleanup_drag_preview();
694
695 if self.active_drag.is_none() {
696 return;
697 }
698
699 let mut drop_target = self
710 .active_drag
711 .as_ref()
712 .and_then(|d| d.current_target)
713 .filter(|&t| self.arena.is_active(t));
714 if drop_target.is_none() {
715 let hit = self.hit_test(position);
716 let mut candidate = hit.and_then(|t| self.find_drop_target_at_or_above(t));
717 while let Some(cand) = candidate {
718 if self
719 .fire_on_drag_hover(cand, position, &mut *ops)
720 .is_engaged()
721 {
722 drop_target = Some(cand);
723 break;
724 }
725 self.fire_on_drag_leave(cand, &mut *ops);
728 candidate = self.next_drop_target_above(cand);
729 }
730 }
731
732 let drag = match self.active_drag.take() {
734 Some(d) => d,
735 None => return,
736 };
737 self.pointer_captured_by = None;
738 self.current_cursor = crate::widget::CursorIcon::Default;
739 let source = drag.source_widget;
742 let mut outcome = crate::drag_payload::DropOutcome::Cancelled;
745
746 if let Some(prev) = drop_target {
750 self.fire_on_drag_leave(prev, &mut *ops);
751 }
752
753 if let Some(target_id) = drop_target {
758 let target_bounds = self.arena.bounds(target_id);
759 let local = teksilo_canvas::Point::new(
760 position.x - target_bounds.x,
761 position.y - target_bounds.y,
762 );
763 let (taken_own, taken_ext) = match self.arena.get_mut(target_id) {
764 Some(node) => {
765 let own = node.handlers.on_drop.take();
766 let ext = if own.is_none() {
767 node.external_handlers.on_drop.take()
768 } else {
769 None
770 };
771 (own, ext)
772 }
773 None => (None, None),
774 };
775 let picked = if let Some(h) = taken_own {
776 Some((h, true))
777 } else {
778 taken_ext.map(|h| (h, false))
779 };
780 if let Some((mut handler, is_own)) = picked {
781 let mut ctx = self.make_event_context(&mut *ops);
782 let accepted = handler(drag.payload, local, &mut ctx);
783 outcome = crate::drag_payload::DropOutcome::InApp { accepted };
784 if let Some(node) = self.arena.get_mut(target_id) {
785 if is_own {
786 node.handlers.on_drop = Some(handler);
787 } else {
788 node.external_handlers.on_drop = Some(handler);
789 }
790 }
791 self.collect_from_ctx(ctx, target_id);
792 self.arena.mark_needs_paint(target_id);
793 }
794 }
795 if let Some(src) = source {
799 self.fire_on_drag_ended(src, outcome, &mut *ops);
800 }
801 }
802
803 fn find_drop_target_at_or_above(&self, start: WidgetId) -> Option<WidgetId> {
806 let mut current = Some(start);
807 while let Some(id) = current {
808 if let Some(node) = self.arena.get(id)
809 && node.any_handler(|h| h.on_drop.is_some() || h.on_drag_hover.is_some())
810 {
811 return Some(id);
812 }
813 current = self.arena.parent(id);
814 }
815 None
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use crate::test_widgets::{FillWidget, StackWidget};
823 use crate::widget::CursorIcon;
824 use crate::widget_builder::WidgetBuilder;
825
826 #[test]
827 fn start_drag_creates_session() {
828 let mut tree = WidgetTree::new();
829 let source = tree.add(FillWidget::new().on_tap({
830 move |_pos, ctx: &mut crate::widget::EventContext| {
831 ctx.start_drag(
832 ctx.focus_requests.first().copied().unwrap_or_default(),
833 crate::drag_payload::DragPayload::typed(42_u32),
834 );
835 }
836 }));
837 tree.layout(SizeProposal::exact(100.0, 50.0));
838
839 let mut ctx = crate::widget::EventContext::new();
841 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
842 tree.collect_from_ctx(ctx, source);
843
844 assert!(tree.active_drag.is_some());
845 let drag = tree.active_drag.as_ref().unwrap();
846 assert_eq!(drag.source_widget, Some(source));
847 assert!(!drag.is_external);
848 assert!(drag.payload.has_typed::<u32>());
849 }
850
851 #[test]
852 fn drag_move_updates_position() {
853 let mut tree = WidgetTree::new();
854 let source = tree.add(FillWidget::new());
855 tree.layout(SizeProposal::exact(200.0, 100.0));
856
857 let mut ctx = crate::widget::EventContext::new();
859 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("hello"));
860 tree.collect_from_ctx(ctx, source);
861 assert!(tree.active_drag.is_some());
862
863 tree.dispatch_event(WidgetEvent::PointerMove {
865 position: Point::new(50.0, 30.0),
866 });
867
868 let drag = tree.active_drag.as_ref().unwrap();
869 assert!((drag.current_position.x - 50.0).abs() < 0.01);
870 assert!((drag.current_position.y - 30.0).abs() < 0.01);
871 }
872
873 #[test]
874 fn escape_cancels_drag() {
875 let mut tree = WidgetTree::new();
876 let source = tree.add(FillWidget::new());
877 tree.layout(SizeProposal::exact(200.0, 100.0));
878
879 let mut ctx = crate::widget::EventContext::new();
880 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(99_i32));
881 tree.collect_from_ctx(ctx, source);
882 assert!(tree.active_drag.is_some());
883
884 tree.press_key(Key::Escape, Modifiers::NONE);
885 assert!(tree.active_drag.is_none(), "drag should be cancelled");
886 }
887
888 #[test]
889 fn drop_on_target_fires_handler() {
890 use std::cell::Cell;
891 use std::rc::Rc;
892
893 let dropped = Rc::new(Cell::new(false));
894 let dropped_value = Rc::new(Cell::new(0_u32));
895 let d = dropped.clone();
896 let dv = dropped_value.clone();
897
898 let mut tree = WidgetTree::new();
899 let source = tree.add(FillWidget::new());
900 let _target = tree.add(FillWidget::new().on_drop(move |mut payload, _pos, _ctx| {
902 d.set(true);
903 if let Some(val) = payload.take_typed::<u32>() {
904 dv.set(val);
905 }
906 true
907 }));
908 tree.layout(SizeProposal::exact(200.0, 100.0));
909
910 let mut ctx = crate::widget::EventContext::new();
912 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
913 tree.collect_from_ctx(ctx, source);
914
915 tree.dispatch_event(WidgetEvent::PointerUp {
917 position: Point::new(150.0, 50.0),
918 button: PointerButton::Primary,
919 modifiers: Modifiers::NONE,
920 });
921
922 assert!(tree.active_drag.is_none(), "drag session should be cleared");
923 assert!(dropped.get(), "on_drop should have been called");
924 assert_eq!(dropped_value.get(), 42);
925 }
926
927 #[test]
928 fn drop_on_no_target_cancels() {
929 let mut tree = WidgetTree::new();
930 let source = tree.add(FillWidget::new());
931 tree.layout(SizeProposal::exact(100.0, 50.0));
932
933 let mut ctx = crate::widget::EventContext::new();
934 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
935 tree.collect_from_ctx(ctx, source);
936
937 tree.dispatch_event(WidgetEvent::PointerUp {
939 position: Point::new(999.0, 999.0),
940 button: PointerButton::Primary,
941 modifiers: Modifiers::NONE,
942 });
943
944 assert!(tree.active_drag.is_none(), "drag session should be cleared");
945 }
946
947 #[test]
948 fn drop_falls_through_a_destroyed_current_target() {
949 use std::cell::Cell;
954 use std::rc::Rc;
955
956 let bg_dropped = Rc::new(Cell::new(false));
957 let bg_sink = bg_dropped.clone();
958
959 let mut tree = WidgetTree::new();
960 let source = tree.add(FillWidget::new());
964 let _bg = tree.add(FillWidget::new().on_drop(move |_p, _pos, _ctx| {
965 bg_sink.set(true);
966 true
967 }));
968 let fg = tree.add(
971 FillWidget::new()
972 .on_drag_hover(|_payload, _pos, _ctx| {
973 crate::drag_state::DropFeedback::InsertionLine {
974 y: 50.0,
975 width: 200.0,
976 }
977 })
978 .on_drop(|_, _, _| true),
979 );
980 tree.layout(SizeProposal::exact(200.0, 100.0));
981
982 let mut ctx = crate::widget::EventContext::new();
983 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
984 tree.collect_from_ctx(ctx, source);
985
986 tree.dispatch_event(WidgetEvent::PointerMove {
988 position: Point::new(100.0, 50.0),
989 });
990 assert_eq!(
991 tree.active_drag.as_ref().unwrap().current_target,
992 Some(fg),
993 "fg engaged as the current drop target"
994 );
995
996 tree.arena.destroy(fg);
998 assert!(!tree.arena.is_active(fg));
999
1000 tree.dispatch_event(WidgetEvent::PointerUp {
1003 position: Point::new(100.0, 50.0),
1004 button: PointerButton::Primary,
1005 modifiers: Modifiers::NONE,
1006 });
1007
1008 assert!(tree.active_drag.is_none(), "drag session cleared");
1009 assert!(
1010 bg_dropped.get(),
1011 "drop landed on the live background target, not the destroyed one"
1012 );
1013 }
1014
1015 #[test]
1016 fn drag_arming_walks_to_an_ancestor_without_a_dead_zone() {
1017 let mut tree = WidgetTree::new();
1021 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1022 let inner = tree.add(StackWidget::new().add_child(button));
1023 let ancestor = tree.add(
1024 StackWidget::new()
1025 .add_child(inner)
1026 .on_drag(|_phase, _ctx| {}),
1027 );
1028 tree.layout(SizeProposal::exact(100.0, 100.0));
1029
1030 let b = tree.bounds(button);
1031 tree.pointer_down_button(
1032 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1033 PointerButton::Primary,
1034 );
1035 assert_eq!(
1036 tree.drag_observers,
1037 vec![ancestor],
1038 "the draggable ancestor is armed when the button press is not in a dead zone"
1039 );
1040 tree.pointer_up_button(
1041 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1042 PointerButton::Primary,
1043 );
1044 }
1045
1046 #[test]
1047 fn gesture_dead_zone_blocks_ancestor_drag_arming() {
1048 use crate::widget_builder::WidgetBuilder;
1054 let mut tree = WidgetTree::new();
1055 let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
1056 let dead_zone = tree.add(StackWidget::new().add_child(button).gesture_dead_zone(true));
1057 let _ancestor = tree.add(
1058 StackWidget::new()
1059 .add_child(dead_zone)
1060 .on_drag(|_phase, _ctx| {}),
1061 );
1062 tree.layout(SizeProposal::exact(100.0, 100.0));
1063
1064 let b = tree.bounds(button);
1065 tree.pointer_down_button(
1066 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1067 PointerButton::Primary,
1068 );
1069 assert!(
1070 tree.drag_observers.is_empty(),
1071 "a dead zone blocks the draggable ancestor from being armed"
1072 );
1073 tree.pointer_up_button(
1074 Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
1075 PointerButton::Primary,
1076 );
1077 }
1078
1079 #[test]
1080 fn drag_hover_calls_on_drag_hover() {
1081 use std::cell::Cell;
1082 use std::rc::Rc;
1083
1084 let hover_count = Rc::new(Cell::new(0));
1085 let hc = hover_count.clone();
1086
1087 let mut tree = WidgetTree::new();
1088 let source = tree.add(FillWidget::new());
1089 let _target = tree.add(
1090 FillWidget::new()
1091 .on_drag_hover(move |_payload, _pos, _ctx| {
1092 hc.set(hc.get() + 1);
1093 crate::drag_state::DropFeedback::InsertionLine {
1094 y: 50.0,
1095 width: 200.0,
1096 }
1097 })
1098 .on_drop(|_, _, _| true),
1099 );
1100 tree.layout(SizeProposal::exact(200.0, 100.0));
1101
1102 let mut ctx = crate::widget::EventContext::new();
1104 ctx.start_drag(source, crate::drag_payload::DragPayload::typed("test"));
1105 tree.collect_from_ctx(ctx, source);
1106
1107 tree.dispatch_event(WidgetEvent::PointerMove {
1109 position: Point::new(150.0, 50.0),
1110 });
1111
1112 assert!(
1113 hover_count.get() > 0,
1114 "on_drag_hover should have been called"
1115 );
1116 }
1117
1118 #[test]
1124 fn rejecter_under_engaging_ancestor_leaves_once() {
1125 use std::cell::Cell;
1126 use std::rc::Rc;
1127
1128 let leaves = Rc::new(Cell::new(0));
1129 let lv = leaves.clone();
1130 let engage = Rc::new(Cell::new(false));
1133 let eg = engage.clone();
1134
1135 let mut tree = WidgetTree::new();
1136 let source = tree.add(FillWidget::new());
1137
1138 let child = tree.add(
1140 FillWidget::new()
1141 .on_drag_hover(|_payload, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1142 .on_drag_leave(move |_ctx| lv.set(lv.get() + 1)),
1143 );
1144 let _ancestor = tree.add(StackWidget::new().add_child(child).on_drag_hover(
1146 move |_payload, _pos, _ctx| {
1147 if eg.get() {
1148 crate::drag_state::DropFeedback::Accept
1149 } else {
1150 crate::drag_state::DropFeedback::NoFeedback
1151 }
1152 },
1153 ));
1154 tree.layout(SizeProposal::exact(200.0, 100.0));
1155
1156 let mut ctx = crate::widget::EventContext::new();
1157 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
1158 tree.collect_from_ctx(ctx, source);
1159
1160 tree.dispatch_event(WidgetEvent::PointerMove {
1162 position: Point::new(100.0, 50.0),
1163 });
1164 assert_eq!(
1165 tree.active_drag.as_ref().unwrap().current_target,
1166 Some(child)
1167 );
1168 assert_eq!(leaves.get(), 0, "no leave yet — child is freshly tracked");
1169
1170 engage.set(true);
1172 tree.dispatch_event(WidgetEvent::PointerMove {
1173 position: Point::new(101.0, 50.0),
1174 });
1175
1176 assert_eq!(
1177 leaves.get(),
1178 1,
1179 "child must receive exactly one on_drag_leave, not two"
1180 );
1181 }
1182
1183 #[test]
1184 fn drop_outside_window_cancels() {
1185 let mut tree = WidgetTree::new();
1186 let source = tree.add(FillWidget::new());
1187 tree.layout(SizeProposal::exact(100.0, 50.0));
1188
1189 let mut ctx = crate::widget::EventContext::new();
1190 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1191 tree.collect_from_ctx(ctx, source);
1192 assert!(tree.active_drag.is_some());
1193
1194 tree.dispatch_event(WidgetEvent::PointerUp {
1196 position: Point::new(-100.0, -100.0),
1197 button: PointerButton::Primary,
1198 modifiers: Modifiers::NONE,
1199 });
1200
1201 assert!(tree.active_drag.is_none(), "drag should be cleared");
1202 }
1203
1204 #[test]
1205 fn drop_target_rejects_wrong_type() {
1206 use std::cell::Cell;
1207 use std::rc::Rc;
1208
1209 let accepted = Rc::new(Cell::new(false));
1210 let a = accepted.clone();
1211
1212 let mut tree = WidgetTree::new();
1213 let source = tree.add(FillWidget::new());
1214 let _target = tree.add(
1216 FillWidget::new()
1217 .on_drag_hover(|payload, _pos, _ctx| {
1218 if payload.has_typed::<String>() {
1219 crate::drag_state::DropFeedback::InsertionLine {
1220 y: 0.0,
1221 width: 100.0,
1222 }
1223 } else {
1224 crate::drag_state::DropFeedback::NoFeedback
1225 }
1226 })
1227 .on_drop(move |payload, _pos, _ctx| {
1228 if payload.has_typed::<String>() {
1229 a.set(true);
1230 true
1231 } else {
1232 false
1233 }
1234 }),
1235 );
1236 tree.layout(SizeProposal::exact(200.0, 100.0));
1237
1238 let mut ctx = crate::widget::EventContext::new();
1240 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
1241 tree.collect_from_ctx(ctx, source);
1242
1243 tree.dispatch_event(WidgetEvent::PointerUp {
1244 position: Point::new(150.0, 50.0),
1245 button: PointerButton::Primary,
1246 modifiers: Modifiers::NONE,
1247 });
1248
1249 assert!(!accepted.get(), "on_drop should reject wrong payload type");
1250 }
1251
1252 #[test]
1253 fn inter_widget_drop_transfers_payload() {
1254 use std::cell::Cell;
1255 use std::rc::Rc;
1256
1257 let received_value = Rc::new(Cell::new(0_u32));
1258 let rv = received_value.clone();
1259
1260 let mut tree = WidgetTree::new();
1261 let source = tree.add(FillWidget::new());
1262 let _target = tree.add(
1263 FillWidget::new()
1264 .on_drag_hover(|_payload, _pos, _ctx| {
1265 crate::drag_state::DropFeedback::InsertionLine {
1266 y: 0.0,
1267 width: 100.0,
1268 }
1269 })
1270 .on_drop(move |mut payload, _pos, _ctx| {
1271 if let Some(val) = payload.take_typed::<u32>() {
1272 rv.set(val);
1273 true
1274 } else {
1275 false
1276 }
1277 }),
1278 );
1279 tree.layout(SizeProposal::exact(200.0, 100.0));
1280
1281 let mut ctx = crate::widget::EventContext::new();
1283 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(777_u32));
1284 tree.collect_from_ctx(ctx, source);
1285
1286 tree.dispatch_event(WidgetEvent::PointerUp {
1288 position: Point::new(150.0, 50.0),
1289 button: PointerButton::Primary,
1290 modifiers: Modifiers::NONE,
1291 });
1292
1293 assert_eq!(
1294 received_value.get(),
1295 777,
1296 "Target should receive the typed payload from source"
1297 );
1298 }
1299
1300 #[test]
1301 fn drop_on_child_walks_up_to_ancestor_drop_target() {
1302 use crate::test_widgets::StackWidget;
1303 use std::cell::Cell;
1304 use std::rc::Rc;
1305
1306 let parent_fired = Rc::new(Cell::new(false));
1309 let pf = parent_fired.clone();
1310
1311 let mut tree = WidgetTree::new();
1312 let source = tree.add(FillWidget::new());
1313 let child = tree.add(FillWidget::new());
1314 let _parent = tree.add(StackWidget::new().add_child(child).on_drop(
1315 move |_payload, _pos, _ctx| {
1316 pf.set(true);
1317 true
1318 },
1319 ));
1320 tree.layout(SizeProposal::exact(200.0, 100.0));
1321
1322 let mut ctx = crate::widget::EventContext::new();
1324 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
1325 tree.collect_from_ctx(ctx, source);
1326
1327 tree.dispatch_event(WidgetEvent::PointerUp {
1330 position: Point::new(100.0, 50.0),
1331 button: PointerButton::Primary,
1332 modifiers: Modifiers::NONE,
1333 });
1334
1335 assert!(
1336 parent_fired.get(),
1337 "Parent's on_drop should fire via ancestor walk"
1338 );
1339 }
1340
1341 #[test]
1342 fn drop_bubbles_past_a_rejecting_child_to_ancestor() {
1343 use crate::test_widgets::StackWidget;
1344 use std::cell::Cell;
1345 use std::rc::Rc;
1346
1347 let child_drop = Rc::new(Cell::new(false));
1352 let parent_drop = Rc::new(Cell::new(false));
1353 let cd = child_drop.clone();
1354 let pd = parent_drop.clone();
1355
1356 let mut tree = WidgetTree::new();
1357 let source = tree.add(FillWidget::new());
1358 let child = tree.add(
1359 FillWidget::new()
1360 .on_drag_hover(|_p, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
1361 .on_drop(move |_p, _pos, _ctx| {
1362 cd.set(true);
1363 false }),
1365 );
1366 let _parent = tree.add(StackWidget::new().add_child(child).on_drop(
1367 move |_p, _pos, _ctx| {
1368 pd.set(true);
1369 true
1370 },
1371 ));
1372 tree.layout(SizeProposal::exact(200.0, 100.0));
1373
1374 let mut ctx = crate::widget::EventContext::new();
1375 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u8));
1376 tree.collect_from_ctx(ctx, source);
1377
1378 tree.dispatch_event(WidgetEvent::PointerMove {
1381 position: Point::new(100.0, 50.0),
1382 });
1383 tree.dispatch_event(WidgetEvent::PointerUp {
1384 position: Point::new(100.0, 50.0),
1385 button: PointerButton::Primary,
1386 modifiers: Modifiers::NONE,
1387 });
1388
1389 assert!(parent_drop.get(), "drop bubbles to the accepting ancestor");
1390 assert!(
1391 !child_drop.get(),
1392 "the rejecting child must not receive the drop"
1393 );
1394 }
1395
1396 #[test]
1397 fn drag_preview_overlay_created_and_dismissed() {
1398 let mut tree = WidgetTree::new();
1399 let source = tree.add(FillWidget::new());
1400 tree.layout(SizeProposal::exact(100.0, 50.0));
1401
1402 let overlay_count_before = tree.overlay_manager().len();
1403
1404 let mut ctx = crate::widget::EventContext::new();
1406 ctx.start_drag_with_preview(
1407 source,
1408 crate::drag_payload::DragPayload::typed(0_u32),
1409 Box::new(FillWidget::new()),
1410 );
1411 tree.collect_from_ctx(ctx, source);
1412
1413 assert!(tree.active_drag.is_some(), "drag session should be active");
1414 assert!(
1415 tree.active_drag
1416 .as_ref()
1417 .unwrap()
1418 .preview_overlay_id
1419 .is_some(),
1420 "preview overlay id should be recorded"
1421 );
1422 assert_eq!(
1423 tree.overlay_manager().len(),
1424 overlay_count_before + 1,
1425 "overlay count should increase by one for the preview"
1426 );
1427
1428 tree.dispatch_event(WidgetEvent::PointerUp {
1430 position: Point::new(999.0, 999.0),
1431 button: PointerButton::Primary,
1432 modifiers: Modifiers::NONE,
1433 });
1434
1435 assert!(tree.active_drag.is_none(), "drag session should be cleared");
1436 assert_eq!(
1437 tree.overlay_manager().len(),
1438 overlay_count_before,
1439 "preview overlay should be dismissed on drop"
1440 );
1441 }
1442
1443 #[test]
1444 fn drag_preview_follows_pointer_position() {
1445 let mut tree = WidgetTree::new();
1446 let source = tree.add(FillWidget::new());
1447 tree.layout(SizeProposal::exact(200.0, 100.0));
1448
1449 let mut ctx = crate::widget::EventContext::new();
1450 ctx.start_drag_with_preview(
1451 source,
1452 crate::drag_payload::DragPayload::typed("p"),
1453 Box::new(FillWidget::new()),
1454 );
1455 tree.collect_from_ctx(ctx, source);
1456
1457 tree.dispatch_event(WidgetEvent::PointerMove {
1458 position: Point::new(73.0, 41.0),
1459 });
1460
1461 let drag = tree.active_drag.as_ref().expect("active drag");
1462 assert!(
1463 (drag.current_position.x - 73.0).abs() < 0.01
1464 && (drag.current_position.y - 41.0).abs() < 0.01,
1465 "drag session position should track the pointer"
1466 );
1467
1468 let overlay_id = drag.preview_overlay_id.expect("preview overlay");
1469 let overlay = tree
1470 .overlay_manager()
1471 .overlay(overlay_id)
1472 .expect("overlay looked up by id");
1473 match &overlay.placement {
1474 crate::overlay::OverlayPlacement::AtPointer(p) => {
1475 assert!(
1476 (p.x - 73.0).abs() < 0.01 && (p.y - 41.0).abs() < 0.01,
1477 "preview overlay placement should follow pointer"
1478 );
1479 }
1480 other => panic!("expected AtPointer placement, got {:?}", other),
1481 }
1482 }
1483
1484 #[test]
1485 fn escape_during_hover_dismisses_preview() {
1486 let mut tree = WidgetTree::new();
1487 let source = tree.add(FillWidget::new());
1488 let _target = tree.add(
1489 FillWidget::new()
1490 .on_drag_hover(|_payload, _pos, _ctx| {
1491 crate::drag_state::DropFeedback::InsertionLine {
1492 y: 0.0,
1493 width: 100.0,
1494 }
1495 })
1496 .on_drop(|_, _, _| true),
1497 );
1498 tree.layout(SizeProposal::exact(200.0, 100.0));
1499
1500 let overlay_count_before = tree.overlay_manager().len();
1501
1502 let mut ctx = crate::widget::EventContext::new();
1503 ctx.start_drag_with_preview(
1504 source,
1505 crate::drag_payload::DragPayload::typed(0_u32),
1506 Box::new(FillWidget::new()),
1507 );
1508 tree.collect_from_ctx(ctx, source);
1509
1510 tree.dispatch_event(WidgetEvent::PointerMove {
1512 position: Point::new(150.0, 50.0),
1513 });
1514
1515 assert!(tree.active_drag.is_some());
1516 assert_eq!(tree.overlay_manager().len(), overlay_count_before + 1);
1517
1518 tree.press_key(Key::Escape, Modifiers::NONE);
1520
1521 assert!(tree.active_drag.is_none(), "drag must be cancelled");
1522 assert_eq!(
1523 tree.overlay_manager().len(),
1524 overlay_count_before,
1525 "preview overlay must be dismissed after Escape"
1526 );
1527 }
1528
1529 #[test]
1530 fn active_drag_blocks_on_tap_on_other_widgets() {
1531 use std::cell::Cell;
1532 use std::rc::Rc;
1533
1534 let tap_fired = Rc::new(Cell::new(false));
1539 let tf = tap_fired.clone();
1540
1541 let mut tree = WidgetTree::new();
1542 let source = tree.add(FillWidget::new());
1543 let _other = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
1544 tf.set(true);
1545 }));
1546 tree.layout(SizeProposal::exact(200.0, 100.0));
1547
1548 let mut ctx = crate::widget::EventContext::new();
1549 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1550 tree.collect_from_ctx(ctx, source);
1551
1552 tree.dispatch_event(WidgetEvent::PointerMove {
1555 position: Point::new(150.0, 50.0),
1556 });
1557 tree.dispatch_event(WidgetEvent::PointerUp {
1558 position: Point::new(150.0, 50.0),
1559 button: PointerButton::Primary,
1560 modifiers: Modifiers::NONE,
1561 });
1562
1563 assert!(
1564 !tap_fired.get(),
1565 "on_tap must not fire during an active drag"
1566 );
1567 }
1568
1569 #[test]
1572 fn on_drag_leave_fires_when_pointer_leaves_target_bounds() {
1573 use crate::test_widgets::InsetWidget;
1579 use std::cell::Cell;
1580 use std::rc::Rc;
1581
1582 let leave = Rc::new(Cell::new(0_u32));
1583 let l = leave.clone();
1584
1585 let mut tree = WidgetTree::new();
1586 let source = tree.add(FillWidget::new());
1587 let target = tree.add(
1588 FillWidget::new()
1589 .on_drag_hover(
1590 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1591 y: 0.0,
1592 width: 10.0,
1593 },
1594 )
1595 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1596 .on_drop(|_, _, _| true),
1597 );
1598 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
1599 tree.layout(SizeProposal::exact(200.0, 100.0));
1600
1601 let mut ctx = crate::widget::EventContext::new();
1602 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1603 tree.collect_from_ctx(ctx, source);
1604
1605 tree.dispatch_event(WidgetEvent::PointerMove {
1607 position: Point::new(100.0, 50.0),
1608 });
1609 assert_eq!(leave.get(), 0, "no leave yet — target just became active");
1610
1611 tree.dispatch_event(WidgetEvent::PointerMove {
1616 position: Point::new(10.0, 10.0),
1617 });
1618 assert_eq!(
1619 leave.get(),
1620 1,
1621 "on_drag_leave fires when pointer exits the target's bounds"
1622 );
1623
1624 tree.dispatch_event(WidgetEvent::PointerMove {
1626 position: Point::new(100.0, 50.0),
1627 });
1628 tree.dispatch_event(WidgetEvent::PointerMove {
1629 position: Point::new(100.0, 50.0),
1630 });
1631 assert_eq!(
1632 leave.get(),
1633 1,
1634 "leave fires at most once per leave transition"
1635 );
1636
1637 tree.dispatch_event(WidgetEvent::PointerMove {
1639 position: Point::new(10.0, 10.0),
1640 });
1641 assert_eq!(leave.get(), 2);
1642 }
1643
1644 #[test]
1645 fn on_drag_leave_fires_on_drop() {
1646 use std::cell::Cell;
1647 use std::rc::Rc;
1648
1649 let leave = Rc::new(Cell::new(0_u32));
1650 let l = leave.clone();
1651
1652 let mut tree = WidgetTree::new();
1653 let source = tree.add(FillWidget::new());
1654 let _target = tree.add(
1655 FillWidget::new()
1656 .on_drag_hover(
1657 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1658 y: 0.0,
1659 width: 10.0,
1660 },
1661 )
1662 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1663 .on_drop(|_, _, _| true),
1664 );
1665 tree.layout(SizeProposal::exact(200.0, 100.0));
1666
1667 let mut ctx = crate::widget::EventContext::new();
1668 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1669 tree.collect_from_ctx(ctx, source);
1670 tree.dispatch_event(WidgetEvent::PointerMove {
1671 position: Point::new(100.0, 50.0),
1672 });
1673 tree.dispatch_event(WidgetEvent::PointerUp {
1674 position: Point::new(100.0, 50.0),
1675 button: PointerButton::Primary,
1676 modifiers: Modifiers::NONE,
1677 });
1678
1679 assert_eq!(leave.get(), 1, "on_drag_leave fires exactly once on drop");
1680 }
1681
1682 #[test]
1683 fn on_drag_leave_fires_on_escape_cancel() {
1684 use std::cell::Cell;
1685 use std::rc::Rc;
1686
1687 let leave = Rc::new(Cell::new(0_u32));
1688 let l = leave.clone();
1689
1690 let mut tree = WidgetTree::new();
1691 let source = tree.add(FillWidget::new());
1692 let _target = tree.add(
1693 FillWidget::new()
1694 .on_drag_hover(
1695 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1696 y: 0.0,
1697 width: 10.0,
1698 },
1699 )
1700 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1701 .on_drop(|_, _, _| true),
1702 );
1703 tree.layout(SizeProposal::exact(200.0, 100.0));
1704
1705 let mut ctx = crate::widget::EventContext::new();
1706 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1707 tree.collect_from_ctx(ctx, source);
1708 tree.dispatch_event(WidgetEvent::PointerMove {
1709 position: Point::new(100.0, 50.0),
1710 });
1711 tree.press_key(Key::Escape, Modifiers::NONE);
1712
1713 assert_eq!(
1714 leave.get(),
1715 1,
1716 "Escape cancel must fire on_drag_leave on the current target"
1717 );
1718 }
1719
1720 #[test]
1721 fn on_drag_leave_fires_when_source_destroyed_mid_drag() {
1722 use std::cell::Cell;
1723 use std::rc::Rc;
1724
1725 let leave = Rc::new(Cell::new(0_u32));
1726 let l = leave.clone();
1727
1728 let mut tree = WidgetTree::new();
1729 let source = tree.add(FillWidget::new());
1730 let _target = tree.add(
1731 FillWidget::new()
1732 .on_drag_hover(
1733 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1734 y: 0.0,
1735 width: 10.0,
1736 },
1737 )
1738 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
1739 .on_drop(|_, _, _| true),
1740 );
1741 tree.layout(SizeProposal::exact(200.0, 100.0));
1742
1743 let mut ctx = crate::widget::EventContext::new();
1744 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1745 tree.collect_from_ctx(ctx, source);
1746 tree.dispatch_event(WidgetEvent::PointerMove {
1747 position: Point::new(100.0, 50.0),
1748 });
1749
1750 tree.arena.destroy(source);
1751 tree.layout(SizeProposal::exact(200.0, 100.0));
1754
1755 assert!(
1756 tree.active_drag.is_none(),
1757 "active drag should have been cancelled"
1758 );
1759 assert_eq!(
1760 leave.get(),
1761 1,
1762 "on_drag_leave fires on the drop target when the source is torn down"
1763 );
1764 }
1765
1766 #[test]
1767 fn on_drag_tick_fires_per_layout_pass() {
1768 use std::cell::Cell;
1769 use std::rc::Rc;
1770
1771 let ticks = Rc::new(Cell::new(0_u32));
1772 let t = ticks.clone();
1773
1774 let mut tree = WidgetTree::new();
1775 let source = tree.add(FillWidget::new());
1776 let _target = tree.add(
1777 FillWidget::new()
1778 .on_drag_hover(
1779 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
1780 y: 0.0,
1781 width: 10.0,
1782 },
1783 )
1784 .on_drag_tick(move |_pos, _ctx| t.set(t.get() + 1))
1785 .on_drop(|_, _, _| true),
1786 );
1787 tree.layout(SizeProposal::exact(200.0, 100.0));
1788
1789 let mut ctx = crate::widget::EventContext::new();
1790 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1791 tree.collect_from_ctx(ctx, source);
1792 tree.dispatch_event(WidgetEvent::PointerMove {
1794 position: Point::new(100.0, 50.0),
1795 });
1796 assert_eq!(ticks.get(), 0, "tick shouldn't have fired yet");
1797
1798 tree.layout(SizeProposal::exact(200.0, 100.0));
1799 assert_eq!(ticks.get(), 1);
1800 tree.layout(SizeProposal::exact(200.0, 100.0));
1801 tree.layout(SizeProposal::exact(200.0, 100.0));
1802 assert_eq!(ticks.get(), 3);
1803
1804 tree.dispatch_event(WidgetEvent::PointerUp {
1806 position: Point::new(100.0, 50.0),
1807 button: PointerButton::Primary,
1808 modifiers: Modifiers::NONE,
1809 });
1810 let after_drop = ticks.get();
1811 tree.layout(SizeProposal::exact(200.0, 100.0));
1812 tree.layout(SizeProposal::exact(200.0, 100.0));
1813 assert_eq!(
1814 ticks.get(),
1815 after_drop,
1816 "on_drag_tick must not fire after drag ends"
1817 );
1818 }
1819
1820 #[test]
1821 fn on_drag_hover_and_on_drop_receive_widget_local_coordinates() {
1822 use crate::test_widgets::InsetWidget;
1829 use std::cell::Cell;
1830 use std::rc::Rc;
1831
1832 let hover_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
1833 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
1834 let h = hover_local.clone();
1835 let d = drop_local.clone();
1836
1837 let mut tree = WidgetTree::new();
1838 let source = tree.add(FillWidget::new());
1839 let target = tree.add(
1841 FillWidget::new()
1842 .on_drag_hover(move |_p, pos, _ctx| {
1843 h.set(pos);
1844 crate::drag_state::DropFeedback::InsertionLine {
1845 y: 0.0,
1846 width: 10.0,
1847 }
1848 })
1849 .on_drop(move |_payload, pos, _ctx| {
1850 d.set(pos);
1851 true
1852 }),
1853 );
1854 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
1855 tree.layout(SizeProposal::exact(200.0, 100.0));
1856
1857 let mut ctx = crate::widget::EventContext::new();
1858 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
1859 tree.collect_from_ctx(ctx, source);
1860
1861 tree.dispatch_event(WidgetEvent::PointerMove {
1865 position: Point::new(100.0, 60.0),
1866 });
1867 let hov = hover_local.get();
1868 assert!(
1869 (hov.x - 60.0).abs() < 0.01 && (hov.y - 20.0).abs() < 0.01,
1870 "on_drag_hover should receive local coords, got {:?}",
1871 hov,
1872 );
1873
1874 tree.dispatch_event(WidgetEvent::PointerUp {
1876 position: Point::new(110.0, 55.0),
1877 button: PointerButton::Primary,
1878 modifiers: Modifiers::NONE,
1879 });
1880 let drp = drop_local.get();
1881 assert!(
1882 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
1883 "on_drop should receive local coords, got {:?}",
1884 drp,
1885 );
1886 }
1887
1888 #[test]
1889 fn active_drag_sets_grabbing_cursor() {
1890 let mut tree = WidgetTree::new();
1895 let source = tree.add(FillWidget::new());
1896 tree.layout(SizeProposal::exact(100.0, 50.0));
1897
1898 assert_eq!(tree.current_cursor(), CursorIcon::Default);
1899
1900 let mut ctx = crate::widget::EventContext::new();
1901 ctx.start_drag_with_preview(
1902 source,
1903 crate::drag_payload::DragPayload::typed(0_u32),
1904 Box::new(FillWidget::new()),
1905 );
1906 tree.collect_from_ctx(ctx, source);
1907 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
1908
1909 tree.dispatch_event(WidgetEvent::PointerUp {
1911 position: Point::new(50.0, 25.0),
1912 button: PointerButton::Primary,
1913 modifiers: Modifiers::NONE,
1914 });
1915 assert_eq!(tree.current_cursor(), CursorIcon::Default);
1916 }
1917
1918 #[test]
1919 fn escape_cancel_resets_cursor() {
1920 let mut tree = WidgetTree::new();
1921 let source = tree.add(FillWidget::new());
1922 tree.layout(SizeProposal::exact(100.0, 50.0));
1923
1924 let mut ctx = crate::widget::EventContext::new();
1925 ctx.start_drag_with_preview(
1926 source,
1927 crate::drag_payload::DragPayload::typed(0_u32),
1928 Box::new(FillWidget::new()),
1929 );
1930 tree.collect_from_ctx(ctx, source);
1931 assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
1932
1933 tree.press_key(Key::Escape, Modifiers::NONE);
1934 assert_eq!(tree.current_cursor(), CursorIcon::Default);
1935 }
1936
1937 #[test]
1938 fn drag_preview_composite_gets_built() {
1939 use std::cell::Cell;
1945 use std::rc::Rc;
1946
1947 let built = Rc::new(Cell::new(false));
1948 let b = built.clone();
1949
1950 #[derive(Debug)]
1951 struct CheckingWidget {
1952 built: Rc<Cell<bool>>,
1953 }
1954 impl Widget for CheckingWidget {
1955 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1956 self.built.set(true);
1957 Vec::new()
1958 }
1959 fn layout_response(
1960 &self,
1961 _: SizeProposal,
1962 _: &crate::widget::LayoutContext,
1963 ) -> crate::widget::LayoutResponse {
1964 teksilo_canvas::Size::new(50.0, 20.0).into()
1965 }
1966 }
1967
1968 let mut tree = WidgetTree::new();
1969 let source = tree.add(FillWidget::new());
1970 tree.layout(SizeProposal::exact(200.0, 100.0));
1971
1972 let mut ctx = crate::widget::EventContext::new();
1973 ctx.start_drag_with_preview(
1974 source,
1975 crate::drag_payload::DragPayload::typed(0_u32),
1976 Box::new(CheckingWidget { built: b }),
1977 );
1978 tree.collect_from_ctx(ctx, source);
1979
1980 assert!(built.get(), "preview's build() must fire on drag start");
1981 }
1982
1983 #[test]
1984 fn preview_placement_drives_layout_needs() {
1985 let mut tree = WidgetTree::new();
1993 let source = tree.add(FillWidget::new());
1994 tree.layout(SizeProposal::exact(200.0, 200.0));
1995
1996 let mut ctx = crate::widget::EventContext::new();
1997 ctx.start_drag_with_preview(
1998 source,
1999 crate::drag_payload::DragPayload::typed(0_u32),
2000 Box::new(FillWidget::new()),
2001 );
2002 tree.collect_from_ctx(ctx, source);
2003
2004 assert!(
2007 tree.needs_layout(),
2008 "drag start must mark preview content for layout"
2009 );
2010 tree.layout(SizeProposal::exact(200.0, 200.0));
2011 assert!(
2012 !tree.needs_layout(),
2013 "layout should have cleared dirty flag"
2014 );
2015
2016 tree.dispatch_event(WidgetEvent::PointerMove {
2019 position: Point::new(75.0, 120.0),
2020 });
2021 assert!(
2022 tree.needs_layout(),
2023 "PointerMove during drag must mark preview for layout"
2024 );
2025 }
2026
2027 #[test]
2028 fn scroll_during_drag_routes_to_drop_target() {
2029 use std::cell::Cell;
2030 use std::rc::Rc;
2031
2032 let scroll_count = Rc::new(Cell::new(0_u32));
2033 let sc = scroll_count.clone();
2034
2035 let mut tree = WidgetTree::new();
2036 let source = tree.add(FillWidget::new());
2037 let _target = tree.add(
2038 FillWidget::new()
2039 .on_drag_hover(
2040 |_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
2041 y: 0.0,
2042 width: 10.0,
2043 },
2044 )
2045 .on_scroll(move |event, _ctx| match event {
2046 WidgetEvent::Scroll { .. } => {
2047 sc.set(sc.get() + 1);
2048 EventResponse::Handled
2049 }
2050 _ => EventResponse::Ignored,
2051 })
2052 .on_drop(|_, _, _| true),
2053 );
2054 tree.layout(SizeProposal::exact(200.0, 100.0));
2055
2056 let mut ctx = crate::widget::EventContext::new();
2057 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2058 tree.collect_from_ctx(ctx, source);
2059 tree.dispatch_event(WidgetEvent::PointerMove {
2061 position: Point::new(100.0, 50.0),
2062 });
2063
2064 tree.dispatch_event(WidgetEvent::Scroll {
2067 delta: crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
2068 modifiers: Default::default(),
2069 });
2070 assert_eq!(
2071 scroll_count.get(),
2072 1,
2073 "Scroll during drag must route to the current drop target"
2074 );
2075 }
2076
2077 #[test]
2080 fn external_drop_delivers_files_and_marks_external() {
2081 use crate::drag_payload::ExternalDropData;
2082 use std::cell::RefCell;
2083 use std::path::PathBuf;
2084 use std::rc::Rc;
2085
2086 let dropped_files: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
2087 let was_external = Rc::new(std::cell::Cell::new(false));
2088 let df = dropped_files.clone();
2089 let we = was_external.clone();
2090
2091 let mut tree = WidgetTree::new();
2092 let _target = tree.add(
2093 FillWidget::new()
2094 .on_drag_hover(|payload, _pos, _ctx| {
2095 if payload.is_external() && !payload.files().is_empty() {
2097 crate::drag_state::DropFeedback::HighlightRect {
2098 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2099 color: teksilo_tokens::Color::WHITE,
2100 }
2101 } else {
2102 crate::drag_state::DropFeedback::NoFeedback
2103 }
2104 })
2105 .on_drop(move |payload, _pos, _ctx| {
2106 we.set(payload.is_external());
2107 *df.borrow_mut() = payload.files().to_vec();
2108 true
2109 }),
2110 );
2111 tree.layout(SizeProposal::exact(200.0, 100.0));
2112
2113 let mut noop = crate::window::NoopWindowOps;
2114 let data = ExternalDropData {
2115 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2116 ..Default::default()
2117 };
2118 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2119 assert!(tree.active_drag.is_some());
2120 assert!(tree.active_drag.as_ref().unwrap().is_external);
2121
2122 tree.update_external_drag(Point::new(110.0, 55.0), &mut noop);
2123 let drop_data = ExternalDropData {
2125 files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2126 ..Default::default()
2127 };
2128 tree.end_external_drag(Point::new(110.0, 55.0), drop_data, &mut noop);
2129
2130 assert!(
2131 tree.active_drag.is_none(),
2132 "external drag must clear on drop"
2133 );
2134 assert!(was_external.get(), "payload should report external origin");
2135 assert_eq!(
2136 *dropped_files.borrow(),
2137 vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
2138 );
2139 }
2140
2141 #[test]
2142 fn external_drop_passes_local_coordinates() {
2143 use crate::drag_payload::ExternalDropData;
2144 use crate::test_widgets::InsetWidget;
2145 use std::cell::Cell;
2146 use std::path::PathBuf;
2147 use std::rc::Rc;
2148
2149 let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
2150 let d = drop_local.clone();
2151
2152 let mut tree = WidgetTree::new();
2153 let target = tree.add(
2155 FillWidget::new()
2156 .on_drag_hover(
2157 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2158 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2159 color: teksilo_tokens::Color::WHITE,
2160 },
2161 )
2162 .on_drop(move |_payload, pos, _ctx| {
2163 d.set(pos);
2164 true
2165 }),
2166 );
2167 let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
2168 tree.layout(SizeProposal::exact(200.0, 100.0));
2169
2170 let mut noop = crate::window::NoopWindowOps;
2171 let data = ExternalDropData {
2172 files: vec![PathBuf::from("/tmp/x")],
2173 ..Default::default()
2174 };
2175 tree.begin_external_drag(Point::new(110.0, 55.0), data, &mut noop);
2177 tree.end_external_drag(
2178 Point::new(110.0, 55.0),
2179 ExternalDropData::default(),
2180 &mut noop,
2181 );
2182
2183 let drp = drop_local.get();
2184 assert!(
2185 (drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
2186 "external on_drop should receive local coords, got {:?}",
2187 drp,
2188 );
2189 }
2190
2191 #[test]
2192 fn cancel_external_drag_clears_session_and_fires_leave() {
2193 use crate::drag_payload::ExternalDropData;
2194 use std::cell::Cell;
2195 use std::path::PathBuf;
2196 use std::rc::Rc;
2197
2198 let left = Rc::new(Cell::new(0_u32));
2199 let l = left.clone();
2200
2201 let mut tree = WidgetTree::new();
2202 let _target = tree.add(
2203 FillWidget::new()
2204 .on_drag_hover(
2205 |_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
2206 rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
2207 color: teksilo_tokens::Color::WHITE,
2208 },
2209 )
2210 .on_drag_leave(move |_ctx| l.set(l.get() + 1))
2211 .on_drop(|_, _, _| true),
2212 );
2213 tree.layout(SizeProposal::exact(200.0, 100.0));
2214
2215 let mut noop = crate::window::NoopWindowOps;
2216 let data = ExternalDropData {
2217 files: vec![PathBuf::from("/tmp/x")],
2218 ..Default::default()
2219 };
2220 tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
2221 assert!(tree.active_drag.is_some());
2222
2223 tree.cancel_external_drag(&mut noop);
2224 assert!(tree.active_drag.is_none(), "cancel must clear the session");
2225 assert_eq!(
2226 left.get(),
2227 1,
2228 "cancel must fire on_drag_leave on the target"
2229 );
2230 }
2231
2232 #[test]
2233 fn external_drag_helpers_noop_without_session() {
2234 let mut tree = WidgetTree::new();
2236 let _t = tree.add(FillWidget::new());
2237 tree.layout(SizeProposal::exact(100.0, 50.0));
2238
2239 let mut noop = crate::window::NoopWindowOps;
2240 tree.update_external_drag(Point::new(10.0, 10.0), &mut noop);
2241 tree.end_external_drag(
2242 Point::new(10.0, 10.0),
2243 crate::drag_payload::ExternalDropData::default(),
2244 &mut noop,
2245 );
2246 tree.cancel_external_drag(&mut noop);
2247 assert!(tree.active_drag.is_none());
2248 }
2249
2250 struct RecordingWindowOps {
2255 started: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OutboundDragData>>>,
2256 succeed: bool,
2257 cancels: std::rc::Rc<std::cell::Cell<usize>>,
2258 }
2259
2260 impl RecordingWindowOps {
2261 fn new(succeed: bool) -> Self {
2262 Self {
2263 started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
2264 succeed,
2265 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2266 }
2267 }
2268 }
2269 impl crate::window::WindowOps for RecordingWindowOps {
2270 fn open_window(
2271 &mut self,
2272 _c: crate::window::WindowConfig,
2273 ) -> crate::window::TeksiloWindowId {
2274 panic!("not used in these tests")
2275 }
2276 fn find_window(&self, _s: &str) -> Option<crate::window::TeksiloWindowId> {
2277 None
2278 }
2279 fn window_state(
2280 &self,
2281 _id: crate::window::TeksiloWindowId,
2282 ) -> Option<crate::window::WindowState> {
2283 None
2284 }
2285 fn windows(&self) -> Vec<crate::window::WindowState> {
2286 Vec::new()
2287 }
2288 fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
2289 fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
2290 fn begin_os_drag(
2291 &mut self,
2292 data: crate::drag_payload::OutboundDragData,
2293 _image: Option<crate::drag_payload::DragImageData>,
2294 ) -> bool {
2295 self.started.borrow_mut().push(data);
2296 self.succeed
2297 }
2298 fn cancel_os_drag(&mut self) {
2299 self.cancels.set(self.cancels.get() + 1);
2300 }
2301 }
2302
2303 fn exportable_payload() -> crate::drag_payload::DragPayload {
2304 crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"hi".to_vec())
2305 }
2306
2307 #[test]
2308 fn internal_exportable_drag_escalates_when_leaving_window() {
2309 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2310 let mut ops = RecordingWindowOps {
2311 started: started.clone(),
2312 succeed: true,
2313 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2314 };
2315
2316 let mut tree = WidgetTree::new();
2317 let source = tree.add(FillWidget::new());
2318 tree.layout(SizeProposal::exact(200.0, 100.0));
2319
2320 let mut ctx = crate::widget::EventContext::new();
2321 ctx.start_drag(source, exportable_payload());
2322 tree.collect_from_ctx(ctx, source);
2323 assert!(tree.active_drag.is_some());
2324
2325 tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
2327 assert!(started.borrow().is_empty());
2328 assert!(tree.active_drag.is_some());
2329
2330 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2332 assert_eq!(started.borrow().len(), 1, "begin_os_drag called once");
2333 assert!(
2334 started.borrow()[0].mime.contains_key("text/plain"),
2335 "outbound data carries the payload's mime"
2336 );
2337 assert!(tree.active_drag.is_none(), "in-app session torn down");
2338 assert_eq!(tree.outbound_drag_source, Some(source));
2339 }
2340
2341 #[test]
2342 fn os_drag_ended_fires_source_on_drag_ended() {
2343 use crate::drag_payload::DropOutcome;
2344 use std::cell::Cell;
2345 use std::rc::Rc;
2346
2347 let outcome = Rc::new(Cell::new(None));
2348 let o = outcome.clone();
2349
2350 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2351 let mut ops = RecordingWindowOps {
2352 started,
2353 succeed: true,
2354 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2355 };
2356
2357 let mut tree = WidgetTree::new();
2358 let source =
2359 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2360 tree.layout(SizeProposal::exact(200.0, 100.0));
2361
2362 let mut ctx = crate::widget::EventContext::new();
2363 ctx.start_drag(source, exportable_payload());
2364 tree.collect_from_ctx(ctx, source);
2365 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2367
2368 tree.handle_os_drag_ended(DropOutcome::OsMove, &mut ops);
2369 assert_eq!(outcome.get(), Some(DropOutcome::OsMove));
2370 assert!(
2371 tree.outbound_drag_source.is_none(),
2372 "cleared after delivery"
2373 );
2374 }
2375
2376 #[test]
2377 fn escape_during_an_escalated_drag_asks_the_platform_to_cancel() {
2378 let mut ops = RecordingWindowOps::new(true);
2383 let cancels = ops.cancels.clone();
2384
2385 let mut tree = WidgetTree::new();
2386 let source = tree.add(FillWidget::new());
2387 tree.layout(SizeProposal::exact(200.0, 100.0));
2388
2389 let mut ctx = crate::widget::EventContext::new();
2390 ctx.start_drag(source, exportable_payload());
2391 tree.collect_from_ctx(ctx, source);
2392 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
2394
2395 tree.dispatch_event_with_ops(
2396 crate::event::WidgetEvent::KeyDown {
2397 key: crate::event::Key::Escape,
2398 modifiers: crate::event::Modifiers::NONE,
2399 text: None,
2400 },
2401 &mut ops,
2402 );
2403 assert_eq!(cancels.get(), 1, "the platform must be asked to cancel");
2404 assert_eq!(
2405 tree.outbound_drag_source,
2406 Some(source),
2407 "the session stays until the backend reports its terminal outcome — \
2408 tearing it down here would drop the source's on_drag_ended"
2409 );
2410 }
2411
2412 #[test]
2413 fn escape_without_an_os_drag_does_not_touch_the_platform() {
2414 let mut ops = RecordingWindowOps::new(true);
2415 let cancels = ops.cancels.clone();
2416
2417 let mut tree = WidgetTree::new();
2418 tree.add(FillWidget::new());
2419 tree.layout(SizeProposal::exact(200.0, 100.0));
2420
2421 tree.dispatch_event_with_ops(
2422 crate::event::WidgetEvent::KeyDown {
2423 key: crate::event::Key::Escape,
2424 modifiers: crate::event::Modifiers::NONE,
2425 text: None,
2426 },
2427 &mut ops,
2428 );
2429 assert_eq!(cancels.get(), 0);
2430 }
2431
2432 #[test]
2433 fn no_backend_keeps_session_active_on_leave() {
2434 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2437 let mut ops = RecordingWindowOps {
2438 started: started.clone(),
2439 succeed: false,
2440 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2441 };
2442
2443 let mut tree = WidgetTree::new();
2444 let source = tree.add(FillWidget::new());
2445 tree.layout(SizeProposal::exact(200.0, 100.0));
2446
2447 let mut ctx = crate::widget::EventContext::new();
2448 ctx.start_drag(source, exportable_payload());
2449 tree.collect_from_ctx(ctx, source);
2450 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2451
2452 assert_eq!(started.borrow().len(), 1, "escalation was attempted");
2453 assert!(tree.active_drag.is_some(), "session kept (no backend)");
2454 assert!(tree.outbound_drag_source.is_none());
2455 }
2456
2457 #[test]
2458 fn non_exportable_drag_does_not_escalate() {
2459 let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2460 let mut ops = RecordingWindowOps {
2461 started: started.clone(),
2462 succeed: true,
2463 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2464 };
2465
2466 let mut tree = WidgetTree::new();
2467 let source = tree.add(FillWidget::new());
2468 tree.layout(SizeProposal::exact(200.0, 100.0));
2469
2470 let mut ctx = crate::widget::EventContext::new();
2472 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u32));
2473 tree.collect_from_ctx(ctx, source);
2474 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2475
2476 assert!(started.borrow().is_empty(), "no escalation attempt");
2477 assert!(tree.active_drag.is_some(), "session unaffected");
2478 }
2479
2480 #[test]
2481 fn in_app_drop_fires_source_on_drag_ended_with_accepted() {
2482 use crate::drag_payload::DropOutcome;
2483 use std::cell::Cell;
2484 use std::rc::Rc;
2485
2486 let outcome = Rc::new(Cell::new(None));
2487 let o = outcome.clone();
2488
2489 let mut tree = WidgetTree::new();
2490 let source =
2491 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2492 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
2493 tree.layout(SizeProposal::exact(200.0, 100.0));
2494
2495 let mut ctx = crate::widget::EventContext::new();
2496 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
2497 tree.collect_from_ctx(ctx, source);
2498
2499 tree.dispatch_event(WidgetEvent::PointerUp {
2500 position: Point::new(150.0, 50.0),
2501 button: PointerButton::Primary,
2502 modifiers: Modifiers::NONE,
2503 });
2504
2505 assert_eq!(outcome.get(), Some(DropOutcome::InApp { accepted: true }));
2506 }
2507
2508 #[test]
2509 fn escape_fires_source_on_drag_ended_cancelled() {
2510 use crate::drag_payload::DropOutcome;
2511 use std::cell::Cell;
2512 use std::rc::Rc;
2513
2514 let outcome = Rc::new(Cell::new(None));
2515 let o = outcome.clone();
2516
2517 let mut tree = WidgetTree::new();
2518 let source =
2519 tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
2520 tree.layout(SizeProposal::exact(200.0, 100.0));
2521
2522 let mut ctx = crate::widget::EventContext::new();
2523 ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
2524 tree.collect_from_ctx(ctx, source);
2525
2526 tree.press_key(Key::Escape, Modifiers::NONE);
2527 assert_eq!(outcome.get(), Some(DropOutcome::Cancelled));
2528 }
2529
2530 #[test]
2535 fn os_drag_reentry_recovers_typed_payload_for_in_app_drop() {
2536 use crate::drag_payload::{DragPayload, DropOutcome};
2537 use std::cell::Cell;
2538 use std::rc::Rc;
2539
2540 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2541 let mut ops = RecordingWindowOps {
2542 started,
2543 succeed: true,
2544 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2545 };
2546
2547 let got_typed = Rc::new(Cell::new(0_u32));
2548 let ended = Rc::new(Cell::new(0_u32));
2549 let last_outcome = Rc::new(Cell::new(None));
2550 let g = got_typed.clone();
2551 let e = ended.clone();
2552 let lo = last_outcome.clone();
2553
2554 let mut tree = WidgetTree::new();
2555 let source = tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| {
2556 e.set(e.get() + 1);
2557 lo.set(Some(outcome));
2558 }));
2559 let _target =
2560 tree.add(
2561 FillWidget::new().on_drop(move |mut p, _, _| match p.take_typed::<u32>() {
2562 Some(v) => {
2563 g.set(v);
2564 true
2565 }
2566 None => false,
2567 }),
2568 );
2569 tree.layout(SizeProposal::exact(200.0, 100.0));
2570
2571 let mut ctx = crate::widget::EventContext::new();
2573 ctx.start_drag(
2574 source,
2575 DragPayload::typed(123_u32).with_mime("text/plain", b"x".to_vec()),
2576 );
2577 tree.collect_from_ctx(ctx, source);
2578
2579 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2581 assert!(tree.active_drag.is_none());
2582 assert!(
2583 super::has_outbound_typed(),
2584 "typed payload stashed globally"
2585 );
2586
2587 tree.begin_external_drag(
2590 Point::new(100.0, 50.0),
2591 crate::drag_payload::ExternalDropData::default(),
2592 &mut ops,
2593 );
2594 let d = tree.active_drag.as_ref().expect("re-entered session");
2595 assert!(!d.is_external, "re-entry is an internal session");
2596 assert!(d.payload.has_typed::<u32>(), "typed payload recovered");
2597 assert_eq!(
2598 d.payload.text(),
2599 Some("x"),
2600 "external view enriched from MIME so DropZone-style targets also accept"
2601 );
2602 assert!(!super::has_outbound_typed(), "stash taken by the re-entry");
2603
2604 tree.end_external_drag(
2606 Point::new(150.0, 50.0),
2607 crate::drag_payload::ExternalDropData::default(),
2608 &mut ops,
2609 );
2610 assert_eq!(got_typed.get(), 123, "target received the typed payload");
2611 assert_eq!(
2612 ended.get(),
2613 0,
2614 "source on_drag_ended not fired by the drop itself"
2615 );
2616
2617 tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2620 assert_eq!(ended.get(), 1, "on_drag_ended fired exactly once");
2621 assert_eq!(last_outcome.get(), Some(DropOutcome::OsCopy));
2622 }
2623
2624 #[test]
2628 fn os_drag_reentry_recovers_typed_payload_across_windows() {
2629 use crate::drag_payload::{DragPayload, DropOutcome};
2630 use std::cell::Cell;
2631 use std::rc::Rc;
2632
2633 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2634 let mut ops = RecordingWindowOps {
2635 started,
2636 succeed: true,
2637 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2638 };
2639
2640 let mut tree_a = WidgetTree::new();
2642 let src = tree_a.add(FillWidget::new());
2643 tree_a.layout(SizeProposal::exact(200.0, 100.0));
2644 let mut ctx = crate::widget::EventContext::new();
2645 ctx.start_drag(
2646 src,
2647 DragPayload::typed(77_u32).with_mime("text/plain", b"x".to_vec()),
2648 );
2649 tree_a.collect_from_ctx(ctx, src);
2650 tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2651 assert!(super::has_outbound_typed());
2652 assert_eq!(tree_a.outbound_drag_source, Some(src));
2653
2654 let got = Rc::new(Cell::new(0_u32));
2657 let g = got.clone();
2658 let mut tree_b = WidgetTree::new();
2659 let _t = tree_b.add(FillWidget::new().on_drop(
2660 move |mut p, _, _| match p.take_typed::<u32>() {
2661 Some(v) => {
2662 g.set(v);
2663 true
2664 }
2665 None => false,
2666 },
2667 ));
2668 tree_b.layout(SizeProposal::exact(200.0, 100.0));
2669
2670 tree_b.begin_external_drag(
2671 Point::new(50.0, 50.0),
2672 crate::drag_payload::ExternalDropData::default(),
2673 &mut ops,
2674 );
2675 assert!(
2676 tree_b
2677 .active_drag
2678 .as_ref()
2679 .is_some_and(|d| d.payload.has_typed::<u32>()),
2680 "window B recovered the typed payload"
2681 );
2682 tree_b.end_external_drag(
2683 Point::new(50.0, 50.0),
2684 crate::drag_payload::ExternalDropData::default(),
2685 &mut ops,
2686 );
2687 assert_eq!(
2688 got.get(),
2689 77,
2690 "window B's target received the typed payload"
2691 );
2692
2693 tree_a.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
2695 }
2696
2697 #[test]
2701 fn os_drag_reexit_restashes_payload() {
2702 use crate::drag_payload::DragPayload;
2703 use std::rc::Rc;
2704
2705 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2706 let mut ops = RecordingWindowOps {
2707 started: started.clone(),
2708 succeed: true,
2709 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2710 };
2711
2712 let mut tree = WidgetTree::new();
2713 let source = tree.add(FillWidget::new());
2714 tree.layout(SizeProposal::exact(200.0, 100.0));
2715 let mut ctx = crate::widget::EventContext::new();
2716 ctx.start_drag(
2717 source,
2718 DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
2719 );
2720 tree.collect_from_ctx(ctx, source);
2721
2722 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
2724 assert_eq!(started.borrow().len(), 1, "OS drag started once");
2725 tree.begin_external_drag(
2726 Point::new(100.0, 50.0),
2727 crate::drag_payload::ExternalDropData::default(),
2728 &mut ops,
2729 );
2730 assert!(tree.os_drag_reentered);
2731 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(!tree.os_drag_reentered, "re-exited");
2734 assert!(tree.active_drag.is_none(), "session torn down on re-exit");
2735 assert!(super::has_outbound_typed(), "payload re-stashed");
2736 assert_eq!(
2737 started.borrow().len(),
2738 1,
2739 "no second OS drag started on re-exit"
2740 );
2741 }
2742
2743 #[test]
2747 fn source_window_close_clears_stash_no_hijack() {
2748 use crate::drag_payload::{DragPayload, ExternalDropData};
2749 use std::path::PathBuf;
2750 use std::rc::Rc;
2751
2752 let started = Rc::new(std::cell::RefCell::new(Vec::new()));
2753 let mut ops = RecordingWindowOps {
2754 started,
2755 succeed: true,
2756 cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
2757 };
2758
2759 let mut tree = WidgetTree::new();
2760 let source = tree.add(FillWidget::new());
2761 let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
2762 tree.layout(SizeProposal::exact(200.0, 100.0));
2763 let mut ctx = crate::widget::EventContext::new();
2764 ctx.start_drag(
2765 source,
2766 DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
2767 );
2768 tree.collect_from_ctx(ctx, source);
2769 tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(super::has_outbound_typed());
2771 assert_eq!(tree.outbound_drag_source, Some(source));
2772
2773 tree.abort_outbound_drag();
2775 assert!(
2776 !super::has_outbound_typed(),
2777 "stash cleared when the source window closes"
2778 );
2779 assert!(tree.outbound_drag_source.is_none());
2780
2781 tree.begin_external_drag(
2784 Point::new(50.0, 50.0),
2785 ExternalDropData {
2786 files: vec![PathBuf::from("/tmp/real")],
2787 ..Default::default()
2788 },
2789 &mut ops,
2790 );
2791 let d = tree.active_drag.as_ref().expect("external session");
2792 assert!(
2793 d.is_external,
2794 "stale stash did not hijack the new external drag"
2795 );
2796 assert!(
2797 !d.payload.has_typed::<u32>(),
2798 "no stale typed payload leaked in"
2799 );
2800 assert_eq!(d.payload.files(), &[PathBuf::from("/tmp/real")]);
2801 }
2802
2803 #[test]
2807 fn restash_after_drag_ended_is_noop() {
2808 use crate::drag_payload::DragPayload;
2809
2810 super::outbound_begin(DragPayload::typed(1_u32).with_mime("text/plain", b"x".to_vec()));
2811 assert!(super::has_outbound_typed());
2812 let held = super::outbound_take_if_live().expect("payload taken while live");
2814 assert!(!super::has_outbound_typed());
2815 super::outbound_end();
2817 super::outbound_restash(held);
2819 assert!(
2820 !super::has_outbound_typed(),
2821 "ended drag is not resurrected by a racing re-stash"
2822 );
2823 assert!(super::outbound_take_if_live().is_none());
2825 }
2826}