1use std::{
2 borrow::Cow,
3 sync::atomic::{AtomicU64, Ordering},
4};
5
6use iced::Vector;
7
8use crate::{
9 Element,
10 widget::{Id, Widget},
11};
12
13use iced::{
14 Event, Length, Rectangle,
15 clipboard::{
16 dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
17 mime::AllowedMimeTypes,
18 },
19 event,
20 id::Internal,
21 mouse, overlay,
22};
23use iced_core::{
24 self, Clipboard, Shell, layout,
25 widget::{Tree, tree},
26};
27
28pub fn dnd_destination<'a, Message: 'static>(
29 child: impl Into<Element<'a, Message>>,
30 mimes: Vec<Cow<'static, str>>,
31) -> DndDestination<'a, Message> {
32 DndDestination::new(child, mimes)
33}
34
35pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>(
36 child: impl Into<Element<'a, Message>>,
37 on_finish: impl Fn(Option<T>, DndAction) -> Message + 'static,
38) -> DndDestination<'a, Message> {
39 DndDestination::for_data(child, on_finish)
40}
41
42static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
43const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination";
44#[cfg(feature = "xdg-portal")]
45pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer";
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub struct DragId(pub u128);
49
50impl DragId {
51 pub fn new() -> Self {
52 DragId(u128::from(u64::MAX) + u128::from(DRAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed)))
53 }
54}
55
56#[allow(clippy::new_without_default)]
57impl Default for DragId {
58 fn default() -> Self {
59 DragId::new()
60 }
61}
62
63pub struct DndDestination<'a, Message> {
64 id: Id,
65 drag_id: Option<u64>,
66 preferred_action: DndAction,
67 action: DndAction,
68 container: Element<'a, Message>,
69 mime_types: Vec<Cow<'static, str>>,
70 forward_drag_as_cursor: bool,
71 on_hold: Option<Box<dyn Fn(f64, f64) -> Message>>,
72 on_drop: Option<Box<dyn Fn(f64, f64) -> Message>>,
73 on_enter: Option<Box<dyn Fn(f64, f64, Vec<String>) -> Message>>,
74 on_leave: Option<Box<dyn Fn() -> Message>>,
75 on_motion: Option<Box<dyn Fn(f64, f64) -> Message>>,
76 on_action_selected: Option<Box<dyn Fn(DndAction) -> Message>>,
77 on_data_received: Option<Box<dyn Fn(String, Vec<u8>) -> Message>>,
78 on_finish: Option<Box<dyn Fn(String, Vec<u8>, DndAction, f64, f64) -> Message>>,
79 #[cfg(feature = "xdg-portal")]
80 on_file_transfer: Option<Box<dyn Fn(String) -> Message>>,
81}
82
83impl<'a, Message: 'static> DndDestination<'a, Message> {
84 fn mime_matches(&self, offered: &[String]) -> bool {
85 self.mime_types.is_empty()
86 || offered
87 .iter()
88 .any(|mime| self.mime_types.iter().any(|allowed| allowed == mime))
89 }
90 pub fn new(child: impl Into<Element<'a, Message>>, mimes: Vec<Cow<'static, str>>) -> Self {
91 Self {
92 id: Id::unique(),
93 drag_id: None,
94 mime_types: mimes,
95 preferred_action: DndAction::Move,
96 action: DndAction::Copy | DndAction::Move,
97 container: child.into(),
98 forward_drag_as_cursor: false,
99 on_hold: None,
100 on_drop: None,
101 on_enter: None,
102 on_leave: None,
103 on_motion: None,
104 on_action_selected: None,
105 on_data_received: None,
106 on_finish: None,
107 #[cfg(feature = "xdg-portal")]
108 on_file_transfer: None,
109 }
110 }
111
112 pub fn for_data<T: AllowedMimeTypes>(
113 child: impl Into<Element<'a, Message>>,
114 on_finish: impl Fn(Option<T>, DndAction) -> Message + 'static,
115 ) -> Self {
116 Self {
117 id: Id::unique(),
118 drag_id: None,
119 mime_types: T::allowed().iter().cloned().map(Cow::Owned).collect(),
120 preferred_action: DndAction::Move,
121 action: DndAction::Copy | DndAction::Move,
122 container: child.into(),
123 forward_drag_as_cursor: false,
124 on_hold: None,
125 on_drop: None,
126 on_enter: None,
127 on_leave: None,
128 on_motion: None,
129 on_action_selected: None,
130 on_data_received: None,
131 on_finish: Some(Box::new(move |mime, data, action, _, _| {
132 on_finish(T::try_from((data, mime)).ok(), action)
133 })),
134 #[cfg(feature = "xdg-portal")]
135 on_file_transfer: None,
136 }
137 }
138
139 #[must_use]
140 pub fn data_received_for<T: AllowedMimeTypes>(
141 mut self,
142 f: impl Fn(Option<T>) -> Message + 'static,
143 ) -> Self {
144 self.on_data_received = Some(Box::new(
145 move |mime, data| f(T::try_from((data, mime)).ok()),
146 ));
147 self
148 }
149
150 pub fn with_id(
151 child: impl Into<Element<'a, Message>>,
152 id: Id,
153 mimes: Vec<Cow<'static, str>>,
154 ) -> Self {
155 Self {
156 id,
157 drag_id: None,
158 mime_types: mimes,
159 preferred_action: DndAction::Move,
160 action: DndAction::Copy | DndAction::Move,
161 container: child.into(),
162 forward_drag_as_cursor: false,
163 on_hold: None,
164 on_drop: None,
165 on_enter: None,
166 on_leave: None,
167 on_motion: None,
168 on_action_selected: None,
169 on_data_received: None,
170 on_finish: None,
171 #[cfg(feature = "xdg-portal")]
172 on_file_transfer: None,
173 }
174 }
175
176 #[must_use]
177 pub fn drag_id(mut self, id: u64) -> Self {
178 self.drag_id = Some(id);
179 self
180 }
181
182 #[must_use]
183 pub fn action(mut self, action: DndAction) -> Self {
184 self.action = action;
185 self
186 }
187
188 #[must_use]
189 pub fn preferred_action(mut self, action: DndAction) -> Self {
190 self.preferred_action = action;
191 self
192 }
193
194 #[must_use]
195 pub fn forward_drag_as_cursor(mut self, forward: bool) -> Self {
196 self.forward_drag_as_cursor = forward;
197 self
198 }
199
200 #[must_use]
201 pub fn on_hold(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self {
202 self.on_hold = Some(Box::new(f));
203 self
204 }
205
206 #[must_use]
207 pub fn on_drop(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self {
208 self.on_drop = Some(Box::new(f));
209 self
210 }
211
212 #[must_use]
213 pub fn on_enter(mut self, f: impl Fn(f64, f64, Vec<String>) -> Message + 'static) -> Self {
214 self.on_enter = Some(Box::new(f));
215 self
216 }
217
218 #[must_use]
219 pub fn on_leave(mut self, m: impl Fn() -> Message + 'static) -> Self {
220 self.on_leave = Some(Box::new(m));
221 self
222 }
223
224 #[must_use]
225 pub fn on_finish(
226 mut self,
227 f: impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message + 'static,
228 ) -> Self {
229 self.on_finish = Some(Box::new(f));
230 self
231 }
232
233 #[must_use]
234 pub fn on_motion(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self {
235 self.on_motion = Some(Box::new(f));
236 self
237 }
238
239 #[must_use]
240 pub fn on_action_selected(mut self, f: impl Fn(DndAction) -> Message + 'static) -> Self {
241 self.on_action_selected = Some(Box::new(f));
242 self
243 }
244
245 #[must_use]
246 pub fn on_data_received(mut self, f: impl Fn(String, Vec<u8>) -> Message + 'static) -> Self {
247 self.on_data_received = Some(Box::new(f));
248 self
249 }
250
251 #[cfg(feature = "xdg-portal")]
255 #[must_use]
256 pub fn on_file_transfer(mut self, f: impl Fn(String) -> Message + 'static) -> Self {
257 match self.mime_types.iter().position(|v| v == "text/uri-list") {
258 Some(i) => self.mime_types.insert(i, Cow::Borrowed(FILE_TRANSFER_MIME)),
259 None => self.mime_types.push(Cow::Borrowed(FILE_TRANSFER_MIME)),
260 }
261 self.on_file_transfer = Some(Box::new(f));
262 self
263 }
264
265 #[must_use]
270 pub fn get_drag_id(&self) -> u128 {
271 u128::from(self.drag_id.unwrap_or_else(|| match &self.id.0 {
272 Internal::Unique(id) | Internal::Custom(id, _) => *id,
273 Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."),
274 }))
275 }
276
277 pub fn id(mut self, id: Id) -> Self {
278 self.id = id;
279 self
280 }
281}
282
283impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
284 for DndDestination<'_, Message>
285{
286 fn children(&self) -> Vec<Tree> {
287 vec![Tree::new(&self.container)]
288 }
289
290 fn tag(&self) -> iced_core::widget::tree::Tag {
291 tree::Tag::of::<State<()>>()
292 }
293
294 fn diff(&mut self, tree: &mut Tree) {
295 tree.diff_children(std::slice::from_mut(&mut self.container));
296 }
297
298 fn state(&self) -> iced_core::widget::tree::State {
299 tree::State::new(State::<()>::new())
300 }
301
302 fn size(&self) -> iced_core::Size<Length> {
303 self.container.as_widget().size()
304 }
305
306 fn layout(
307 &mut self,
308 tree: &mut Tree,
309 renderer: &crate::Renderer,
310 limits: &layout::Limits,
311 ) -> layout::Node {
312 self.container
313 .as_widget_mut()
314 .layout(&mut tree.children[0], renderer, limits)
315 }
316
317 fn operate(
318 &mut self,
319 tree: &mut Tree,
320 layout: layout::Layout<'_>,
321 renderer: &crate::Renderer,
322 operation: &mut dyn iced_core::widget::Operation<()>,
323 ) {
324 self.container
325 .as_widget_mut()
326 .operate(&mut tree.children[0], layout, renderer, operation);
327 }
328
329 #[allow(clippy::too_many_lines)]
330 fn update(
331 &mut self,
332 tree: &mut Tree,
333 event: &Event,
334 layout: layout::Layout<'_>,
335 cursor: mouse::Cursor,
336 renderer: &crate::Renderer,
337 clipboard: &mut dyn Clipboard,
338 shell: &mut Shell<'_, Message>,
339 viewport: &Rectangle,
340 ) {
341 self.container.as_widget_mut().update(
342 &mut tree.children[0],
343 event,
344 layout,
345 cursor,
346 renderer,
347 clipboard,
348 shell,
349 viewport,
350 );
351 if shell.is_event_captured() {
352 return;
353 }
354
355 let state = tree.state.downcast_mut::<State<()>>();
356
357 let my_id = self.get_drag_id();
358
359 log::trace!(
360 target: DND_DEST_LOG_TARGET,
361 "dnd_destination id={:?}: event {:?}",
362 self.drag_id.unwrap_or_default(),
363 event
364 );
365 match event {
366 Event::Dnd(DndEvent::Offer(
367 id,
368 OfferEvent::Enter {
369 x, y, mime_types, ..
370 },
371 )) if *id == Some(my_id) => {
372 if !self.mime_matches(&mime_types) {
373 log::trace!(
374 target: DND_DEST_LOG_TARGET,
375 "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})",
376 self.mime_types
377 );
378 return;
379 }
380 log::trace!(
381 target: DND_DEST_LOG_TARGET,
382 "offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}"
383 );
384 if let Some(msg) = state.on_enter(
385 *x,
386 *y,
387 mime_types.clone(),
388 self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
389 (),
390 ) {
391 shell.publish(msg);
392 }
393 if self.forward_drag_as_cursor {
394 #[allow(clippy::cast_possible_truncation)]
395 let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into());
396 let event = Event::Mouse(mouse::Event::CursorMoved {
397 position: drag_cursor.position().unwrap(),
398 });
399 self.container.as_widget_mut().update(
400 &mut tree.children[0],
401 &event,
402 layout,
403 drag_cursor,
404 renderer,
405 clipboard,
406 shell,
407 viewport,
408 );
409 }
410 shell.capture_event();
411 return;
412 }
413 Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => {
414 log::trace!(
415 target: DND_DEST_LOG_TARGET,
416 "offer leave id={:?}",
417 my_id
418 );
419 if let Some(msg) =
420 state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref))
421 {
422 shell.publish(msg);
423 }
424
425 if self.forward_drag_as_cursor {
426 let drag_cursor = mouse::Cursor::Unavailable;
427 let event = Event::Mouse(mouse::Event::CursorLeft);
428 self.container.as_widget_mut().update(
429 &mut tree.children[0],
430 &event,
431 layout,
432 drag_cursor,
433 renderer,
434 clipboard,
435 shell,
436 viewport,
437 );
438 }
439 return;
440 }
441 Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => {
442 log::trace!(
443 target: DND_DEST_LOG_TARGET,
444 "offer motion id={my_id:?} coords=({x},{y})"
445 );
446 if let Some(msg) = state.on_motion(
447 *x,
448 *y,
449 self.on_motion.as_ref().map(std::convert::AsRef::as_ref),
450 self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
451 (),
452 ) {
453 shell.publish(msg);
454 }
455
456 if self.forward_drag_as_cursor {
457 #[allow(clippy::cast_possible_truncation)]
458 let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into());
459 let event = Event::Mouse(mouse::Event::CursorMoved {
460 position: drag_cursor.position().unwrap(),
461 });
462 self.container.as_widget_mut().update(
463 &mut tree.children[0],
464 &event,
465 layout,
466 drag_cursor,
467 renderer,
468 clipboard,
469 shell,
470 viewport,
471 );
472 }
473 shell.capture_event();
474 return;
475 }
476 Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => {
477 log::trace!(
478 target: DND_DEST_LOG_TARGET,
479 "offer leave-destination id={:?}",
480 my_id
481 );
482 if let Some(msg) =
483 state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref))
484 {
485 shell.publish(msg);
486 }
487 return;
488 }
489 Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => {
490 log::trace!(
491 target: DND_DEST_LOG_TARGET,
492 "offer drop id={my_id:?}"
493 );
494 if let Some(msg) =
495 state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref))
496 {
497 shell.publish(msg);
498 }
499 shell.capture_event();
500 return;
501 }
502 Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action)))
503 if *id == Some(my_id) =>
504 {
505 log::trace!(
506 target: DND_DEST_LOG_TARGET,
507 "offer selected-action id={my_id:?} action={action:?}"
508 );
509 if let Some(msg) = state.on_action_selected(
510 *action,
511 self.on_action_selected
512 .as_ref()
513 .map(std::convert::AsRef::as_ref),
514 ) {
515 shell.publish(msg);
516 }
517 shell.capture_event();
518 return;
519 }
520 Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type }))
521 if *id == Some(my_id) =>
522 {
523 log::trace!(
524 target: DND_DEST_LOG_TARGET,
525 "offer data id={my_id:?} mime={mime_type:?} bytes={}",
526 data.len()
527 );
528
529 #[cfg(feature = "xdg-portal")]
530 if mime_type == FILE_TRANSFER_MIME
531 && let Some(f) = self.on_file_transfer.as_ref()
532 && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec())
533 {
534 shell.publish(f(s));
535 shell.capture_event();
536 return;
537 }
538
539 if let (Some(msg), ret) = state.on_data_received(
540 mime_type.clone(),
541 data.clone(),
542 self.on_data_received
543 .as_ref()
544 .map(std::convert::AsRef::as_ref),
545 self.on_finish.as_ref().map(std::convert::AsRef::as_ref),
546 ) {
547 shell.publish(msg);
548 if ret == event::Status::Captured {
549 log::trace!(
550 target: DND_DEST_LOG_TARGET,
551 "offer data id={my_id:?} captured"
552 );
553 shell.capture_event();
554 }
555 return;
556 }
557 shell.capture_event();
558 return;
559 }
560 _ => {}
561 }
562 }
563
564 fn mouse_interaction(
565 &self,
566 tree: &Tree,
567 layout: layout::Layout<'_>,
568 cursor_position: mouse::Cursor,
569 viewport: &Rectangle,
570 renderer: &crate::Renderer,
571 ) -> mouse::Interaction {
572 self.container.as_widget().mouse_interaction(
573 &tree.children[0],
574 layout,
575 cursor_position,
576 viewport,
577 renderer,
578 )
579 }
580
581 fn draw(
582 &self,
583 tree: &Tree,
584 renderer: &mut crate::Renderer,
585 theme: &crate::Theme,
586 renderer_style: &iced_core::renderer::Style,
587 layout: layout::Layout<'_>,
588 cursor_position: mouse::Cursor,
589 viewport: &Rectangle,
590 ) {
591 self.container.as_widget().draw(
592 &tree.children[0],
593 renderer,
594 theme,
595 renderer_style,
596 layout,
597 cursor_position,
598 viewport,
599 );
600 }
601
602 fn overlay<'b>(
603 &'b mut self,
604 tree: &'b mut Tree,
605 layout: layout::Layout<'b>,
606 renderer: &crate::Renderer,
607 viewport: &Rectangle,
608 translation: Vector,
609 ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
610 self.container.as_widget_mut().overlay(
611 &mut tree.children[0],
612 layout,
613 renderer,
614 viewport,
615 translation,
616 )
617 }
618
619 fn drag_destinations(
620 &self,
621 state: &Tree,
622 layout: layout::Layout<'_>,
623 renderer: &crate::Renderer,
624 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
625 ) {
626 let bounds = layout.bounds();
627 let my_id = self.get_drag_id();
628 log::trace!(
629 target: DND_DEST_LOG_TARGET,
630 "register destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
631 my_id,
632 bounds.x,
633 bounds.y,
634 bounds.width,
635 bounds.height,
636 self.mime_types
637 );
638 let my_dest = DndDestinationRectangle {
639 id: my_id,
640 rectangle: dnd::Rectangle {
641 x: f64::from(bounds.x),
642 y: f64::from(bounds.y),
643 width: f64::from(bounds.width),
644 height: f64::from(bounds.height),
645 },
646 mime_types: self.mime_types.clone(),
647 actions: self.action,
648 preferred: self.preferred_action,
649 };
650 dnd_rectangles.push(my_dest);
651
652 self.container.as_widget().drag_destinations(
653 &state.children[0],
654 layout,
655 renderer,
656 dnd_rectangles,
657 );
658 }
659
660 fn id(&self) -> Option<Id> {
661 Some(self.id.clone())
662 }
663
664 fn set_id(&mut self, id: Id) {
665 self.id = id;
666 }
667
668 #[cfg(feature = "a11y")]
669 fn a11y_nodes(
671 &self,
672 layout: iced_core::Layout<'_>,
673 state: &Tree,
674 p: mouse::Cursor,
675 ) -> iced_accessibility::A11yTree {
676 let c_state = &state.children[0];
677 self.container.as_widget().a11y_nodes(layout, c_state, p)
678 }
679}
680
681#[derive(Default)]
682pub struct State<T> {
683 pub drag_offer: Option<DragOffer<T>>,
684}
685
686pub struct DragOffer<T> {
687 pub x: f64,
688 pub y: f64,
689 pub dropped: bool,
690 pub selected_action: DndAction,
691 pub data: T,
692}
693
694impl<T> State<T> {
695 #[must_use]
696 pub fn new() -> Self {
697 Self { drag_offer: None }
698 }
699
700 pub fn on_enter<Message>(
701 &mut self,
702 x: f64,
703 y: f64,
704 mime_types: Vec<String>,
705 on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
706 data: T,
707 ) -> Option<Message> {
708 self.drag_offer = Some(DragOffer {
709 x,
710 y,
711 dropped: false,
712 selected_action: DndAction::empty(),
713 data,
714 });
715 on_enter.map(|f| f(x, y, mime_types))
716 }
717
718 pub fn on_leave<Message>(&mut self, on_leave: Option<&dyn Fn() -> Message>) -> Option<Message> {
719 if self.drag_offer.as_ref().is_some_and(|d| !d.dropped) {
720 self.drag_offer = None;
721 on_leave.map(|f| f())
722 } else {
723 None
724 }
725 }
726
727 pub fn on_motion<Message>(
728 &mut self,
729 x: f64,
730 y: f64,
731 on_motion: Option<impl Fn(f64, f64) -> Message>,
732 on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
733 data: T,
734 ) -> Option<Message> {
735 if let Some(s) = self.drag_offer.as_mut() {
736 s.x = x;
737 s.y = y;
738 } else {
739 self.drag_offer = Some(DragOffer {
740 x,
741 y,
742 dropped: false,
743 selected_action: DndAction::empty(),
744 data,
745 });
746 if let Some(f) = on_enter {
747 return Some(f(x, y, vec![]));
748 }
749 }
750 on_motion.map(|f| f(x, y))
751 }
752
753 pub fn on_drop<Message>(
754 &mut self,
755 on_drop: Option<impl Fn(f64, f64) -> Message>,
756 ) -> Option<Message> {
757 if let Some(offer) = self.drag_offer.as_mut() {
758 offer.dropped = true;
759 if let Some(f) = on_drop {
760 return Some(f(offer.x, offer.y));
761 }
762 }
763 None
764 }
765
766 pub fn on_action_selected<Message>(
767 &mut self,
768 action: DndAction,
769 on_action_selected: Option<impl Fn(DndAction) -> Message>,
770 ) -> Option<Message> {
771 if let Some(s) = self.drag_offer.as_mut() {
772 s.selected_action = action;
773 }
774 if let Some(f) = on_action_selected {
775 f(action).into()
776 } else {
777 None
778 }
779 }
780
781 pub fn on_data_received<Message>(
782 &mut self,
783 mime: String,
784 data: Vec<u8>,
785 on_data_received: Option<impl Fn(String, Vec<u8>) -> Message>,
786 on_finish: Option<impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message>,
787 ) -> (Option<Message>, event::Status) {
788 let Some(dnd) = self.drag_offer.as_ref() else {
789 self.drag_offer = None;
790 return (None, event::Status::Ignored);
791 };
792
793 if dnd.dropped {
794 let ret = (
795 on_finish.map(|f| f(mime, data, dnd.selected_action, dnd.x, dnd.y)),
796 event::Status::Captured,
797 );
798 self.drag_offer = None;
799 ret
800 } else if let Some(f) = on_data_received {
801 (Some(f(mime, data)), event::Status::Captured)
802 } else {
803 (None, event::Status::Ignored)
804 }
805 }
806}
807
808impl<'a, Message: 'static> From<DndDestination<'a, Message>> for Element<'a, Message> {
809 fn from(wrapper: DndDestination<'a, Message>) -> Self {
810 Element::new(wrapper)
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 #[derive(Clone, Copy, Debug, PartialEq)]
819 enum TestMsg {
820 Data,
821 Finished,
822 }
823
824 #[test]
825 fn data_before_drop_invokes_data_handler_only() {
826 let mut state: State<()> = State::new();
827 assert!(state.drag_offer.is_none());
828 state.on_enter::<TestMsg>(
829 4.0,
830 2.0,
831 vec!["text/plain".into()],
832 Option::<fn(_, _, _) -> TestMsg>::None,
833 (),
834 );
835 let (message, status) = state.on_data_received(
836 "text/plain".into(),
837 vec![1],
838 Some(|mime, data| {
839 assert_eq!(mime, "text/plain");
840 assert_eq!(data, vec![1]);
841 TestMsg::Data
842 }),
843 Option::<fn(_, _, _, _, _) -> TestMsg>::None,
844 );
845 assert!(matches!(message, Some(TestMsg::Data)));
846 assert_eq!(status, event::Status::Captured);
847 assert!(state.drag_offer.is_some());
848 }
849
850 #[test]
851 fn finish_only_emits_after_drop() {
852 let mut state: State<()> = State::new();
853 state.on_enter::<TestMsg>(
854 5.0,
855 -1.0,
856 vec![],
857 Option::<fn(_, _, _) -> TestMsg>::None,
858 (),
859 );
860 state.on_action_selected::<TestMsg>(DndAction::Move, Option::<fn(_) -> TestMsg>::None);
861 state.on_drop::<TestMsg>(Option::<fn(_, _) -> TestMsg>::None);
862
863 let (message, status) = state.on_data_received(
864 "application/x-test".into(),
865 vec![7],
866 Option::<fn(_, _) -> TestMsg>::None,
867 Some(|mime, data, action, x, y| {
868 assert_eq!(mime, "application/x-test");
869 assert_eq!(data, vec![7]);
870 assert_eq!(action, DndAction::Move);
871 assert_eq!(x, 5.0);
872 assert_eq!(y, -1.0);
873 TestMsg::Finished
874 }),
875 );
876 assert!(matches!(message, Some(TestMsg::Finished)));
877 assert_eq!(status, event::Status::Captured);
878 assert!(state.drag_offer.is_none());
879 }
880}