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