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_target<T, F>(mut modifier: crate::Modifier, on_drop: F) -> crate::Modifier
72where
73 T: 'static,
74 F: Fn(&DropEvent, &T) -> bool + 'static,
75{
76 modifier = modifier.on_drop(move |ev| match downcast_drag_payload::<T>(&ev.payload) {
77 Some(v) => on_drop(&ev, v),
78 None => false,
79 });
80 modifier
81}
82
83#[derive(Clone, Debug)]
84pub struct DragStart {
85 pub source_id: u64,
86 pub position: Vec2,
87 pub modifiers: Modifiers,
88}
89
90#[derive(Clone, Debug)]
91pub struct DragOver {
92 pub source_id: u64,
93 pub target_id: u64,
94 pub position: Vec2,
95 pub modifiers: Modifiers,
96 pub payload: DragPayload,
97}
98
99#[derive(Clone, Debug)]
100pub struct DropEvent {
101 pub source_id: u64,
102 pub target_id: u64,
103 pub position: Vec2,
104 pub modifiers: Modifiers,
105 pub payload: DragPayload,
106}
107
108#[derive(Clone, Copy, Debug)]
110pub struct DragEnd {
111 pub accepted: bool,
112}
113
114#[derive(Clone, Debug)]
118pub struct DroppedFile {
119 pub name: String,
120 pub path: Option<PathBuf>,
121}
122
123#[derive(Clone, Debug)]
125pub struct DroppedFiles {
126 pub files: Vec<DroppedFile>,
127}
128
129#[derive(Clone, Debug)]
131pub struct DragSession {
132 pub source_id: u64,
133 pub payload: DragPayload,
134 pub start_px: (f32, f32),
135 pub over_id: Option<u64>,
136}
137
138#[derive(Clone)]
139struct MouseDownState {
140 position: Vec2,
141 capture_id: u64,
142}
143
144#[derive(Clone)]
145struct TouchDownState {
146 time: Instant,
147 position: Vec2,
148 capture_id: u64,
149 long_press_pending: bool,
150}
151
152const LONG_PRESS_MS: u128 = 400;
153
154thread_local! {
155 static DND_FRAME: RefCell<Option<Frame>> = const { RefCell::new(None) };
156 static DND_SCALE: RefCell<f32> = const { RefCell::new(1.0) };
157 static DND_SESSION: RefCell<Option<DragSession>> = const { RefCell::new(None) };
158 static DND_MOUSE_DOWN: RefCell<Option<MouseDownState>> = const { RefCell::new(None) };
159 static DND_TOUCH_DOWN: RefCell<Option<TouchDownState>> = const { RefCell::new(None) };
160}
161
162pub fn set_dnd_frame(frame: Option<Frame>) {
164 DND_FRAME.with(|f| *f.borrow_mut() = frame);
165}
166
167pub fn set_dnd_scale(scale: f32) {
169 DND_SCALE.with(|s| *s.borrow_mut() = scale);
170}
171
172pub fn is_dragging() -> bool {
174 DND_SESSION.with(|s| s.borrow().is_some())
175}
176
177fn touch_slop_px(scale: f32) -> f32 {
178 6.0 * scale
179}
180
181fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
182 frame.hit_regions.iter().position(|h| h.id == id)
183}
184
185fn is_dnd_target(hit: &HitRegion) -> bool {
186 hit.on_drop.is_some()
187 || hit.on_drag_enter.is_some()
188 || hit.on_drag_over.is_some()
189 || hit.on_drag_leave.is_some()
190}
191
192pub fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
193 frame
194 .hit_regions
195 .iter()
196 .rev()
197 .filter(|h| h.rect.contains(pos))
198 .find(|h| is_dnd_target(h))
199 .map(|h| h.id)
200}
201
202fn dnd_update_over(frame: &Frame, session: &mut DragSession, modifiers: Modifiers, pos: Vec2) {
203 let new_over = dnd_target_id_at(frame, pos);
204
205 if new_over != session.over_id {
206 if let Some(prev) = session.over_id {
207 if let Some(i) = hit_index_by_id(frame, prev) {
208 if let Some(cb) = &frame.hit_regions[i].on_drag_leave {
209 cb(DragOver {
210 source_id: session.source_id,
211 target_id: prev,
212 position: pos,
213 modifiers,
214 payload: session.payload.clone(),
215 });
216 }
217 }
218 }
219
220 if let Some(now) = new_over {
221 if let Some(i) = hit_index_by_id(frame, now) {
222 if let Some(cb) = &frame.hit_regions[i].on_drag_enter {
223 cb(DragOver {
224 source_id: session.source_id,
225 target_id: now,
226 position: pos,
227 modifiers,
228 payload: session.payload.clone(),
229 });
230 }
231 }
232 }
233
234 session.over_id = new_over;
235 }
236
237 if let Some(over) = session.over_id {
238 if let Some(i) = hit_index_by_id(frame, over) {
239 if let Some(cb) = &frame.hit_regions[i].on_drag_over {
240 cb(DragOver {
241 source_id: session.source_id,
242 target_id: over,
243 position: pos,
244 modifiers,
245 payload: session.payload.clone(),
246 });
247 }
248 }
249 }
250}
251
252fn dnd_finish(
254 frame: &Frame,
255 session: DragSession,
256 modifiers: Modifiers,
257 pos: Vec2,
258 accept_if_possible: bool,
259) -> bool {
260 let mut accepted = false;
261 if accept_if_possible {
262 let drop_target = dnd_target_id_at(frame, pos);
263 if let Some(tid) = drop_target {
264 if let Some(i) = hit_index_by_id(frame, tid) {
265 if let Some(cb) = &frame.hit_regions[i].on_drop {
266 accepted = cb(DropEvent {
267 source_id: session.source_id,
268 target_id: tid,
269 position: pos,
270 modifiers,
271 payload: session.payload.clone(),
272 });
273 }
274 }
275 }
276 }
277
278 if let Some(i) = hit_index_by_id(frame, session.source_id) {
279 if let Some(cb) = &frame.hit_regions[i].on_drag_end {
280 cb(DragEnd { accepted });
281 }
282 }
283
284 accepted
285}
286
287fn initiate_drag(
288 frame: &Frame,
289 capture_id: u64,
290 start_pos: Vec2,
291 current_pos: Vec2,
292 modifiers: Modifiers,
293) -> bool {
294 let Some(i) = hit_index_by_id(frame, capture_id) else {
295 return false;
296 };
297 let Some(cb) = &frame.hit_regions[i].on_drag_start else {
298 return false;
299 };
300
301 let payload = cb(DragStart {
302 source_id: capture_id,
303 position: current_pos,
304 modifiers,
305 });
306 let Some(payload) = payload else {
307 return false;
308 };
309
310 DND_SESSION.with(|s| {
311 *s.borrow_mut() = Some(DragSession {
312 source_id: capture_id,
313 payload,
314 start_px: (start_pos.x, start_pos.y),
315 over_id: None,
316 });
317 });
318 true
319}
320
321pub fn handle_drag_action(action: &DragAction) -> bool {
323 let scale = DND_SCALE.with(|s| *s.borrow());
324 let slop = touch_slop_px(scale);
325
326 match *action {
327 DragAction::Press {
328 position,
329 capture_id,
330 kind,
331 ..
332 } => {
333 match kind {
334 PointerKind::Mouse => {
335 DND_MOUSE_DOWN.with(|m| {
336 *m.borrow_mut() = Some(MouseDownState {
337 position,
338 capture_id,
339 });
340 });
341 }
342 _ => {
343 DND_TOUCH_DOWN.with(|t| {
345 *t.borrow_mut() = Some(TouchDownState {
346 time: web_time::Instant::now(),
347 position,
348 capture_id,
349 long_press_pending: true,
350 });
351 });
352 }
353 }
354 false
355 }
356
357 DragAction::Move {
358 position,
359 modifiers,
360 } => {
361 if DND_SESSION.with(|s| s.borrow().is_some()) {
363 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
364 DND_SESSION.with(|s| {
365 if let Some(ref mut session) = *s.borrow_mut() {
366 dnd_update_over(&frame, session, modifiers, position);
367 }
368 });
369 }
370 return true;
371 }
372
373 if let Some(down) = DND_MOUSE_DOWN.with(|m| m.borrow().clone()) {
375 let dx = position.x - down.position.x;
376 let dy = position.y - down.position.y;
377 let dist = (dx * dx + dy * dy).sqrt();
378 if dist >= slop {
379 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
380 if initiate_drag(
381 &frame,
382 down.capture_id,
383 down.position,
384 position,
385 modifiers,
386 ) {
387 DND_SESSION.with(|s| {
389 if let Some(ref mut session) = *s.borrow_mut() {
390 dnd_update_over(&frame, session, modifiers, position);
391 }
392 });
393 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
394 return true;
395 }
396 }
397 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
400 }
401 return true; }
403
404 if let Some(touch) = DND_TOUCH_DOWN.with(|t| t.borrow().clone()) {
406 if touch.long_press_pending {
407 let elapsed_ms = (Instant::now() - touch.time).as_millis() as u128;
408 let dx = position.x - touch.position.x;
409 let dy = position.y - touch.position.y;
410 let dist = (dx * dx + dy * dy).sqrt();
411
412 if elapsed_ms >= LONG_PRESS_MS && dist <= slop {
413 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
414 if initiate_drag(
415 &frame,
416 touch.capture_id,
417 touch.position,
418 position,
419 modifiers,
420 ) {
421 DND_SESSION.with(|s| {
422 if let Some(ref mut session) = *s.borrow_mut() {
423 dnd_update_over(&frame, session, modifiers, position);
424 }
425 });
426 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
427 return true;
428 }
429 DND_TOUCH_DOWN.with(|t| {
431 if let Some(ref mut td) = *t.borrow_mut() {
432 td.long_press_pending = false;
433 }
434 });
435 }
436 }
437 if dist > slop {
438 DND_TOUCH_DOWN.with(|t| {
439 if let Some(ref mut td) = *t.borrow_mut() {
440 td.long_press_pending = false;
441 }
442 });
443 }
444 }
445 let still_pending = DND_TOUCH_DOWN.with(|t| {
448 t.borrow().as_ref().map(|td| td.long_press_pending).unwrap_or(false)
449 });
450 if still_pending {
451 return true;
452 }
453 }
454
455 false
456 }
457
458 DragAction::Release {
459 position,
460 modifiers,
461 } => {
462 let mut consumed = false;
463
464 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
465 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
466 dnd_finish(&frame, session, modifiers, position, true);
467 }
468 consumed = true;
469 }
470
471 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
472 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
473
474 consumed
475 }
476
477 DragAction::Cancel => {
478 let mut consumed = false;
479 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
480 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
481 dnd_finish(
482 &frame,
483 session,
484 Modifiers::default(),
485 Vec2::default(),
486 false,
487 );
488 }
489 consumed = true;
490 }
491 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
492 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
493 consumed
494 }
495 }
496}
497
498pub fn overlay_drag_indicator(
501 scene: &mut Scene,
502 mouse_pos_px: (f32, f32),
503 external_file_drag: bool,
504) {
505 if !is_dragging() && !external_file_drag {
506 return;
507 }
508
509 let pos = Vec2 {
510 x: mouse_pos_px.0,
511 y: mouse_pos_px.1,
512 };
513
514 let frame = DND_FRAME.with(|f| f.borrow().clone());
515 let Some(ref f) = frame else {
516 return;
517 };
518
519 let color = if external_file_drag {
520 Color::from_hex("#FFAA00")
521 } else {
522 Color::from_hex("#44AAFF")
523 };
524
525 if let Some(tid) = dnd_target_id_at(f, pos)
527 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
528 {
529 let r = crate::locals::dp_to_px(8.0);
530 scene.nodes.push(SceneNode::Border {
531 rect: hit.rect,
532 color,
533 width: crate::locals::dp_to_px(2.0),
534 radius: [r; 4],
535 });
536 }
537
538 let badge = Rect {
540 x: pos.x + crate::locals::dp_to_px(12.0),
541 y: pos.y + crate::locals::dp_to_px(12.0),
542 w: crate::locals::dp_to_px(110.0),
543 h: crate::locals::dp_to_px(24.0),
544 };
545
546 let bg = if external_file_drag {
547 Color::from_hex("#FFAA0077")
548 } else {
549 Color::from_hex("#44AAFF77")
550 };
551
552 let r = crate::locals::dp_to_px(8.0);
553 scene.nodes.push(SceneNode::Rect {
554 rect: badge,
555 brush: Brush::Solid(bg),
556 radius: [r; 4],
557 });
558 scene.nodes.push(SceneNode::Text {
559 rect: Rect {
560 x: badge.x + crate::locals::dp_to_px(8.0),
561 y: badge.y + crate::locals::dp_to_px(6.0),
562 w: 0.0,
563 h: crate::locals::dp_to_px(14.0),
564 },
565 text: Arc::<str>::from(" "),
566 color: Color::WHITE,
567 size: crate::locals::dp_to_px(12.0),
568 font_family: None,
569 text_align: TextAlign::Unspecified,
570 font_weight: FontWeight::NORMAL,
571 font_style: FontStyle::Normal,
572 text_decoration: TextDecoration::default(),
573 letter_spacing: 0.0,
574 line_height: 0.0,
575 extra_style: Default::default(),
576 url: None,
577 });
578}