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