1use crate::Vec2;
2use crate::color::{Brush, Color};
3use crate::geometry::Rect;
4use crate::input::{Modifiers, PointerKind};
5use crate::runtime::{Frame, HitRegion};
6use crate::shortcuts::DragAction;
7use crate::text::FontWeight;
8use crate::view::{Scene, SceneNode, TextPaintStyle};
9use std::cell::RefCell;
10use std::{any::Any, path::PathBuf, rc::Rc, sync::Arc};
11use web_time::Instant;
12
13pub type DragPayload = Rc<dyn Any>;
16
17pub fn drag_payload<T: 'static>(value: T) -> DragPayload {
23 Rc::new(value)
24}
25
26pub fn downcast_drag_payload<T: 'static>(payload: &DragPayload) -> Option<&T> {
34 payload.as_ref().downcast_ref::<T>()
35}
36
37pub fn drag_and_drop_source<T, F>(mut modifier: crate::Modifier, on_start: F) -> crate::Modifier
51where
52 T: 'static,
53 F: Fn(DragStart) -> Option<T> + 'static,
54{
55 modifier = modifier.on_drag_start(move |start| on_start(start).map(drag_payload::<T>));
56 modifier
57}
58
59pub fn drag_and_drop_source_with_preview<T, F>(
61 mut modifier: crate::Modifier,
62 preview: DragPreview,
63 on_start: F,
64) -> crate::Modifier
65where
66 T: 'static,
67 F: Fn(DragStart) -> Option<T> + 'static,
68{
69 modifier = modifier
70 .draw_drag_decoration_rc(preview)
71 .on_drag_start(move |start| on_start(start).map(drag_payload::<T>));
72 modifier
73}
74
75pub fn drag_and_drop_target<T, F>(mut modifier: crate::Modifier, on_drop: F) -> crate::Modifier
88where
89 T: 'static,
90 F: Fn(&DropEvent, &T) -> bool + 'static,
91{
92 modifier = modifier.on_drop(move |ev| match downcast_drag_payload::<T>(&ev.payload) {
93 Some(v) => on_drop(&ev, v),
94 None => false,
95 });
96 modifier
97}
98
99pub trait DragDropModifierExt: Sized {
101 fn drag_source<T>(
103 self,
104 make_payload: impl Fn(DragStart) -> Option<T> + 'static,
105 ) -> crate::Modifier
106 where
107 T: 'static;
108
109 fn on_drag_enter_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
111 where
112 T: 'static;
113
114 fn on_drag_over_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
116 where
117 T: 'static;
118
119 fn on_drag_leave_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
121 where
122 T: 'static;
123
124 fn on_drop_typed<T>(self, f: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
126 where
127 T: 'static;
128
129 fn drop_target<T>(self, on_drop: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
131 where
132 T: 'static;
133}
134
135impl DragDropModifierExt for crate::Modifier {
136 fn drag_source<T>(
137 self,
138 make_payload: impl Fn(DragStart) -> Option<T> + 'static,
139 ) -> crate::Modifier
140 where
141 T: 'static,
142 {
143 self.on_drag_start(move |start| make_payload(start).map(drag_payload::<T>))
144 }
145
146 fn on_drag_enter_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
147 where
148 T: 'static,
149 {
150 self.on_drag_enter(move |ev| {
151 if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
152 f(&ev, payload);
153 }
154 })
155 }
156
157 fn on_drag_over_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
158 where
159 T: 'static,
160 {
161 self.on_drag_over(move |ev| {
162 if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
163 f(&ev, payload);
164 }
165 })
166 }
167
168 fn on_drag_leave_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
169 where
170 T: 'static,
171 {
172 self.on_drag_leave(move |ev| {
173 if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
174 f(&ev, payload);
175 }
176 })
177 }
178
179 fn on_drop_typed<T>(self, f: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
180 where
181 T: 'static,
182 {
183 self.on_drop(move |ev| {
184 let Some(payload) = downcast_drag_payload::<T>(&ev.payload) else {
185 return false;
186 };
187 f(&ev, payload)
188 })
189 }
190
191 fn drop_target<T>(self, on_drop: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
192 where
193 T: 'static,
194 {
195 self.on_drop_typed(on_drop)
196 }
197}
198
199#[derive(Clone, Debug)]
200pub struct DragStart {
201 pub source_id: u64,
202 pub position: Vec2,
203 pub modifiers: Modifiers,
204}
205
206#[derive(Clone, Debug)]
207pub struct DragOver {
208 pub source_id: u64,
209 pub target_id: u64,
210 pub position: Vec2,
211 pub modifiers: Modifiers,
212 pub payload: DragPayload,
213}
214
215#[derive(Clone, Debug)]
216pub struct DropEvent {
217 pub source_id: u64,
218 pub target_id: u64,
219 pub position: Vec2,
220 pub modifiers: Modifiers,
221 pub payload: DragPayload,
222}
223
224#[derive(Clone, Copy, Debug)]
226pub struct DragEnd {
227 pub accepted: bool,
228}
229
230#[derive(Clone)]
232pub struct DragPreviewCtx {
233 pub pointer: Vec2,
235 pub start_pointer: Vec2,
237 pub source_rect: Rect,
239 pub grab_offset: Vec2,
241 pub payload: DragPayload,
243}
244
245pub type DragPreview = Rc<dyn Fn(&mut Scene, &DragPreviewCtx)>;
248
249pub fn provide_drag_preview(preview: DragPreview) {
258 with_dnd_state(|state| state.pending_preview = Some(preview));
259}
260
261fn take_pending_preview() -> Option<DragPreview> {
262 with_dnd_state(|state| state.pending_preview.take())
263}
264
265pub fn drag_preview_label(label: impl Into<String>, accent: Color) -> DragPreview {
267 let label = label.into();
268 Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
269 draw_label_chip(scene, ctx.pointer, &label, accent, false);
270 })
271}
272
273pub fn drag_preview_chip(label: impl Into<String>, accent: Color) -> DragPreview {
275 let label = label.into();
276 Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
277 draw_label_chip(scene, ctx.pointer, &label, accent, true);
278 })
279}
280
281fn draw_label_chip(scene: &mut Scene, pointer: Vec2, label: &str, accent: Color, elevated: bool) {
282 use crate::units::{Px, UnitExt as _};
283 let pad_x = 10.0.dp().to_px();
286 let pad_y = 6.0.dp().to_px();
287 let font_px = 13.0.sp().to_px();
288 let text_w = Px((label.chars().count() as f32 * font_px.0 * 0.55).max(24.0.dp().to_px().0));
290 let w = text_w.0 + pad_x.0 * 2.0;
291 let h = font_px.0 + pad_y.0 * 2.0;
292 let r = 8.0.dp().to_px();
293
294 let origin = Vec2 {
295 x: pointer.x + 14.0.dp().to_px().0,
296 y: pointer.y + 14.0.dp().to_px().0,
297 };
298 let rect = Rect {
299 x: origin.x,
300 y: origin.y,
301 w,
302 h,
303 };
304
305 if elevated {
306 scene.nodes.push(SceneNode::Rect {
308 rect: Rect {
309 x: rect.x + 2.0.dp().to_px().0,
310 y: rect.y + 3.0.dp().to_px().0,
311 w: rect.w,
312 h: rect.h,
313 },
314 brush: Brush::Solid(Color::from_rgba(0, 0, 0, 50)),
315 radius: [r; 4],
316 });
317 }
318
319 let bg = accent.with_alpha(0xDD);
320 scene.nodes.push(SceneNode::Rect {
321 rect,
322 brush: Brush::Solid(bg),
323 radius: [r; 4],
324 });
325 scene.nodes.push(SceneNode::Border {
326 rect,
327 brush: Brush::Solid(accent.with_alpha(0xFF)),
328 width: 1.0.dp().to_px(),
329 radius: [r; 4],
330 });
331 scene.nodes.push(SceneNode::Text {
332 rect: Rect {
333 x: rect.x + pad_x.0,
334 y: rect.y + pad_y.0,
335 w: text_w.0,
336 h: font_px.0,
337 },
338 text: Arc::<str>::from(label),
339 color: Color::WHITE,
340 size: font_px,
341 style: TextPaintStyle {
342 font_weight: FontWeight::MEDIUM,
343 ..Default::default()
344 },
345 });
346}
347
348fn draw_default_source_ghost(scene: &mut Scene, ctx: &DragPreviewCtx, accent: Color) {
350 use crate::units::UnitExt as _;
351 let w = ctx.source_rect.w.max(24.0.dp().to_px().0);
352 let h = ctx.source_rect.h.max(16.0.dp().to_px().0);
353 let rect = Rect {
354 x: ctx.pointer.x - ctx.grab_offset.x,
355 y: ctx.pointer.y - ctx.grab_offset.y,
356 w,
357 h,
358 };
359 let r = 6.0.dp().to_px();
360 scene.nodes.push(SceneNode::Rect {
361 rect,
362 brush: Brush::Solid(accent.with_alpha(0x55)),
363 radius: [r; 4],
364 });
365 scene.nodes.push(SceneNode::Border {
366 rect,
367 brush: Brush::Solid(accent.with_alpha(0xCC)),
368 width: 1.5.dp().to_px(),
369 radius: [r; 4],
370 });
371}
372
373#[derive(Clone, Debug)]
377pub struct DroppedFile {
378 pub name: String,
379 pub path: Option<PathBuf>,
380}
381
382#[derive(Clone, Debug)]
384pub struct DroppedFiles {
385 pub files: Vec<DroppedFile>,
386}
387
388#[derive(Clone)]
390pub struct DragSession {
391 pub source_id: u64,
392 pub payload: DragPayload,
393 pub start_px: (f32, f32),
394 pub over_id: Option<u64>,
395 pub source_rect: Rect,
397 pub grab_offset: Vec2,
399 pub preview: Option<DragPreview>,
401}
402
403impl std::fmt::Debug for DragSession {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 f.debug_struct("DragSession")
407 .field("source_id", &self.source_id)
408 .field("start_px", &self.start_px)
409 .field("over_id", &self.over_id)
410 .field("source_rect", &self.source_rect)
411 .field("grab_offset", &self.grab_offset)
412 .field("preview", &self.preview.as_ref().map(|_| "…"))
413 .finish_non_exhaustive()
414 }
415}
416
417#[derive(Clone)]
418struct MouseDownState {
419 position: Vec2,
420 capture_id: u64,
421}
422
423#[derive(Clone)]
424struct TouchDownState {
425 time: Instant,
426 position: Vec2,
427 capture_id: u64,
428 long_press_pending: bool,
429}
430
431const LONG_PRESS_MS: u128 = 400;
432
433#[derive(Default)]
434struct DndStorage {
435 frame: Option<Rc<Frame>>,
436 scale: f32,
437 session: Option<DragSession>,
438 mouse_down: Option<MouseDownState>,
439 touch_down: Option<TouchDownState>,
440 pending_preview: Option<DragPreview>,
441}
442
443#[derive(Clone)]
444pub struct DndContext {
445 storage: Rc<RefCell<DndStorage>>,
446}
447
448impl Default for DndContext {
449 fn default() -> Self {
450 Self {
451 storage: Rc::new(RefCell::new(DndStorage {
452 scale: 1.0,
453 ..DndStorage::default()
454 })),
455 }
456 }
457}
458
459pub struct DndContextGuard {
460 previous: Option<DndContext>,
461}
462
463impl DndContext {
464 pub fn enter(&self) -> DndContextGuard {
465 let previous = ACTIVE_DND_CONTEXT.with(|slot| slot.borrow_mut().replace(self.clone()));
466 DndContextGuard { previous }
467 }
468}
469
470impl Drop for DndContextGuard {
471 fn drop(&mut self) {
472 ACTIVE_DND_CONTEXT.with(|slot| {
473 *slot.borrow_mut() = self.previous.take();
474 });
475 }
476}
477
478thread_local! {
479 static GLOBAL_DND: RefCell<DndStorage> = RefCell::new(DndStorage {
480 scale: 1.0,
481 ..DndStorage::default()
482 });
483 static ACTIVE_DND_CONTEXT: RefCell<Option<DndContext>> = const { RefCell::new(None) };
484}
485
486fn with_dnd_state<R>(f: impl FnOnce(&mut DndStorage) -> R) -> R {
487 if let Some(context) = ACTIVE_DND_CONTEXT.with(|slot| slot.borrow().clone()) {
488 let mut storage = context.storage.borrow_mut();
489 f(&mut storage)
490 } else {
491 GLOBAL_DND.with(|storage| f(&mut storage.borrow_mut()))
492 }
493}
494
495fn dnd_frame() -> Option<Rc<Frame>> {
496 with_dnd_state(|state| state.frame.clone())
497}
498
499fn dnd_scale() -> f32 {
500 with_dnd_state(|state| state.scale)
501}
502
503fn mouse_down() -> Option<MouseDownState> {
504 with_dnd_state(|state| state.mouse_down.clone())
505}
506
507fn set_mouse_down(state: Option<MouseDownState>) {
508 with_dnd_state(|storage| storage.mouse_down = state);
509}
510
511fn touch_down() -> Option<TouchDownState> {
512 with_dnd_state(|state| state.touch_down.clone())
513}
514
515fn set_touch_down(state: Option<TouchDownState>) {
516 with_dnd_state(|storage| storage.touch_down = state);
517}
518
519fn update_touch_down(f: impl FnOnce(&mut TouchDownState)) {
520 with_dnd_state(|state| {
521 if let Some(touch) = state.touch_down.as_mut() {
522 f(touch);
523 }
524 });
525}
526
527fn with_session_mut<R>(f: impl FnOnce(&mut Option<DragSession>) -> R) -> R {
528 with_dnd_state(|state| f(&mut state.session))
529}
530
531fn take_session() -> Option<DragSession> {
532 with_dnd_state(|state| state.session.take())
533}
534
535fn set_session(session: DragSession) {
536 with_dnd_state(|state| state.session = Some(session));
537}
538
539pub fn set_dnd_frame(frame: Option<Frame>) {
541 set_shared_dnd_frame(frame.map(Rc::new));
542}
543
544pub fn set_shared_dnd_frame(frame: Option<Rc<Frame>>) {
545 with_dnd_state(|state| state.frame = frame);
546}
547
548pub fn set_dnd_scale(scale: f32) {
550 with_dnd_state(|state| state.scale = scale);
551}
552
553pub fn is_dragging() -> bool {
555 with_dnd_state(|state| state.session.is_some())
556}
557
558pub fn current_drag_session() -> Option<DragSession> {
560 with_dnd_state(|state| state.session.clone())
561}
562
563fn touch_slop_px(scale: f32) -> f32 {
564 6.0 * scale
565}
566
567fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
568 frame.hit_regions.iter().position(|h| h.id == id)
569}
570
571fn is_dnd_target(hit: &HitRegion) -> bool {
572 hit.on_drop.is_some()
573 || hit.on_drag_enter.is_some()
574 || hit.on_drag_over.is_some()
575 || hit.on_drag_leave.is_some()
576}
577
578pub fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
579 frame
580 .hit_regions
581 .iter()
582 .rev()
583 .filter(|h| h.rect.contains(pos))
584 .find(|h| is_dnd_target(h))
585 .map(|h| h.id)
586}
587
588fn dnd_update_over(frame: &Frame, session: &mut DragSession, modifiers: Modifiers, pos: Vec2) {
589 let new_over = dnd_target_id_at(frame, pos);
590
591 if new_over != session.over_id {
592 if let Some(prev) = session.over_id
593 && let Some(i) = hit_index_by_id(frame, prev)
594 && let Some(cb) = &frame.hit_regions[i].on_drag_leave
595 {
596 cb(DragOver {
597 source_id: session.source_id,
598 target_id: prev,
599 position: pos,
600 modifiers,
601 payload: session.payload.clone(),
602 });
603 }
604
605 if let Some(now) = new_over
606 && let Some(i) = hit_index_by_id(frame, now)
607 && let Some(cb) = &frame.hit_regions[i].on_drag_enter
608 {
609 cb(DragOver {
610 source_id: session.source_id,
611 target_id: now,
612 position: pos,
613 modifiers,
614 payload: session.payload.clone(),
615 });
616 }
617
618 session.over_id = new_over;
619 }
620
621 if let Some(over) = session.over_id
622 && let Some(i) = hit_index_by_id(frame, over)
623 && let Some(cb) = &frame.hit_regions[i].on_drag_over
624 {
625 cb(DragOver {
626 source_id: session.source_id,
627 target_id: over,
628 position: pos,
629 modifiers,
630 payload: session.payload.clone(),
631 });
632 }
633}
634
635fn dnd_finish(
637 frame: &Frame,
638 session: DragSession,
639 modifiers: Modifiers,
640 pos: Vec2,
641 accept_if_possible: bool,
642) -> bool {
643 let mut accepted = false;
644 if accept_if_possible {
645 let drop_target = dnd_target_id_at(frame, pos);
646 if let Some(tid) = drop_target
647 && let Some(i) = hit_index_by_id(frame, tid)
648 && let Some(cb) = &frame.hit_regions[i].on_drop
649 {
650 accepted = cb(DropEvent {
651 source_id: session.source_id,
652 target_id: tid,
653 position: pos,
654 modifiers,
655 payload: session.payload.clone(),
656 });
657 }
658 }
659
660 if let Some(i) = hit_index_by_id(frame, session.source_id)
661 && let Some(cb) = &frame.hit_regions[i].on_drag_end
662 {
663 cb(DragEnd { accepted });
664 }
665
666 accepted
667}
668
669fn initiate_drag(
670 frame: &Frame,
671 capture_id: u64,
672 start_pos: Vec2,
673 current_pos: Vec2,
674 modifiers: Modifiers,
675) -> bool {
676 let Some(i) = hit_index_by_id(frame, capture_id) else {
677 return false;
678 };
679 let Some(cb) = &frame.hit_regions[i].on_drag_start else {
680 return false;
681 };
682
683 let _ = take_pending_preview();
685
686 let payload = cb(DragStart {
687 source_id: capture_id,
688 position: current_pos,
689 modifiers,
690 });
691 let Some(payload) = payload else {
692 let _ = take_pending_preview();
693 return false;
694 };
695
696 let preview = take_pending_preview().or_else(|| frame.hit_regions[i].drag_preview.clone());
698
699 let source_rect = frame.hit_regions[i].rect;
700 let grab_offset = Vec2 {
701 x: start_pos.x - source_rect.x,
702 y: start_pos.y - source_rect.y,
703 };
704
705 set_session(DragSession {
706 source_id: capture_id,
707 payload,
708 start_px: (start_pos.x, start_pos.y),
709 over_id: None,
710 source_rect,
711 grab_offset,
712 preview,
713 });
714 true
715}
716
717pub fn handle_drag_action(action: &DragAction) -> bool {
719 let scale = dnd_scale();
720 let slop = touch_slop_px(scale);
721
722 match *action {
723 DragAction::Press {
724 position,
725 capture_id,
726 kind,
727 ..
728 } => {
729 match kind {
730 PointerKind::Mouse => {
731 set_mouse_down(Some(MouseDownState {
732 position,
733 capture_id,
734 }));
735 }
736 _ => {
737 set_touch_down(Some(TouchDownState {
738 time: web_time::Instant::now(),
739 position,
740 capture_id,
741 long_press_pending: true,
742 }));
743 }
744 }
745 false
746 }
747
748 DragAction::Move {
749 position,
750 modifiers,
751 } => {
752 if is_dragging() {
754 if let Some(frame) = dnd_frame() {
755 with_session_mut(|session| {
756 if let Some(session) = session.as_mut() {
757 dnd_update_over(&frame, session, modifiers, position);
758 }
759 });
760 }
761 return true;
762 }
763
764 if let Some(down) = mouse_down() {
766 let dx = position.x - down.position.x;
767 let dy = position.y - down.position.y;
768 let dist = (dx * dx + dy * dy).sqrt();
769 if dist >= slop {
770 if let Some(frame) = dnd_frame()
771 && initiate_drag(
772 &frame,
773 down.capture_id,
774 down.position,
775 position,
776 modifiers,
777 )
778 {
779 with_session_mut(|session| {
780 if let Some(session) = session.as_mut() {
781 dnd_update_over(&frame, session, modifiers, position);
782 }
783 });
784 set_mouse_down(None);
785 return true;
786 }
787 set_mouse_down(None);
788 }
789 return true; }
791
792 if let Some(touch) = touch_down() {
794 if touch.long_press_pending {
795 let elapsed_ms = (Instant::now() - touch.time).as_millis();
796 let dx = position.x - touch.position.x;
797 let dy = position.y - touch.position.y;
798 let dist = (dx * dx + dy * dy).sqrt();
799
800 if elapsed_ms >= LONG_PRESS_MS
801 && dist <= slop
802 && let Some(frame) = dnd_frame()
803 {
804 if initiate_drag(
805 &frame,
806 touch.capture_id,
807 touch.position,
808 position,
809 modifiers,
810 ) {
811 with_session_mut(|session| {
812 if let Some(session) = session.as_mut() {
813 dnd_update_over(&frame, session, modifiers, position);
814 }
815 });
816 set_touch_down(None);
817 return true;
818 }
819 update_touch_down(|touch| touch.long_press_pending = false);
820 }
821 if dist > slop {
822 update_touch_down(|touch| touch.long_press_pending = false);
823 }
824 }
825 let still_pending = touch_down().is_some_and(|touch| touch.long_press_pending);
828 if still_pending {
829 return true;
830 }
831 }
832
833 false
834 }
835
836 DragAction::Release {
837 position,
838 modifiers,
839 } => {
840 let mut consumed = false;
841
842 if let Some(session) = take_session() {
843 if let Some(frame) = dnd_frame() {
844 dnd_finish(&frame, session, modifiers, position, true);
845 }
846 consumed = true;
847 }
848
849 set_mouse_down(None);
850 set_touch_down(None);
851
852 consumed
853 }
854
855 DragAction::Cancel => {
856 let mut consumed = false;
857 if let Some(session) = take_session() {
858 if let Some(frame) = dnd_frame() {
859 dnd_finish(
860 &frame,
861 session,
862 Modifiers::default(),
863 Vec2::default(),
864 false,
865 );
866 }
867 consumed = true;
868 }
869 set_mouse_down(None);
870 set_touch_down(None);
871 consumed
872 }
873 }
874}
875
876pub fn overlay_drag_indicator(
879 scene: &mut Scene,
880 mouse_pos_px: (f32, f32),
881 external_file_drag: bool,
882) {
883 if !is_dragging() && !external_file_drag {
884 return;
885 }
886
887 let pos = Vec2 {
888 x: mouse_pos_px.0,
889 y: mouse_pos_px.1,
890 };
891
892 let frame = dnd_frame();
893 let Some(ref f) = frame else {
894 return;
895 };
896
897 let accent = if external_file_drag {
898 Color::from_hex("#FFAA00")
899 } else {
900 Color::from_hex("#44AAFF")
901 };
902
903 if let Some(tid) = dnd_target_id_at(f, pos)
905 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
906 {
907 use crate::units::UnitExt as _;
908 let r = 8.0.dp().to_px();
909 scene.nodes.push(SceneNode::Border {
910 rect: hit.rect,
911 brush: Brush::Solid(accent),
912 width: 2.0.dp().to_px(),
913 radius: [r; 4],
914 });
915 }
916
917 if external_file_drag {
918 draw_label_chip(scene, pos, "Drop files", accent, true);
919 return;
920 }
921
922 let Some(session) = current_drag_session() else {
923 return;
924 };
925 let ctx = DragPreviewCtx {
926 pointer: pos,
927 start_pointer: Vec2 {
928 x: session.start_px.0,
929 y: session.start_px.1,
930 },
931 source_rect: session.source_rect,
932 grab_offset: session.grab_offset,
933 payload: session.payload.clone(),
934 };
935
936 if let Some(preview) = session.preview {
937 preview(scene, &ctx);
938 } else {
939 draw_default_source_ghost(scene, &ctx, accent);
940 }
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946 use crate::input::PointerKind;
947 use crate::runtime::HitRegion;
948
949 #[test]
950 fn contexts_isolate_drag_sessions() {
951 let frame = Frame {
952 scene: Scene::default(),
953 hit_regions: vec![HitRegion {
954 id: 1,
955 rect: Rect {
956 x: 0.0,
957 y: 0.0,
958 w: 100.0,
959 h: 100.0,
960 },
961 on_drag_start: Some(Rc::new(|_| Some(drag_payload(7u32)))),
962 ..Default::default()
963 }],
964 semantics_nodes: Vec::new(),
965 focus_chain: Vec::new(),
966 };
967 let a = DndContext::default();
968 let b = DndContext::default();
969 {
970 let _guard = a.enter();
971 set_dnd_frame(Some(frame.clone()));
972 handle_drag_action(&DragAction::Press {
973 position: Vec2 { x: 10.0, y: 10.0 },
974 capture_id: 1,
975 kind: PointerKind::Mouse,
976 modifiers: Modifiers::default(),
977 });
978 handle_drag_action(&DragAction::Move {
979 position: Vec2 { x: 40.0, y: 40.0 },
980 modifiers: Modifiers::default(),
981 });
982 assert!(is_dragging());
983 }
984 {
985 let _guard = b.enter();
986 assert!(!is_dragging());
987 set_dnd_frame(Some(frame.clone()));
988 }
989 {
990 let _guard = a.enter();
991 assert!(is_dragging());
992 handle_drag_action(&DragAction::Cancel);
993 assert!(!is_dragging());
994 }
995 }
996}