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    pub fn id(mut self, id: Id) -> Self {
246        self.id = id;
247        self
248    }
249}
250
251impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
252    for DndDestination<'_, Message>
253{
254    fn children(&self) -> Vec<Tree> {
255        vec![Tree::new(&self.container)]
256    }
257
258    fn tag(&self) -> iced_core::widget::tree::Tag {
259        tree::Tag::of::<State<()>>()
260    }
261
262    fn diff(&mut self, tree: &mut Tree) {
263        tree.children[0].diff(self.container.as_widget_mut());
264    }
265
266    fn state(&self) -> iced_core::widget::tree::State {
267        tree::State::new(State::<()>::new())
268    }
269
270    fn size(&self) -> iced_core::Size<Length> {
271        self.container.as_widget().size()
272    }
273
274    fn layout(
275        &self,
276        tree: &mut Tree,
277        renderer: &crate::Renderer,
278        limits: &layout::Limits,
279    ) -> layout::Node {
280        self.container
281            .as_widget()
282            .layout(&mut tree.children[0], renderer, limits)
283    }
284
285    fn operate(
286        &self,
287        tree: &mut Tree,
288        layout: layout::Layout<'_>,
289        renderer: &crate::Renderer,
290        operation: &mut dyn iced_core::widget::Operation<()>,
291    ) {
292        self.container
293            .as_widget()
294            .operate(&mut tree.children[0], layout, renderer, operation);
295    }
296
297    #[allow(clippy::too_many_lines)]
298    fn on_event(
299        &mut self,
300        tree: &mut Tree,
301        event: Event,
302        layout: layout::Layout<'_>,
303        cursor: mouse::Cursor,
304        renderer: &crate::Renderer,
305        clipboard: &mut dyn Clipboard,
306        shell: &mut Shell<'_, Message>,
307        viewport: &Rectangle,
308    ) -> event::Status {
309        let s = self.container.as_widget_mut().on_event(
310            &mut tree.children[0],
311            event.clone(),
312            layout,
313            cursor,
314            renderer,
315            clipboard,
316            shell,
317            viewport,
318        );
319        if matches!(s, event::Status::Captured) {
320            return event::Status::Captured;
321        }
322
323        let state = tree.state.downcast_mut::<State<()>>();
324
325        let my_id = self.get_drag_id();
326
327        match event {
328            Event::Dnd(DndEvent::Offer(
329                id,
330                OfferEvent::Enter {
331                    x, y, mime_types, ..
332                },
333            )) if id == Some(my_id) => {
334                if let Some(msg) = state.on_enter(
335                    x,
336                    y,
337                    mime_types,
338                    self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
339                    (),
340                ) {
341                    shell.publish(msg);
342                }
343                if self.forward_drag_as_cursor {
344                    #[allow(clippy::cast_possible_truncation)]
345                    let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into());
346                    let event = Event::Mouse(mouse::Event::CursorMoved {
347                        position: drag_cursor.position().unwrap(),
348                    });
349                    self.container.as_widget_mut().on_event(
350                        &mut tree.children[0],
351                        event,
352                        layout,
353                        drag_cursor,
354                        renderer,
355                        clipboard,
356                        shell,
357                        viewport,
358                    );
359                }
360                return event::Status::Captured;
361            }
362            Event::Dnd(DndEvent::Offer(id, OfferEvent::Leave)) => {
363                if let Some(msg) =
364                    state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref))
365                {
366                    shell.publish(msg);
367                }
368
369                if self.forward_drag_as_cursor {
370                    let drag_cursor = mouse::Cursor::Unavailable;
371                    let event = Event::Mouse(mouse::Event::CursorLeft);
372                    self.container.as_widget_mut().on_event(
373                        &mut tree.children[0],
374                        event,
375                        layout,
376                        drag_cursor,
377                        renderer,
378                        clipboard,
379                        shell,
380                        viewport,
381                    );
382                }
383                return event::Status::Captured;
384            }
385            Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => {
386                if let Some(msg) = state.on_motion(
387                    x,
388                    y,
389                    self.on_motion.as_ref().map(std::convert::AsRef::as_ref),
390                    self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
391                    (),
392                ) {
393                    shell.publish(msg);
394                }
395
396                if self.forward_drag_as_cursor {
397                    #[allow(clippy::cast_possible_truncation)]
398                    let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into());
399                    let event = Event::Mouse(mouse::Event::CursorMoved {
400                        position: drag_cursor.position().unwrap(),
401                    });
402                    self.container.as_widget_mut().on_event(
403                        &mut tree.children[0],
404                        event,
405                        layout,
406                        drag_cursor,
407                        renderer,
408                        clipboard,
409                        shell,
410                        viewport,
411                    );
412                }
413                return event::Status::Captured;
414            }
415            Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) => {
416                if let Some(msg) =
417                    state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref))
418                {
419                    shell.publish(msg);
420                }
421                return event::Status::Captured;
422            }
423            Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => {
424                if let Some(msg) =
425                    state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref))
426                {
427                    shell.publish(msg);
428                }
429                return event::Status::Captured;
430            }
431            Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action)))
432                if id == Some(my_id) =>
433            {
434                if let Some(msg) = state.on_action_selected(
435                    action,
436                    self.on_action_selected
437                        .as_ref()
438                        .map(std::convert::AsRef::as_ref),
439                ) {
440                    shell.publish(msg);
441                }
442                return event::Status::Captured;
443            }
444            Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type }))
445                if id == Some(my_id) =>
446            {
447                if let (Some(msg), ret) = state.on_data_received(
448                    mime_type,
449                    data,
450                    self.on_data_received
451                        .as_ref()
452                        .map(std::convert::AsRef::as_ref),
453                    self.on_finish.as_ref().map(std::convert::AsRef::as_ref),
454                ) {
455                    shell.publish(msg);
456                    return ret;
457                }
458                return event::Status::Captured;
459            }
460            _ => {}
461        }
462        event::Status::Ignored
463    }
464
465    fn mouse_interaction(
466        &self,
467        tree: &Tree,
468        layout: layout::Layout<'_>,
469        cursor_position: mouse::Cursor,
470        viewport: &Rectangle,
471        renderer: &crate::Renderer,
472    ) -> mouse::Interaction {
473        self.container.as_widget().mouse_interaction(
474            &tree.children[0],
475            layout,
476            cursor_position,
477            viewport,
478            renderer,
479        )
480    }
481
482    fn draw(
483        &self,
484        tree: &Tree,
485        renderer: &mut crate::Renderer,
486        theme: &crate::Theme,
487        renderer_style: &iced_core::renderer::Style,
488        layout: layout::Layout<'_>,
489        cursor_position: mouse::Cursor,
490        viewport: &Rectangle,
491    ) {
492        self.container.as_widget().draw(
493            &tree.children[0],
494            renderer,
495            theme,
496            renderer_style,
497            layout,
498            cursor_position,
499            viewport,
500        );
501    }
502
503    fn overlay<'b>(
504        &'b mut self,
505        tree: &'b mut Tree,
506        layout: layout::Layout<'_>,
507        renderer: &crate::Renderer,
508        translation: Vector,
509    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
510        self.container
511            .as_widget_mut()
512            .overlay(&mut tree.children[0], layout, renderer, translation)
513    }
514
515    fn drag_destinations(
516        &self,
517        state: &Tree,
518        layout: layout::Layout<'_>,
519        renderer: &crate::Renderer,
520        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
521    ) {
522        let bounds = layout.bounds();
523        let my_id = self.get_drag_id();
524        let my_dest = DndDestinationRectangle {
525            id: my_id,
526            rectangle: dnd::Rectangle {
527                x: f64::from(bounds.x),
528                y: f64::from(bounds.y),
529                width: f64::from(bounds.width),
530                height: f64::from(bounds.height),
531            },
532            mime_types: self.mime_types.clone(),
533            actions: self.action,
534            preferred: self.preferred_action,
535        };
536        dnd_rectangles.push(my_dest);
537
538        self.container.as_widget().drag_destinations(
539            &state.children[0],
540            layout,
541            renderer,
542            dnd_rectangles,
543        );
544    }
545
546    fn id(&self) -> Option<Id> {
547        Some(self.id.clone())
548    }
549
550    fn set_id(&mut self, id: Id) {
551        self.id = id;
552    }
553
554    #[cfg(feature = "a11y")]
555    /// get the a11y nodes for the widget
556    fn a11y_nodes(
557        &self,
558        layout: iced_core::Layout<'_>,
559        state: &Tree,
560        p: mouse::Cursor,
561    ) -> iced_accessibility::A11yTree {
562        let c_state = &state.children[0];
563        self.container.as_widget().a11y_nodes(layout, c_state, p)
564    }
565}
566
567#[derive(Default)]
568pub struct State<T> {
569    pub drag_offer: Option<DragOffer<T>>,
570}
571
572pub struct DragOffer<T> {
573    pub x: f64,
574    pub y: f64,
575    pub dropped: bool,
576    pub selected_action: DndAction,
577    pub data: T,
578}
579
580impl<T> State<T> {
581    #[must_use]
582    pub fn new() -> Self {
583        Self { drag_offer: None }
584    }
585
586    pub fn on_enter<Message>(
587        &mut self,
588        x: f64,
589        y: f64,
590        mime_types: Vec<String>,
591        on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
592        data: T,
593    ) -> Option<Message> {
594        self.drag_offer = Some(DragOffer {
595            x,
596            y,
597            dropped: false,
598            selected_action: DndAction::empty(),
599            data,
600        });
601        on_enter.map(|f| f(x, y, mime_types))
602    }
603
604    pub fn on_leave<Message>(&mut self, on_leave: Option<&dyn Fn() -> Message>) -> Option<Message> {
605        if self.drag_offer.as_ref().is_some_and(|d| !d.dropped) {
606            self.drag_offer = None;
607            on_leave.map(|f| f())
608        } else {
609            None
610        }
611    }
612
613    pub fn on_motion<Message>(
614        &mut self,
615        x: f64,
616        y: f64,
617        on_motion: Option<impl Fn(f64, f64) -> Message>,
618        on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
619        data: T,
620    ) -> Option<Message> {
621        if let Some(s) = self.drag_offer.as_mut() {
622            s.x = x;
623            s.y = y;
624        } else {
625            self.drag_offer = Some(DragOffer {
626                x,
627                y,
628                dropped: false,
629                selected_action: DndAction::empty(),
630                data,
631            });
632            if let Some(f) = on_enter {
633                return Some(f(x, y, vec![]));
634            }
635        }
636        on_motion.map(|f| f(x, y))
637    }
638
639    pub fn on_drop<Message>(
640        &mut self,
641        on_drop: Option<impl Fn(f64, f64) -> Message>,
642    ) -> Option<Message> {
643        if let Some(offer) = self.drag_offer.as_mut() {
644            offer.dropped = true;
645            if let Some(f) = on_drop {
646                return Some(f(offer.x, offer.y));
647            }
648        }
649        None
650    }
651
652    pub fn on_action_selected<Message>(
653        &mut self,
654        action: DndAction,
655        on_action_selected: Option<impl Fn(DndAction) -> Message>,
656    ) -> Option<Message> {
657        if let Some(s) = self.drag_offer.as_mut() {
658            s.selected_action = action;
659        }
660        if let Some(f) = on_action_selected {
661            f(action).into()
662        } else {
663            None
664        }
665    }
666
667    pub fn on_data_received<Message>(
668        &mut self,
669        mime: String,
670        data: Vec<u8>,
671        on_data_received: Option<impl Fn(String, Vec<u8>) -> Message>,
672        on_finish: Option<impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message>,
673    ) -> (Option<Message>, event::Status) {
674        let Some(dnd) = self.drag_offer.as_ref() else {
675            self.drag_offer = None;
676            return (None, event::Status::Ignored);
677        };
678
679        if dnd.dropped {
680            let ret = (
681                on_finish.map(|f| f(mime, data, dnd.selected_action, dnd.x, dnd.y)),
682                event::Status::Captured,
683            );
684            self.drag_offer = None;
685            ret
686        } else if let Some(f) = on_data_received {
687            (Some(f(mime, data)), event::Status::Captured)
688        } else {
689            (None, event::Status::Ignored)
690        }
691    }
692}
693
694impl<'a, Message: 'static> From<DndDestination<'a, Message>> for Element<'a, Message> {
695    fn from(wrapper: DndDestination<'a, Message>) -> Self {
696        Element::new(wrapper)
697    }
698}