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 return true; }
447
448 false
449 }
450
451 DragAction::Release {
452 position,
453 modifiers,
454 } => {
455 let mut consumed = false;
456
457 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
458 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
459 dnd_finish(&frame, session, modifiers, position, true);
460 }
461 consumed = true;
462 }
463
464 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
465 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
466
467 consumed
468 }
469
470 DragAction::Cancel => {
471 let mut consumed = false;
472 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
473 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
474 dnd_finish(
475 &frame,
476 session,
477 Modifiers::default(),
478 Vec2::default(),
479 false,
480 );
481 }
482 consumed = true;
483 }
484 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
485 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
486 consumed
487 }
488 }
489}
490
491pub fn overlay_drag_indicator(
494 scene: &mut Scene,
495 mouse_pos_px: (f32, f32),
496 external_file_drag: bool,
497) {
498 if !is_dragging() && !external_file_drag {
499 return;
500 }
501
502 let pos = Vec2 {
503 x: mouse_pos_px.0,
504 y: mouse_pos_px.1,
505 };
506
507 let frame = DND_FRAME.with(|f| f.borrow().clone());
508 let Some(ref f) = frame else {
509 return;
510 };
511
512 let color = if external_file_drag {
513 Color::from_hex("#FFAA00")
514 } else {
515 Color::from_hex("#44AAFF")
516 };
517
518 if let Some(tid) = dnd_target_id_at(f, pos)
520 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
521 {
522 let r = crate::locals::dp_to_px(8.0);
523 scene.nodes.push(SceneNode::Border {
524 rect: hit.rect,
525 color,
526 width: crate::locals::dp_to_px(2.0),
527 radius: [r; 4],
528 });
529 }
530
531 let badge = Rect {
533 x: pos.x + crate::locals::dp_to_px(12.0),
534 y: pos.y + crate::locals::dp_to_px(12.0),
535 w: crate::locals::dp_to_px(110.0),
536 h: crate::locals::dp_to_px(24.0),
537 };
538
539 let bg = if external_file_drag {
540 Color::from_hex("#FFAA0077")
541 } else {
542 Color::from_hex("#44AAFF77")
543 };
544
545 let r = crate::locals::dp_to_px(8.0);
546 scene.nodes.push(SceneNode::Rect {
547 rect: badge,
548 brush: Brush::Solid(bg),
549 radius: [r; 4],
550 });
551 scene.nodes.push(SceneNode::Text {
552 rect: Rect {
553 x: badge.x + crate::locals::dp_to_px(8.0),
554 y: badge.y + crate::locals::dp_to_px(6.0),
555 w: 0.0,
556 h: crate::locals::dp_to_px(14.0),
557 },
558 text: Arc::<str>::from(" "),
559 color: Color::WHITE,
560 size: crate::locals::dp_to_px(12.0),
561 font_family: None,
562 text_align: TextAlign::Unspecified,
563 font_weight: FontWeight::NORMAL,
564 font_style: FontStyle::Normal,
565 text_decoration: TextDecoration::default(),
566 letter_spacing: 0.0,
567 line_height: 0.0,
568 });
569}