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