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    iced::{
11        Event, Length, Rectangle,
12        clipboard::{
13            dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
14            mime::AllowedMimeTypes,
15        },
16        event,
17        id::Internal,
18        mouse, overlay,
19    },
20    iced_core::{
21        self, Clipboard, Shell, layout,
22        widget::{Tree, tree},
23    },
24    widget::{Id, Widget},
25};
26
27pub fn dnd_destination<'a, Message: 'static>(
28    child: impl Into<Element<'a, Message>>,
29    mimes: Vec<Cow<'static, str>>,
30) -> DndDestination<'a, Message> {
31    DndDestination::new(child, mimes)
32}
33
34pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>(
35    child: impl Into<Element<'a, Message>>,
36    on_finish: impl Fn(Option<T>, DndAction) -> Message + 'static,
37) -> DndDestination<'a, Message> {
38    DndDestination::for_data(child, on_finish)
39}
40
41static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub struct DragId(pub u128);
45
46impl DragId {
47    pub fn new() -> Self {
48        DragId(u128::from(u64::MAX) + u128::from(DRAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed)))
49    }
50}
51
52#[allow(clippy::new_without_default)]
53impl Default for DragId {
54    fn default() -> Self {
55        DragId::new()
56    }
57}
58
59pub struct DndDestination<'a, Message> {
60    id: Id,
61    drag_id: Option<u64>,
62    preferred_action: DndAction,
63    action: DndAction,
64    container: Element<'a, Message>,
65    mime_types: Vec<Cow<'static, str>>,
66    forward_drag_as_cursor: bool,
67    on_hold: Option<Box<dyn Fn(f64, f64) -> Message>>,
68    on_drop: Option<Box<dyn Fn(f64, f64) -> Message>>,
69    on_enter: Option<Box<dyn Fn(f64, f64, Vec<String>) -> Message>>,
70    on_leave: Option<Box<dyn Fn() -> Message>>,
71    on_motion: Option<Box<dyn Fn(f64, f64) -> Message>>,
72    on_action_selected: Option<Box<dyn Fn(DndAction) -> Message>>,
73    on_data_received: Option<Box<dyn Fn(String, Vec<u8>) -> Message>>,
74    on_finish: Option<Box<dyn Fn(String, Vec<u8>, DndAction, f64, f64) -> Message>>,
75}
76
77impl<'a, Message: 'static> DndDestination<'a, Message> {
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        }
96    }
97
98    pub fn for_data<T: AllowedMimeTypes>(
99        child: impl Into<Element<'a, Message>>,
100        on_finish: impl Fn(Option<T>, DndAction) -> Message + 'static,
101    ) -> Self {
102        Self {
103            id: Id::unique(),
104            drag_id: None,
105            mime_types: T::allowed().iter().cloned().map(Cow::Owned).collect(),
106            preferred_action: DndAction::Move,
107            action: DndAction::Copy | DndAction::Move,
108            container: child.into(),
109            forward_drag_as_cursor: false,
110            on_hold: None,
111            on_drop: None,
112            on_enter: None,
113            on_leave: None,
114            on_motion: None,
115            on_action_selected: None,
116            on_data_received: None,
117            on_finish: Some(Box::new(move |mime, data, action, _, _| {
118                on_finish(T::try_from((data, mime)).ok(), action)
119            })),
120        }
121    }
122
123    #[must_use]
124    pub fn data_received_for<T: AllowedMimeTypes>(
125        mut self,
126        f: impl Fn(Option<T>) -> Message + 'static,
127    ) -> Self {
128        self.on_data_received = Some(Box::new(
129            move |mime, data| f(T::try_from((data, mime)).ok()),
130        ));
131        self
132    }
133
134    pub fn with_id(
135        child: impl Into<Element<'a, Message>>,
136        id: Id,
137        mimes: Vec<Cow<'static, str>>,
138    ) -> Self {
139        Self {
140            id,
141            drag_id: None,
142            mime_types: mimes,
143            preferred_action: DndAction::Move,
144            action: DndAction::Copy | DndAction::Move,
145            container: child.into(),
146            forward_drag_as_cursor: false,
147            on_hold: None,
148            on_drop: None,
149            on_enter: None,
150            on_leave: None,
151            on_motion: None,
152            on_action_selected: None,
153            on_data_received: None,
154            on_finish: None,
155        }
156    }
157
158    #[must_use]
159    pub fn drag_id(mut self, id: u64) -> Self {
160        self.drag_id = Some(id);
161        self
162    }
163
164    #[must_use]
165    pub fn action(mut self, action: DndAction) -> Self {
166        self.action = action;
167        self
168    }
169
170    #[must_use]
171    pub fn preferred_action(mut self, action: DndAction) -> Self {
172        self.preferred_action = action;
173        self
174    }
175
176    #[must_use]
177    pub fn forward_drag_as_cursor(mut self, forward: bool) -> Self {
178        self.forward_drag_as_cursor = forward;
179        self
180    }
181
182    #[must_use]
183    pub fn on_hold(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self {
184        self.on_hold = Some(Box::new(f));
185        self
186    }
187
188    #[must_use]
189    pub fn on_drop(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self {
190        self.on_drop = Some(Box::new(f));
191        self
192    }
193
194    #[must_use]
195    pub fn on_enter(mut self, f: impl Fn(f64, f64, Vec<String>) -> Message + 'static) -> Self {
196        self.on_enter = Some(Box::new(f));
197        self
198    }
199
200    #[must_use]
201    pub fn on_leave(mut self, m: impl Fn() -> Message + 'static) -> Self {
202        self.on_leave = Some(Box::new(m));
203        self
204    }
205
206    #[must_use]
207    pub fn on_finish(
208        mut self,
209        f: impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message + 'static,
210    ) -> Self {
211        self.on_finish = Some(Box::new(f));
212        self
213    }
214
215    #[must_use]
216    pub fn on_motion(mut self, f: impl Fn(f64, f64) -> Message + 'static) -> Self {
217        self.on_motion = Some(Box::new(f));
218        self
219    }
220
221    #[must_use]
222    pub fn on_action_selected(mut self, f: impl Fn(DndAction) -> Message + 'static) -> Self {
223        self.on_action_selected = Some(Box::new(f));
224        self
225    }
226
227    #[must_use]
228    pub fn on_data_received(mut self, f: impl Fn(String, Vec<u8>) -> Message + 'static) -> Self {
229        self.on_data_received = Some(Box::new(f));
230        self
231    }
232
233    /// Returns the drag id of the destination.
234    ///
235    /// # Panics
236    /// Panics if the destination has been assigned a Set id, which is invalid.
237    #[must_use]
238    pub fn get_drag_id(&self) -> u128 {
239        u128::from(self.drag_id.unwrap_or_else(|| match &self.id.0 {
240            Internal::Unique(id) | Internal::Custom(id, _) => *id,
241            Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."),
242        }))
243    }
244}
245
246impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
247    for DndDestination<'_, Message>
248{
249    fn children(&self) -> Vec<Tree> {
250        vec![Tree::new(&self.container)]
251    }
252
253    fn tag(&self) -> iced_core::widget::tree::Tag {
254        tree::Tag::of::<State<()>>()
255    }
256
257    fn diff(&mut self, tree: &mut Tree) {
258        tree.children[0].diff(self.container.as_widget_mut());
259    }
260
261    fn state(&self) -> iced_core::widget::tree::State {
262        tree::State::new(State::<()>::new())
263    }
264
265    fn size(&self) -> iced_core::Size<Length> {
266        self.container.as_widget().size()
267    }
268
269    fn layout(
270        &self,
271        tree: &mut Tree,
272        renderer: &crate::Renderer,
273        limits: &layout::Limits,
274    ) -> layout::Node {
275        self.container
276            .as_widget()
277            .layout(&mut tree.children[0], renderer, limits)
278    }
279
280    fn operate(
281        &self,
282        tree: &mut Tree,
283        layout: layout::Layout<'_>,
284        renderer: &crate::Renderer,
285        operation: &mut dyn iced_core::widget::Operation<()>,
286    ) {
287        self.container
288            .as_widget()
289            .operate(&mut tree.children[0], layout, renderer, operation);
290    }
291
292    #[allow(clippy::too_many_lines)]
293    fn on_event(
294        &mut self,
295        tree: &mut Tree,
296        event: Event,
297        layout: layout::Layout<'_>,
298        cursor: mouse::Cursor,
299        renderer: &crate::Renderer,
300        clipboard: &mut dyn Clipboard,
301        shell: &mut Shell<'_, Message>,
302        viewport: &Rectangle,
303    ) -> event::Status {
304        let s = self.container.as_widget_mut().on_event(
305            &mut tree.children[0],
306            event.clone(),
307            layout,
308            cursor,
309            renderer,
310            clipboard,
311            shell,
312            viewport,
313        );
314        if matches!(s, event::Status::Captured) {
315            return event::Status::Captured;
316        }
317
318        let state = tree.state.downcast_mut::<State<()>>();
319
320        let my_id = self.get_drag_id();
321
322        match event {
323            Event::Dnd(DndEvent::Offer(
324                id,
325                OfferEvent::Enter {
326                    x, y, mime_types, ..
327                },
328            )) if id == Some(my_id) => {
329                if let Some(msg) = state.on_enter(
330                    x,
331                    y,
332                    mime_types,
333                    self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
334                    (),
335                ) {
336                    shell.publish(msg);
337                }
338                if self.forward_drag_as_cursor {
339                    #[allow(clippy::cast_possible_truncation)]
340                    let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into());
341                    let event = Event::Mouse(mouse::Event::CursorMoved {
342                        position: drag_cursor.position().unwrap(),
343                    });
344                    self.container.as_widget_mut().on_event(
345                        &mut tree.children[0],
346                        event,
347                        layout,
348                        drag_cursor,
349                        renderer,
350                        clipboard,
351                        shell,
352                        viewport,
353                    );
354                }
355                return event::Status::Captured;
356            }
357            Event::Dnd(DndEvent::Offer(id, OfferEvent::Leave)) if id == Some(my_id) => {
358                state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref));
359
360                if self.forward_drag_as_cursor {
361                    let drag_cursor = mouse::Cursor::Unavailable;
362                    let event = Event::Mouse(mouse::Event::CursorLeft);
363                    self.container.as_widget_mut().on_event(
364                        &mut tree.children[0],
365                        event,
366                        layout,
367                        drag_cursor,
368                        renderer,
369                        clipboard,
370                        shell,
371                        viewport,
372                    );
373                }
374                return event::Status::Captured;
375            }
376            Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => {
377                if let Some(msg) = state.on_motion(
378                    x,
379                    y,
380                    self.on_motion.as_ref().map(std::convert::AsRef::as_ref),
381                    self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
382                    (),
383                ) {
384                    shell.publish(msg);
385                }
386
387                if self.forward_drag_as_cursor {
388                    #[allow(clippy::cast_possible_truncation)]
389                    let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into());
390                    let event = Event::Mouse(mouse::Event::CursorMoved {
391                        position: drag_cursor.position().unwrap(),
392                    });
393                    self.container.as_widget_mut().on_event(
394                        &mut tree.children[0],
395                        event,
396                        layout,
397                        drag_cursor,
398                        renderer,
399                        clipboard,
400                        shell,
401                        viewport,
402                    );
403                }
404                return event::Status::Captured;
405            }
406            Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if id == Some(my_id) => {
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                return event::Status::Captured;
413            }
414            Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => {
415                if let Some(msg) =
416                    state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref))
417                {
418                    shell.publish(msg);
419                }
420                return event::Status::Captured;
421            }
422            Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action)))
423                if id == Some(my_id) =>
424            {
425                if let Some(msg) = state.on_action_selected(
426                    action,
427                    self.on_action_selected
428                        .as_ref()
429                        .map(std::convert::AsRef::as_ref),
430                ) {
431                    shell.publish(msg);
432                }
433                return event::Status::Captured;
434            }
435            Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type }))
436                if id == Some(my_id) =>
437            {
438                if let (Some(msg), ret) = state.on_data_received(
439                    mime_type,
440                    data,
441                    self.on_data_received
442                        .as_ref()
443                        .map(std::convert::AsRef::as_ref),
444                    self.on_finish.as_ref().map(std::convert::AsRef::as_ref),
445                ) {
446                    shell.publish(msg);
447                    return ret;
448                }
449                return event::Status::Captured;
450            }
451            _ => {}
452        }
453        event::Status::Ignored
454    }
455
456    fn mouse_interaction(
457        &self,
458        tree: &Tree,
459        layout: layout::Layout<'_>,
460        cursor_position: mouse::Cursor,
461        viewport: &Rectangle,
462        renderer: &crate::Renderer,
463    ) -> mouse::Interaction {
464        self.container.as_widget().mouse_interaction(
465            &tree.children[0],
466            layout,
467            cursor_position,
468            viewport,
469            renderer,
470        )
471    }
472
473    fn draw(
474        &self,
475        tree: &Tree,
476        renderer: &mut crate::Renderer,
477        theme: &crate::Theme,
478        renderer_style: &iced_core::renderer::Style,
479        layout: layout::Layout<'_>,
480        cursor_position: mouse::Cursor,
481        viewport: &Rectangle,
482    ) {
483        self.container.as_widget().draw(
484            &tree.children[0],
485            renderer,
486            theme,
487            renderer_style,
488            layout,
489            cursor_position,
490            viewport,
491        );
492    }
493
494    fn overlay<'b>(
495        &'b mut self,
496        tree: &'b mut Tree,
497        layout: layout::Layout<'_>,
498        renderer: &crate::Renderer,
499        translation: Vector,
500    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
501        self.container
502            .as_widget_mut()
503            .overlay(&mut tree.children[0], layout, renderer, translation)
504    }
505
506    fn drag_destinations(
507        &self,
508        state: &Tree,
509        layout: layout::Layout<'_>,
510        renderer: &crate::Renderer,
511        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
512    ) {
513        let bounds = layout.bounds();
514        let my_id = self.get_drag_id();
515        let my_dest = DndDestinationRectangle {
516            id: my_id,
517            rectangle: dnd::Rectangle {
518                x: f64::from(bounds.x),
519                y: f64::from(bounds.y),
520                width: f64::from(bounds.width),
521                height: f64::from(bounds.height),
522            },
523            mime_types: self.mime_types.clone(),
524            actions: self.action,
525            preferred: self.preferred_action,
526        };
527        dnd_rectangles.push(my_dest);
528
529        self.container.as_widget().drag_destinations(
530            &state.children[0],
531            layout,
532            renderer,
533            dnd_rectangles,
534        );
535    }
536
537    fn id(&self) -> Option<Id> {
538        Some(self.id.clone())
539    }
540
541    fn set_id(&mut self, id: Id) {
542        self.id = id;
543    }
544
545    #[cfg(feature = "a11y")]
546    /// get the a11y nodes for the widget
547    fn a11y_nodes(
548        &self,
549        layout: iced_core::Layout<'_>,
550        state: &Tree,
551        p: mouse::Cursor,
552    ) -> iced_accessibility::A11yTree {
553        let c_state = &state.children[0];
554        self.container.as_widget().a11y_nodes(layout, c_state, p)
555    }
556}
557
558#[derive(Default)]
559pub struct State<T> {
560    pub drag_offer: Option<DragOffer<T>>,
561}
562
563pub struct DragOffer<T> {
564    pub x: f64,
565    pub y: f64,
566    pub dropped: bool,
567    pub selected_action: DndAction,
568    pub data: T,
569}
570
571impl<T> State<T> {
572    #[must_use]
573    pub fn new() -> Self {
574        Self { drag_offer: None }
575    }
576
577    pub fn on_enter<Message>(
578        &mut self,
579        x: f64,
580        y: f64,
581        mime_types: Vec<String>,
582        on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
583        data: T,
584    ) -> Option<Message> {
585        self.drag_offer = Some(DragOffer {
586            x,
587            y,
588            dropped: false,
589            selected_action: DndAction::empty(),
590            data,
591        });
592        on_enter.map(|f| f(x, y, mime_types))
593    }
594
595    pub fn on_leave<Message>(&mut self, on_leave: Option<&dyn Fn() -> Message>) -> Option<Message> {
596        if self.drag_offer.as_ref().is_some_and(|d| !d.dropped) {
597            self.drag_offer = None;
598            on_leave.map(|f| f())
599        } else {
600            None
601        }
602    }
603
604    pub fn on_motion<Message>(
605        &mut self,
606        x: f64,
607        y: f64,
608        on_motion: Option<impl Fn(f64, f64) -> Message>,
609        on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
610        data: T,
611    ) -> Option<Message> {
612        if let Some(s) = self.drag_offer.as_mut() {
613            s.x = x;
614            s.y = y;
615        } else {
616            self.drag_offer = Some(DragOffer {
617                x,
618                y,
619                dropped: false,
620                selected_action: DndAction::empty(),
621                data,
622            });
623            if let Some(f) = on_enter {
624                return Some(f(x, y, vec![]));
625            }
626        }
627        on_motion.map(|f| f(x, y))
628    }
629
630    pub fn on_drop<Message>(
631        &mut self,
632        on_drop: Option<impl Fn(f64, f64) -> Message>,
633    ) -> Option<Message> {
634        if let Some(offer) = self.drag_offer.as_mut() {
635            offer.dropped = true;
636            if let Some(f) = on_drop {
637                return Some(f(offer.x, offer.y));
638            }
639        }
640        None
641    }
642
643    pub fn on_action_selected<Message>(
644        &mut self,
645        action: DndAction,
646        on_action_selected: Option<impl Fn(DndAction) -> Message>,
647    ) -> Option<Message> {
648        if let Some(s) = self.drag_offer.as_mut() {
649            s.selected_action = action;
650        }
651        if let Some(f) = on_action_selected {
652            f(action).into()
653        } else {
654            None
655        }
656    }
657
658    pub fn on_data_received<Message>(
659        &mut self,
660        mime: String,
661        data: Vec<u8>,
662        on_data_received: Option<impl Fn(String, Vec<u8>) -> Message>,
663        on_finish: Option<impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message>,
664    ) -> (Option<Message>, event::Status) {
665        let Some(dnd) = self.drag_offer.as_ref() else {
666            self.drag_offer = None;
667            return (None, event::Status::Ignored);
668        };
669
670        if dnd.dropped {
671            let ret = (
672                on_finish.map(|f| f(mime, data, dnd.selected_action, dnd.x, dnd.y)),
673                event::Status::Captured,
674            );
675            self.drag_offer = None;
676            ret
677        } else if let Some(f) = on_data_received {
678            (Some(f(mime, data)), event::Status::Captured)
679        } else {
680            (None, event::Status::Ignored)
681        }
682    }
683}
684
685impl<'a, Message: 'static> From<DndDestination<'a, Message>> for Element<'a, Message> {
686    fn from(wrapper: DndDestination<'a, Message>) -> Self {
687        Element::new(wrapper)
688    }
689}