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
249thread_local! {
251 static PENDING_PREVIEW: RefCell<Option<DragPreview>> = const { RefCell::new(None) };
252}
253
254pub fn provide_drag_preview(preview: DragPreview) {
263 PENDING_PREVIEW.with(|p| *p.borrow_mut() = Some(preview));
264}
265
266fn take_pending_preview() -> Option<DragPreview> {
267 PENDING_PREVIEW.with(|p| p.borrow_mut().take())
268}
269
270pub fn drag_preview_label(label: impl Into<String>, accent: Color) -> DragPreview {
272 let label = label.into();
273 Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
274 draw_label_chip(scene, ctx.pointer, &label, accent, false);
275 })
276}
277
278pub fn drag_preview_chip(label: impl Into<String>, accent: Color) -> DragPreview {
280 let label = label.into();
281 Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
282 draw_label_chip(scene, ctx.pointer, &label, accent, true);
283 })
284}
285
286fn draw_label_chip(scene: &mut Scene, pointer: Vec2, label: &str, accent: Color, elevated: bool) {
287 use crate::units::{Px, UnitExt as _};
288 let pad_x = 10.0.dp().to_px();
291 let pad_y = 6.0.dp().to_px();
292 let font_px = 13.0.sp().to_px();
293 let text_w = Px((label.chars().count() as f32 * font_px.0 * 0.55).max(24.0.dp().to_px().0));
295 let w = text_w.0 + pad_x.0 * 2.0;
296 let h = font_px.0 + pad_y.0 * 2.0;
297 let r = 8.0.dp().to_px();
298
299 let origin = Vec2 {
300 x: pointer.x + 14.0.dp().to_px().0,
301 y: pointer.y + 14.0.dp().to_px().0,
302 };
303 let rect = Rect {
304 x: origin.x,
305 y: origin.y,
306 w,
307 h,
308 };
309
310 if elevated {
311 scene.nodes.push(SceneNode::Rect {
313 rect: Rect {
314 x: rect.x + 2.0.dp().to_px().0,
315 y: rect.y + 3.0.dp().to_px().0,
316 w: rect.w,
317 h: rect.h,
318 },
319 brush: Brush::Solid(Color::from_rgba(0, 0, 0, 50)),
320 radius: [r; 4],
321 });
322 }
323
324 let bg = accent.with_alpha(0xDD);
325 scene.nodes.push(SceneNode::Rect {
326 rect,
327 brush: Brush::Solid(bg),
328 radius: [r; 4],
329 });
330 scene.nodes.push(SceneNode::Border {
331 rect,
332 color: accent.with_alpha(0xFF),
333 width: 1.0.dp().to_px(),
334 radius: [r; 4],
335 });
336 scene.nodes.push(SceneNode::Text {
337 rect: Rect {
338 x: rect.x + pad_x.0,
339 y: rect.y + pad_y.0,
340 w: text_w.0,
341 h: font_px.0,
342 },
343 text: Arc::<str>::from(label),
344 color: Color::WHITE,
345 size: font_px,
346 font_family: None,
347 text_align: TextAlign::Unspecified,
348 font_weight: FontWeight::MEDIUM,
349 font_style: FontStyle::Normal,
350 text_decoration: TextDecoration::default(),
351 letter_spacing: Px::ZERO,
352 line_height: Px::ZERO,
353 extra_style: Default::default(),
354 url: None,
355 font_variation_settings: None,
356 });
357}
358
359fn draw_default_source_ghost(scene: &mut Scene, ctx: &DragPreviewCtx, accent: Color) {
361 use crate::units::UnitExt as _;
362 let w = ctx.source_rect.w.max(24.0.dp().to_px().0);
363 let h = ctx.source_rect.h.max(16.0.dp().to_px().0);
364 let rect = Rect {
365 x: ctx.pointer.x - ctx.grab_offset.x,
366 y: ctx.pointer.y - ctx.grab_offset.y,
367 w,
368 h,
369 };
370 let r = 6.0.dp().to_px();
371 scene.nodes.push(SceneNode::Rect {
372 rect,
373 brush: Brush::Solid(accent.with_alpha(0x55)),
374 radius: [r; 4],
375 });
376 scene.nodes.push(SceneNode::Border {
377 rect,
378 color: accent.with_alpha(0xCC),
379 width: 1.5.dp().to_px(),
380 radius: [r; 4],
381 });
382}
383
384#[derive(Clone, Debug)]
388pub struct DroppedFile {
389 pub name: String,
390 pub path: Option<PathBuf>,
391}
392
393#[derive(Clone, Debug)]
395pub struct DroppedFiles {
396 pub files: Vec<DroppedFile>,
397}
398
399#[derive(Clone)]
401pub struct DragSession {
402 pub source_id: u64,
403 pub payload: DragPayload,
404 pub start_px: (f32, f32),
405 pub over_id: Option<u64>,
406 pub source_rect: Rect,
408 pub grab_offset: Vec2,
410 pub preview: Option<DragPreview>,
412}
413
414impl std::fmt::Debug for DragSession {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 f.debug_struct("DragSession")
418 .field("source_id", &self.source_id)
419 .field("start_px", &self.start_px)
420 .field("over_id", &self.over_id)
421 .field("source_rect", &self.source_rect)
422 .field("grab_offset", &self.grab_offset)
423 .field("preview", &self.preview.as_ref().map(|_| "…"))
424 .finish_non_exhaustive()
425 }
426}
427
428#[derive(Clone)]
429struct MouseDownState {
430 position: Vec2,
431 capture_id: u64,
432}
433
434#[derive(Clone)]
435struct TouchDownState {
436 time: Instant,
437 position: Vec2,
438 capture_id: u64,
439 long_press_pending: bool,
440}
441
442const LONG_PRESS_MS: u128 = 400;
443
444thread_local! {
445 static DND_FRAME: RefCell<Option<Frame>> = const { RefCell::new(None) };
446 static DND_SCALE: RefCell<f32> = const { RefCell::new(1.0) };
447 static DND_SESSION: RefCell<Option<DragSession>> = const { RefCell::new(None) };
448 static DND_MOUSE_DOWN: RefCell<Option<MouseDownState>> = const { RefCell::new(None) };
449 static DND_TOUCH_DOWN: RefCell<Option<TouchDownState>> = const { RefCell::new(None) };
450}
451
452pub fn set_dnd_frame(frame: Option<Frame>) {
454 DND_FRAME.with(|f| *f.borrow_mut() = frame);
455}
456
457pub fn set_dnd_scale(scale: f32) {
459 DND_SCALE.with(|s| *s.borrow_mut() = scale);
460}
461
462pub fn is_dragging() -> bool {
464 DND_SESSION.with(|s| s.borrow().is_some())
465}
466
467pub fn current_drag_session() -> Option<DragSession> {
469 DND_SESSION.with(|s| s.borrow().clone())
470}
471
472fn touch_slop_px(scale: f32) -> f32 {
473 6.0 * scale
474}
475
476fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
477 frame.hit_regions.iter().position(|h| h.id == id)
478}
479
480fn is_dnd_target(hit: &HitRegion) -> bool {
481 hit.on_drop.is_some()
482 || hit.on_drag_enter.is_some()
483 || hit.on_drag_over.is_some()
484 || hit.on_drag_leave.is_some()
485}
486
487pub fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
488 frame
489 .hit_regions
490 .iter()
491 .rev()
492 .filter(|h| h.rect.contains(pos))
493 .find(|h| is_dnd_target(h))
494 .map(|h| h.id)
495}
496
497fn dnd_update_over(frame: &Frame, session: &mut DragSession, modifiers: Modifiers, pos: Vec2) {
498 let new_over = dnd_target_id_at(frame, pos);
499
500 if new_over != session.over_id {
501 if let Some(prev) = session.over_id
502 && let Some(i) = hit_index_by_id(frame, prev)
503 && let Some(cb) = &frame.hit_regions[i].on_drag_leave
504 {
505 cb(DragOver {
506 source_id: session.source_id,
507 target_id: prev,
508 position: pos,
509 modifiers,
510 payload: session.payload.clone(),
511 });
512 }
513
514 if let Some(now) = new_over
515 && let Some(i) = hit_index_by_id(frame, now)
516 && let Some(cb) = &frame.hit_regions[i].on_drag_enter
517 {
518 cb(DragOver {
519 source_id: session.source_id,
520 target_id: now,
521 position: pos,
522 modifiers,
523 payload: session.payload.clone(),
524 });
525 }
526
527 session.over_id = new_over;
528 }
529
530 if let Some(over) = session.over_id
531 && let Some(i) = hit_index_by_id(frame, over)
532 && let Some(cb) = &frame.hit_regions[i].on_drag_over
533 {
534 cb(DragOver {
535 source_id: session.source_id,
536 target_id: over,
537 position: pos,
538 modifiers,
539 payload: session.payload.clone(),
540 });
541 }
542}
543
544fn dnd_finish(
546 frame: &Frame,
547 session: DragSession,
548 modifiers: Modifiers,
549 pos: Vec2,
550 accept_if_possible: bool,
551) -> bool {
552 let mut accepted = false;
553 if accept_if_possible {
554 let drop_target = dnd_target_id_at(frame, pos);
555 if let Some(tid) = drop_target
556 && let Some(i) = hit_index_by_id(frame, tid)
557 && let Some(cb) = &frame.hit_regions[i].on_drop
558 {
559 accepted = cb(DropEvent {
560 source_id: session.source_id,
561 target_id: tid,
562 position: pos,
563 modifiers,
564 payload: session.payload.clone(),
565 });
566 }
567 }
568
569 if let Some(i) = hit_index_by_id(frame, session.source_id)
570 && let Some(cb) = &frame.hit_regions[i].on_drag_end
571 {
572 cb(DragEnd { accepted });
573 }
574
575 accepted
576}
577
578fn initiate_drag(
579 frame: &Frame,
580 capture_id: u64,
581 start_pos: Vec2,
582 current_pos: Vec2,
583 modifiers: Modifiers,
584) -> bool {
585 let Some(i) = hit_index_by_id(frame, capture_id) else {
586 return false;
587 };
588 let Some(cb) = &frame.hit_regions[i].on_drag_start else {
589 return false;
590 };
591
592 let _ = take_pending_preview();
594
595 let payload = cb(DragStart {
596 source_id: capture_id,
597 position: current_pos,
598 modifiers,
599 });
600 let Some(payload) = payload else {
601 let _ = take_pending_preview();
602 return false;
603 };
604
605 let preview = take_pending_preview().or_else(|| frame.hit_regions[i].drag_preview.clone());
607
608 let source_rect = frame.hit_regions[i].rect;
609 let grab_offset = Vec2 {
610 x: start_pos.x - source_rect.x,
611 y: start_pos.y - source_rect.y,
612 };
613
614 DND_SESSION.with(|s| {
615 *s.borrow_mut() = Some(DragSession {
616 source_id: capture_id,
617 payload,
618 start_px: (start_pos.x, start_pos.y),
619 over_id: None,
620 source_rect,
621 grab_offset,
622 preview,
623 });
624 });
625 true
626}
627
628pub fn handle_drag_action(action: &DragAction) -> bool {
630 let scale = DND_SCALE.with(|s| *s.borrow());
631 let slop = touch_slop_px(scale);
632
633 match *action {
634 DragAction::Press {
635 position,
636 capture_id,
637 kind,
638 ..
639 } => {
640 match kind {
641 PointerKind::Mouse => {
642 DND_MOUSE_DOWN.with(|m| {
643 *m.borrow_mut() = Some(MouseDownState {
644 position,
645 capture_id,
646 });
647 });
648 }
649 _ => {
650 DND_TOUCH_DOWN.with(|t| {
652 *t.borrow_mut() = Some(TouchDownState {
653 time: web_time::Instant::now(),
654 position,
655 capture_id,
656 long_press_pending: true,
657 });
658 });
659 }
660 }
661 false
662 }
663
664 DragAction::Move {
665 position,
666 modifiers,
667 } => {
668 if DND_SESSION.with(|s| s.borrow().is_some()) {
670 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
671 DND_SESSION.with(|s| {
672 if let Some(ref mut session) = *s.borrow_mut() {
673 dnd_update_over(&frame, session, modifiers, position);
674 }
675 });
676 }
677 return true;
678 }
679
680 if let Some(down) = DND_MOUSE_DOWN.with(|m| m.borrow().clone()) {
682 let dx = position.x - down.position.x;
683 let dy = position.y - down.position.y;
684 let dist = (dx * dx + dy * dy).sqrt();
685 if dist >= slop {
686 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone())
687 && initiate_drag(
688 &frame,
689 down.capture_id,
690 down.position,
691 position,
692 modifiers,
693 )
694 {
695 DND_SESSION.with(|s| {
696 if let Some(ref mut session) = *s.borrow_mut() {
697 dnd_update_over(&frame, session, modifiers, position);
698 }
699 });
700 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
701 return true;
702 }
703 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
706 }
707 return true; }
709
710 if let Some(touch) = DND_TOUCH_DOWN.with(|t| t.borrow().clone()) {
712 if touch.long_press_pending {
713 let elapsed_ms = (Instant::now() - touch.time).as_millis();
714 let dx = position.x - touch.position.x;
715 let dy = position.y - touch.position.y;
716 let dist = (dx * dx + dy * dy).sqrt();
717
718 if elapsed_ms >= LONG_PRESS_MS
719 && dist <= slop
720 && let Some(frame) = DND_FRAME.with(|f| f.borrow().clone())
721 {
722 if initiate_drag(
723 &frame,
724 touch.capture_id,
725 touch.position,
726 position,
727 modifiers,
728 ) {
729 DND_SESSION.with(|s| {
730 if let Some(ref mut session) = *s.borrow_mut() {
731 dnd_update_over(&frame, session, modifiers, position);
732 }
733 });
734 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
735 return true;
736 }
737 DND_TOUCH_DOWN.with(|t| {
739 if let Some(ref mut td) = *t.borrow_mut() {
740 td.long_press_pending = false;
741 }
742 });
743 }
744 if dist > slop {
745 DND_TOUCH_DOWN.with(|t| {
746 if let Some(ref mut td) = *t.borrow_mut() {
747 td.long_press_pending = false;
748 }
749 });
750 }
751 }
752 let still_pending = DND_TOUCH_DOWN.with(|t| {
755 t.borrow()
756 .as_ref()
757 .map(|td| td.long_press_pending)
758 .unwrap_or(false)
759 });
760 if still_pending {
761 return true;
762 }
763 }
764
765 false
766 }
767
768 DragAction::Release {
769 position,
770 modifiers,
771 } => {
772 let mut consumed = false;
773
774 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
775 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
776 dnd_finish(&frame, session, modifiers, position, true);
777 }
778 consumed = true;
779 }
780
781 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
782 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
783
784 consumed
785 }
786
787 DragAction::Cancel => {
788 let mut consumed = false;
789 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
790 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
791 dnd_finish(
792 &frame,
793 session,
794 Modifiers::default(),
795 Vec2::default(),
796 false,
797 );
798 }
799 consumed = true;
800 }
801 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
802 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
803 consumed
804 }
805 }
806}
807
808pub fn overlay_drag_indicator(
811 scene: &mut Scene,
812 mouse_pos_px: (f32, f32),
813 external_file_drag: bool,
814) {
815 if !is_dragging() && !external_file_drag {
816 return;
817 }
818
819 let pos = Vec2 {
820 x: mouse_pos_px.0,
821 y: mouse_pos_px.1,
822 };
823
824 let frame = DND_FRAME.with(|f| f.borrow().clone());
825 let Some(ref f) = frame else {
826 return;
827 };
828
829 let accent = if external_file_drag {
830 Color::from_hex("#FFAA00")
831 } else {
832 Color::from_hex("#44AAFF")
833 };
834
835 if let Some(tid) = dnd_target_id_at(f, pos)
837 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
838 {
839 use crate::units::UnitExt as _;
840 let r = 8.0.dp().to_px();
841 scene.nodes.push(SceneNode::Border {
842 rect: hit.rect,
843 color: accent,
844 width: 2.0.dp().to_px(),
845 radius: [r; 4],
846 });
847 }
848
849 if external_file_drag {
850 draw_label_chip(scene, pos, "Drop files", accent, true);
851 return;
852 }
853
854 DND_SESSION.with(|s| {
855 let session = s.borrow();
856 let Some(ref session) = *session else {
857 return;
858 };
859
860 let ctx = DragPreviewCtx {
861 pointer: pos,
862 start_pointer: Vec2 {
863 x: session.start_px.0,
864 y: session.start_px.1,
865 },
866 source_rect: session.source_rect,
867 grab_offset: session.grab_offset,
868 payload: session.payload.clone(),
869 };
870
871 if let Some(ref preview) = session.preview {
872 preview(scene, &ctx);
873 } else {
874 draw_default_source_ghost(scene, &ctx, accent);
875 }
876 });
877}