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| {
694 if let Some(ref mut session) = *s.borrow_mut() {
695 dnd_update_over(&frame, session, modifiers, position);
696 }
697 });
698 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
699 return true;
700 }
701 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
704 }
705 return true; }
707
708 if let Some(touch) = DND_TOUCH_DOWN.with(|t| t.borrow().clone()) {
710 if touch.long_press_pending {
711 let elapsed_ms = (Instant::now() - touch.time).as_millis();
712 let dx = position.x - touch.position.x;
713 let dy = position.y - touch.position.y;
714 let dist = (dx * dx + dy * dy).sqrt();
715
716 if elapsed_ms >= LONG_PRESS_MS
717 && dist <= slop
718 && let Some(frame) = DND_FRAME.with(|f| f.borrow().clone())
719 {
720 if initiate_drag(
721 &frame,
722 touch.capture_id,
723 touch.position,
724 position,
725 modifiers,
726 ) {
727 DND_SESSION.with(|s| {
728 if let Some(ref mut session) = *s.borrow_mut() {
729 dnd_update_over(&frame, session, modifiers, position);
730 }
731 });
732 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
733 return true;
734 }
735 DND_TOUCH_DOWN.with(|t| {
737 if let Some(ref mut td) = *t.borrow_mut() {
738 td.long_press_pending = false;
739 }
740 });
741 }
742 if dist > slop {
743 DND_TOUCH_DOWN.with(|t| {
744 if let Some(ref mut td) = *t.borrow_mut() {
745 td.long_press_pending = false;
746 }
747 });
748 }
749 }
750 let still_pending = DND_TOUCH_DOWN.with(|t| {
753 t.borrow()
754 .as_ref()
755 .map(|td| td.long_press_pending)
756 .unwrap_or(false)
757 });
758 if still_pending {
759 return true;
760 }
761 }
762
763 false
764 }
765
766 DragAction::Release {
767 position,
768 modifiers,
769 } => {
770 let mut consumed = false;
771
772 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
773 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
774 dnd_finish(&frame, session, modifiers, position, true);
775 }
776 consumed = true;
777 }
778
779 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
780 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
781
782 consumed
783 }
784
785 DragAction::Cancel => {
786 let mut consumed = false;
787 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
788 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
789 dnd_finish(
790 &frame,
791 session,
792 Modifiers::default(),
793 Vec2::default(),
794 false,
795 );
796 }
797 consumed = true;
798 }
799 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
800 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
801 consumed
802 }
803 }
804}
805
806pub fn overlay_drag_indicator(
809 scene: &mut Scene,
810 mouse_pos_px: (f32, f32),
811 external_file_drag: bool,
812) {
813 if !is_dragging() && !external_file_drag {
814 return;
815 }
816
817 let pos = Vec2 {
818 x: mouse_pos_px.0,
819 y: mouse_pos_px.1,
820 };
821
822 let frame = DND_FRAME.with(|f| f.borrow().clone());
823 let Some(ref f) = frame else {
824 return;
825 };
826
827 let accent = if external_file_drag {
828 Color::from_hex("#FFAA00")
829 } else {
830 Color::from_hex("#44AAFF")
831 };
832
833 if let Some(tid) = dnd_target_id_at(f, pos)
835 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
836 {
837 let r = crate::locals::dp_to_px(8.0);
838 scene.nodes.push(SceneNode::Border {
839 rect: hit.rect,
840 color: accent,
841 width: crate::locals::dp_to_px(2.0),
842 radius: [r; 4],
843 });
844 }
845
846 if external_file_drag {
847 draw_label_chip(scene, pos, "Drop files", accent, true);
848 return;
849 }
850
851 DND_SESSION.with(|s| {
852 let session = s.borrow();
853 let Some(ref session) = *session else {
854 return;
855 };
856
857 let ctx = DragPreviewCtx {
858 pointer: pos,
859 start_pointer: Vec2 {
860 x: session.start_px.0,
861 y: session.start_px.1,
862 },
863 source_rect: session.source_rect,
864 grab_offset: session.grab_offset,
865 payload: session.payload.clone(),
866 };
867
868 if let Some(ref preview) = session.preview {
869 preview(scene, &ctx);
870 } else {
871 draw_default_source_ghost(scene, &ctx, accent);
872 }
873 });
874}