Skip to main content

cosmic/widget/
dnd_destination.rs

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