cosmic/widget/
dnd_destination.rs

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    /// Add a message that will be emitted instead of [`on_data_received`](Self::on_data_received) if the dropped files
252    /// are offered through the xdg share portal. You can then use [`crate::command::file_transfer_receive`]
253    /// with the key to receive the files.
254    #[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    /// Returns the drag id of the destination.
266    ///
267    /// # Panics
268    /// Panics if the destination has been assigned a Set id, which is invalid.
269    #[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    /// get the a11y nodes for the widget
670    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}