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::{FontStyle, FontWeight, TextAlign, TextDecoration};
8use crate::view::{Scene, SceneNode};
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 font_family: None,
342 text_align: TextAlign::Unspecified,
343 font_weight: FontWeight::MEDIUM,
344 font_style: FontStyle::Normal,
345 text_decoration: TextDecoration::default(),
346 letter_spacing: Px::ZERO,
347 line_height: Px::ZERO,
348 extra_style: Default::default(),
349 url: None,
350 font_variation_settings: None,
351 });
352}
353
354fn draw_default_source_ghost(scene: &mut Scene, ctx: &DragPreviewCtx, accent: Color) {
356 use crate::units::UnitExt as _;
357 let w = ctx.source_rect.w.max(24.0.dp().to_px().0);
358 let h = ctx.source_rect.h.max(16.0.dp().to_px().0);
359 let rect = Rect {
360 x: ctx.pointer.x - ctx.grab_offset.x,
361 y: ctx.pointer.y - ctx.grab_offset.y,
362 w,
363 h,
364 };
365 let r = 6.0.dp().to_px();
366 scene.nodes.push(SceneNode::Rect {
367 rect,
368 brush: Brush::Solid(accent.with_alpha(0x55)),
369 radius: [r; 4],
370 });
371 scene.nodes.push(SceneNode::Border {
372 rect,
373 brush: Brush::Solid(accent.with_alpha(0xCC)),
374 width: 1.5.dp().to_px(),
375 radius: [r; 4],
376 });
377}
378
379#[derive(Clone, Debug)]
383pub struct DroppedFile {
384 pub name: String,
385 pub path: Option<PathBuf>,
386}
387
388#[derive(Clone, Debug)]
390pub struct DroppedFiles {
391 pub files: Vec<DroppedFile>,
392}
393
394#[derive(Clone)]
396pub struct DragSession {
397 pub source_id: u64,
398 pub payload: DragPayload,
399 pub start_px: (f32, f32),
400 pub over_id: Option<u64>,
401 pub source_rect: Rect,
403 pub grab_offset: Vec2,
405 pub preview: Option<DragPreview>,
407}
408
409impl std::fmt::Debug for DragSession {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 f.debug_struct("DragSession")
413 .field("source_id", &self.source_id)
414 .field("start_px", &self.start_px)
415 .field("over_id", &self.over_id)
416 .field("source_rect", &self.source_rect)
417 .field("grab_offset", &self.grab_offset)
418 .field("preview", &self.preview.as_ref().map(|_| "…"))
419 .finish_non_exhaustive()
420 }
421}
422
423#[derive(Clone)]
424struct MouseDownState {
425 position: Vec2,
426 capture_id: u64,
427}
428
429#[derive(Clone)]
430struct TouchDownState {
431 time: Instant,
432 position: Vec2,
433 capture_id: u64,
434 long_press_pending: bool,
435}
436
437const LONG_PRESS_MS: u128 = 400;
438
439#[derive(Default)]
440struct DndStorage {
441 frame: Option<Rc<Frame>>,
442 scale: f32,
443 session: Option<DragSession>,
444 mouse_down: Option<MouseDownState>,
445 touch_down: Option<TouchDownState>,
446 pending_preview: Option<DragPreview>,
447}
448
449#[derive(Clone)]
450pub struct DndContext {
451 storage: Rc<RefCell<DndStorage>>,
452}
453
454impl Default for DndContext {
455 fn default() -> Self {
456 Self {
457 storage: Rc::new(RefCell::new(DndStorage {
458 scale: 1.0,
459 ..DndStorage::default()
460 })),
461 }
462 }
463}
464
465pub struct DndContextGuard {
466 previous: Option<DndContext>,
467}
468
469impl DndContext {
470 pub fn enter(&self) -> DndContextGuard {
471 let previous = ACTIVE_DND_CONTEXT.with(|slot| slot.borrow_mut().replace(self.clone()));
472 DndContextGuard { previous }
473 }
474}
475
476impl Drop for DndContextGuard {
477 fn drop(&mut self) {
478 ACTIVE_DND_CONTEXT.with(|slot| {
479 *slot.borrow_mut() = self.previous.take();
480 });
481 }
482}
483
484thread_local! {
485 static GLOBAL_DND: RefCell<DndStorage> = RefCell::new(DndStorage {
486 scale: 1.0,
487 ..DndStorage::default()
488 });
489 static ACTIVE_DND_CONTEXT: RefCell<Option<DndContext>> = const { RefCell::new(None) };
490}
491
492fn with_dnd_state<R>(f: impl FnOnce(&mut DndStorage) -> R) -> R {
493 if let Some(context) = ACTIVE_DND_CONTEXT.with(|slot| slot.borrow().clone()) {
494 let mut storage = context.storage.borrow_mut();
495 f(&mut storage)
496 } else {
497 GLOBAL_DND.with(|storage| f(&mut storage.borrow_mut()))
498 }
499}
500
501fn dnd_frame() -> Option<Rc<Frame>> {
502 with_dnd_state(|state| state.frame.clone())
503}
504
505fn dnd_scale() -> f32 {
506 with_dnd_state(|state| state.scale)
507}
508
509fn mouse_down() -> Option<MouseDownState> {
510 with_dnd_state(|state| state.mouse_down.clone())
511}
512
513fn set_mouse_down(state: Option<MouseDownState>) {
514 with_dnd_state(|storage| storage.mouse_down = state);
515}
516
517fn touch_down() -> Option<TouchDownState> {
518 with_dnd_state(|state| state.touch_down.clone())
519}
520
521fn set_touch_down(state: Option<TouchDownState>) {
522 with_dnd_state(|storage| storage.touch_down = state);
523}
524
525fn update_touch_down(f: impl FnOnce(&mut TouchDownState)) {
526 with_dnd_state(|state| {
527 if let Some(touch) = state.touch_down.as_mut() {
528 f(touch);
529 }
530 });
531}
532
533fn with_session_mut<R>(f: impl FnOnce(&mut Option<DragSession>) -> R) -> R {
534 with_dnd_state(|state| f(&mut state.session))
535}
536
537fn take_session() -> Option<DragSession> {
538 with_dnd_state(|state| state.session.take())
539}
540
541fn set_session(session: DragSession) {
542 with_dnd_state(|state| state.session = Some(session));
543}
544
545pub fn set_dnd_frame(frame: Option<Frame>) {
547 set_shared_dnd_frame(frame.map(Rc::new));
548}
549
550pub fn set_shared_dnd_frame(frame: Option<Rc<Frame>>) {
551 with_dnd_state(|state| state.frame = frame);
552}
553
554pub fn set_dnd_scale(scale: f32) {
556 with_dnd_state(|state| state.scale = scale);
557}
558
559pub fn is_dragging() -> bool {
561 with_dnd_state(|state| state.session.is_some())
562}
563
564pub fn current_drag_session() -> Option<DragSession> {
566 with_dnd_state(|state| state.session.clone())
567}
568
569fn touch_slop_px(scale: f32) -> f32 {
570 6.0 * scale
571}
572
573fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
574 frame.hit_regions.iter().position(|h| h.id == id)
575}
576
577fn is_dnd_target(hit: &HitRegion) -> bool {
578 hit.on_drop.is_some()
579 || hit.on_drag_enter.is_some()
580 || hit.on_drag_over.is_some()
581 || hit.on_drag_leave.is_some()
582}
583
584pub fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
585 frame
586 .hit_regions
587 .iter()
588 .rev()
589 .filter(|h| h.rect.contains(pos))
590 .find(|h| is_dnd_target(h))
591 .map(|h| h.id)
592}
593
594fn dnd_update_over(frame: &Frame, session: &mut DragSession, modifiers: Modifiers, pos: Vec2) {
595 let new_over = dnd_target_id_at(frame, pos);
596
597 if new_over != session.over_id {
598 if let Some(prev) = session.over_id
599 && let Some(i) = hit_index_by_id(frame, prev)
600 && let Some(cb) = &frame.hit_regions[i].on_drag_leave
601 {
602 cb(DragOver {
603 source_id: session.source_id,
604 target_id: prev,
605 position: pos,
606 modifiers,
607 payload: session.payload.clone(),
608 });
609 }
610
611 if let Some(now) = new_over
612 && let Some(i) = hit_index_by_id(frame, now)
613 && let Some(cb) = &frame.hit_regions[i].on_drag_enter
614 {
615 cb(DragOver {
616 source_id: session.source_id,
617 target_id: now,
618 position: pos,
619 modifiers,
620 payload: session.payload.clone(),
621 });
622 }
623
624 session.over_id = new_over;
625 }
626
627 if let Some(over) = session.over_id
628 && let Some(i) = hit_index_by_id(frame, over)
629 && let Some(cb) = &frame.hit_regions[i].on_drag_over
630 {
631 cb(DragOver {
632 source_id: session.source_id,
633 target_id: over,
634 position: pos,
635 modifiers,
636 payload: session.payload.clone(),
637 });
638 }
639}
640
641fn dnd_finish(
643 frame: &Frame,
644 session: DragSession,
645 modifiers: Modifiers,
646 pos: Vec2,
647 accept_if_possible: bool,
648) -> bool {
649 let mut accepted = false;
650 if accept_if_possible {
651 let drop_target = dnd_target_id_at(frame, pos);
652 if let Some(tid) = drop_target
653 && let Some(i) = hit_index_by_id(frame, tid)
654 && let Some(cb) = &frame.hit_regions[i].on_drop
655 {
656 accepted = cb(DropEvent {
657 source_id: session.source_id,
658 target_id: tid,
659 position: pos,
660 modifiers,
661 payload: session.payload.clone(),
662 });
663 }
664 }
665
666 if let Some(i) = hit_index_by_id(frame, session.source_id)
667 && let Some(cb) = &frame.hit_regions[i].on_drag_end
668 {
669 cb(DragEnd { accepted });
670 }
671
672 accepted
673}
674
675fn initiate_drag(
676 frame: &Frame,
677 capture_id: u64,
678 start_pos: Vec2,
679 current_pos: Vec2,
680 modifiers: Modifiers,
681) -> bool {
682 let Some(i) = hit_index_by_id(frame, capture_id) else {
683 return false;
684 };
685 let Some(cb) = &frame.hit_regions[i].on_drag_start else {
686 return false;
687 };
688
689 let _ = take_pending_preview();
691
692 let payload = cb(DragStart {
693 source_id: capture_id,
694 position: current_pos,
695 modifiers,
696 });
697 let Some(payload) = payload else {
698 let _ = take_pending_preview();
699 return false;
700 };
701
702 let preview = take_pending_preview().or_else(|| frame.hit_regions[i].drag_preview.clone());
704
705 let source_rect = frame.hit_regions[i].rect;
706 let grab_offset = Vec2 {
707 x: start_pos.x - source_rect.x,
708 y: start_pos.y - source_rect.y,
709 };
710
711 set_session(DragSession {
712 source_id: capture_id,
713 payload,
714 start_px: (start_pos.x, start_pos.y),
715 over_id: None,
716 source_rect,
717 grab_offset,
718 preview,
719 });
720 true
721}
722
723pub fn handle_drag_action(action: &DragAction) -> bool {
725 let scale = dnd_scale();
726 let slop = touch_slop_px(scale);
727
728 match *action {
729 DragAction::Press {
730 position,
731 capture_id,
732 kind,
733 ..
734 } => {
735 match kind {
736 PointerKind::Mouse => {
737 set_mouse_down(Some(MouseDownState {
738 position,
739 capture_id,
740 }));
741 }
742 _ => {
743 set_touch_down(Some(TouchDownState {
744 time: web_time::Instant::now(),
745 position,
746 capture_id,
747 long_press_pending: true,
748 }));
749 }
750 }
751 false
752 }
753
754 DragAction::Move {
755 position,
756 modifiers,
757 } => {
758 if is_dragging() {
760 if let Some(frame) = dnd_frame() {
761 with_session_mut(|session| {
762 if let Some(session) = session.as_mut() {
763 dnd_update_over(&frame, session, modifiers, position);
764 }
765 });
766 }
767 return true;
768 }
769
770 if let Some(down) = mouse_down() {
772 let dx = position.x - down.position.x;
773 let dy = position.y - down.position.y;
774 let dist = (dx * dx + dy * dy).sqrt();
775 if dist >= slop {
776 if let Some(frame) = dnd_frame()
777 && initiate_drag(
778 &frame,
779 down.capture_id,
780 down.position,
781 position,
782 modifiers,
783 )
784 {
785 with_session_mut(|session| {
786 if let Some(session) = session.as_mut() {
787 dnd_update_over(&frame, session, modifiers, position);
788 }
789 });
790 set_mouse_down(None);
791 return true;
792 }
793 set_mouse_down(None);
794 }
795 return true; }
797
798 if let Some(touch) = touch_down() {
800 if touch.long_press_pending {
801 let elapsed_ms = (Instant::now() - touch.time).as_millis();
802 let dx = position.x - touch.position.x;
803 let dy = position.y - touch.position.y;
804 let dist = (dx * dx + dy * dy).sqrt();
805
806 if elapsed_ms >= LONG_PRESS_MS
807 && dist <= slop
808 && let Some(frame) = dnd_frame()
809 {
810 if initiate_drag(
811 &frame,
812 touch.capture_id,
813 touch.position,
814 position,
815 modifiers,
816 ) {
817 with_session_mut(|session| {
818 if let Some(session) = session.as_mut() {
819 dnd_update_over(&frame, session, modifiers, position);
820 }
821 });
822 set_touch_down(None);
823 return true;
824 }
825 update_touch_down(|touch| touch.long_press_pending = false);
826 }
827 if dist > slop {
828 update_touch_down(|touch| touch.long_press_pending = false);
829 }
830 }
831 let still_pending = touch_down().is_some_and(|touch| touch.long_press_pending);
834 if still_pending {
835 return true;
836 }
837 }
838
839 false
840 }
841
842 DragAction::Release {
843 position,
844 modifiers,
845 } => {
846 let mut consumed = false;
847
848 if let Some(session) = take_session() {
849 if let Some(frame) = dnd_frame() {
850 dnd_finish(&frame, session, modifiers, position, true);
851 }
852 consumed = true;
853 }
854
855 set_mouse_down(None);
856 set_touch_down(None);
857
858 consumed
859 }
860
861 DragAction::Cancel => {
862 let mut consumed = false;
863 if let Some(session) = take_session() {
864 if let Some(frame) = dnd_frame() {
865 dnd_finish(
866 &frame,
867 session,
868 Modifiers::default(),
869 Vec2::default(),
870 false,
871 );
872 }
873 consumed = true;
874 }
875 set_mouse_down(None);
876 set_touch_down(None);
877 consumed
878 }
879 }
880}
881
882pub fn overlay_drag_indicator(
885 scene: &mut Scene,
886 mouse_pos_px: (f32, f32),
887 external_file_drag: bool,
888) {
889 if !is_dragging() && !external_file_drag {
890 return;
891 }
892
893 let pos = Vec2 {
894 x: mouse_pos_px.0,
895 y: mouse_pos_px.1,
896 };
897
898 let frame = dnd_frame();
899 let Some(ref f) = frame else {
900 return;
901 };
902
903 let accent = if external_file_drag {
904 Color::from_hex("#FFAA00")
905 } else {
906 Color::from_hex("#44AAFF")
907 };
908
909 if let Some(tid) = dnd_target_id_at(f, pos)
911 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
912 {
913 use crate::units::UnitExt as _;
914 let r = 8.0.dp().to_px();
915 scene.nodes.push(SceneNode::Border {
916 rect: hit.rect,
917 brush: Brush::Solid(accent),
918 width: 2.0.dp().to_px(),
919 radius: [r; 4],
920 });
921 }
922
923 if external_file_drag {
924 draw_label_chip(scene, pos, "Drop files", accent, true);
925 return;
926 }
927
928 let Some(session) = current_drag_session() else {
929 return;
930 };
931 let ctx = DragPreviewCtx {
932 pointer: pos,
933 start_pointer: Vec2 {
934 x: session.start_px.0,
935 y: session.start_px.1,
936 },
937 source_rect: session.source_rect,
938 grab_offset: session.grab_offset,
939 payload: session.payload.clone(),
940 };
941
942 if let Some(preview) = session.preview {
943 preview(scene, &ctx);
944 } else {
945 draw_default_source_ghost(scene, &ctx, accent);
946 }
947}
948
949#[cfg(test)]
950mod tests {
951 use super::*;
952 use crate::input::PointerKind;
953 use crate::runtime::HitRegion;
954
955 #[test]
956 fn contexts_isolate_drag_sessions() {
957 let frame = Frame {
958 scene: Scene::default(),
959 hit_regions: vec![HitRegion {
960 id: 1,
961 rect: Rect {
962 x: 0.0,
963 y: 0.0,
964 w: 100.0,
965 h: 100.0,
966 },
967 on_drag_start: Some(Rc::new(|_| Some(drag_payload(7u32)))),
968 ..Default::default()
969 }],
970 semantics_nodes: Vec::new(),
971 focus_chain: Vec::new(),
972 };
973 let a = DndContext::default();
974 let b = DndContext::default();
975 {
976 let _guard = a.enter();
977 set_dnd_frame(Some(frame.clone()));
978 handle_drag_action(&DragAction::Press {
979 position: Vec2 { x: 10.0, y: 10.0 },
980 capture_id: 1,
981 kind: PointerKind::Mouse,
982 modifiers: Modifiers::default(),
983 });
984 handle_drag_action(&DragAction::Move {
985 position: Vec2 { x: 40.0, y: 40.0 },
986 modifiers: Modifiers::default(),
987 });
988 assert!(is_dragging());
989 }
990 {
991 let _guard = b.enter();
992 assert!(!is_dragging());
993 set_dnd_frame(Some(frame.clone()));
994 }
995 {
996 let _guard = a.enter();
997 assert!(is_dragging());
998 handle_drag_action(&DragAction::Cancel);
999 assert!(!is_dragging());
1000 }
1001 }
1002}