cosmic/widget/
dnd_source.rs

1use std::any::Any;
2
3use iced_core::{widget::Operation, window};
4
5use crate::{
6    Element,
7    widget::{Id, Widget, container},
8};
9use iced::{
10    Event, Length, Point, Rectangle, Vector,
11    clipboard::dnd::{DndAction, DndEvent, SourceEvent},
12    event, mouse, overlay,
13};
14use iced_core::{
15    self, Clipboard, Shell, layout, renderer,
16    widget::{Tree, tree},
17};
18
19pub fn dnd_source<
20    'a,
21    Message: Clone + 'static,
22    D: iced::clipboard::mime::AsMimeTypes + Send + 'static,
23>(
24    child: impl Into<Element<'a, Message>>,
25) -> DndSource<'a, Message, D> {
26    DndSource::new(child)
27}
28
29pub struct DndSource<'a, Message, D> {
30    id: Id,
31    action: DndAction,
32    container: Element<'a, Message>,
33    window: Option<window::Id>,
34    drag_content: Option<Box<dyn Fn() -> D>>,
35    drag_icon: Option<Box<dyn Fn(Vector) -> (Element<'static, ()>, tree::State, Vector)>>,
36    on_start: Option<Message>,
37    on_cancelled: Option<Message>,
38    on_finish: Option<Message>,
39    drag_threshold: f32,
40}
41
42impl<
43    'a,
44    Message: Clone + 'static,
45    D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
46> DndSource<'a, Message, D>
47{
48    pub fn new(child: impl Into<Element<'a, Message>>) -> Self {
49        Self {
50            id: Id::unique(),
51            window: None,
52            action: DndAction::Copy | DndAction::Move,
53            container: container(child).into(),
54            drag_content: None,
55            drag_icon: None,
56            drag_threshold: 8.0,
57            on_start: None,
58            on_cancelled: None,
59            on_finish: None,
60        }
61    }
62
63    pub fn with_id(child: impl Into<Element<'a, Message>>, id: Id) -> Self {
64        Self {
65            id,
66            window: None,
67            action: DndAction::Copy | DndAction::Move,
68            container: container(child).into(),
69            drag_content: None,
70            drag_icon: None,
71            drag_threshold: 8.0,
72            on_start: None,
73            on_cancelled: None,
74            on_finish: None,
75        }
76    }
77
78    #[must_use]
79    pub fn action(mut self, action: DndAction) -> Self {
80        self.action = action;
81        self
82    }
83
84    #[must_use]
85    pub fn drag_content(mut self, f: impl Fn() -> D + 'static) -> Self {
86        self.drag_content = Some(Box::new(f));
87        self
88    }
89
90    #[must_use]
91    pub fn drag_icon(
92        mut self,
93        f: impl Fn(Vector) -> (Element<'static, ()>, tree::State, Vector) + 'static,
94    ) -> Self {
95        self.drag_icon = Some(Box::new(f));
96        self
97    }
98
99    #[must_use]
100    pub fn drag_threshold(mut self, threshold: f32) -> Self {
101        self.drag_threshold = threshold;
102        self
103    }
104
105    pub fn start_dnd(&self, clipboard: &mut dyn Clipboard, bounds: Rectangle, offset: Vector) {
106        let Some(content) = self.drag_content.as_ref().map(|f| f()) else {
107            return;
108        };
109
110        iced_core::clipboard::start_dnd(
111            clipboard,
112            false,
113            if let Some(window) = self.window.as_ref() {
114                Some(iced_core::clipboard::DndSource::Surface(*window))
115            } else {
116                Some(iced_core::clipboard::DndSource::Widget(self.id.clone()))
117            },
118            self.drag_icon.as_ref().map(|f| {
119                let (icon, state, offset) = f(offset);
120                iced_core::clipboard::IconSurface::new(
121                    container(icon)
122                        .width(Length::Fixed(bounds.width))
123                        .height(Length::Fixed(bounds.height))
124                        .into(),
125                    state,
126                    offset,
127                )
128            }),
129            Box::new(content),
130            self.action,
131        );
132    }
133
134    #[must_use]
135    pub fn on_start(mut self, on_start: Option<Message>) -> Self {
136        self.on_start = on_start;
137        self
138    }
139
140    #[must_use]
141    pub fn on_cancel(mut self, on_cancelled: Option<Message>) -> Self {
142        self.on_cancelled = on_cancelled;
143        self
144    }
145
146    #[must_use]
147    pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
148        self.on_finish = on_finish;
149        self
150    }
151
152    #[must_use]
153    pub fn window(mut self, window: window::Id) -> Self {
154        self.window = Some(window);
155        self
156    }
157}
158
159impl<Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static>
160    Widget<Message, crate::Theme, crate::Renderer> for DndSource<'_, Message, D>
161{
162    fn children(&self) -> Vec<Tree> {
163        vec![Tree::new(&self.container)]
164    }
165
166    fn tag(&self) -> iced_core::widget::tree::Tag {
167        tree::Tag::of::<State>()
168    }
169
170    fn diff(&mut self, tree: &mut Tree) {
171        tree.diff_children(std::slice::from_mut(&mut self.container));
172    }
173
174    fn state(&self) -> iced_core::widget::tree::State {
175        tree::State::new(State::new())
176    }
177
178    fn size(&self) -> iced_core::Size<Length> {
179        self.container.as_widget().size()
180    }
181
182    fn layout(
183        &mut self,
184        tree: &mut Tree,
185        renderer: &crate::Renderer,
186        limits: &layout::Limits,
187    ) -> layout::Node {
188        let state = tree.state.downcast_mut::<State>();
189        let node = self
190            .container
191            .as_widget_mut()
192            .layout(&mut tree.children[0], renderer, limits);
193        state.cached_bounds = node.bounds();
194        node
195    }
196
197    fn operate(
198        &mut self,
199        tree: &mut Tree,
200        layout: layout::Layout<'_>,
201        renderer: &crate::Renderer,
202        operation: &mut dyn Operation,
203    ) {
204        operation.custom(
205            Some(&self.id),
206            layout.bounds(),
207            (&mut tree.state) as &mut dyn Any,
208        );
209
210        self.container
211            .as_widget_mut()
212            .operate(&mut tree.children[0], layout, renderer, operation);
213    }
214
215    fn update(
216        &mut self,
217        tree: &mut Tree,
218        event: &Event,
219        layout: layout::Layout<'_>,
220        cursor: mouse::Cursor,
221        renderer: &crate::Renderer,
222        clipboard: &mut dyn Clipboard,
223        shell: &mut Shell<'_, Message>,
224        viewport: &Rectangle,
225    ) {
226        self.container.as_widget_mut().update(
227            &mut tree.children[0],
228            event,
229            layout,
230            cursor,
231            renderer,
232            clipboard,
233            shell,
234            viewport,
235        );
236
237        let state = tree.state.downcast_mut::<State>();
238
239        match event {
240            Event::Mouse(mouse_event) => match mouse_event {
241                mouse::Event::ButtonPressed(mouse::Button::Left) => {
242                    if let Some(position) = cursor.position() {
243                        if !cursor.is_over(layout.bounds()) {
244                            return;
245                        }
246
247                        state.left_pressed_position = Some(position);
248                        shell.capture_event();
249                    }
250                }
251                mouse::Event::ButtonReleased(mouse::Button::Left)
252                    if state.left_pressed_position.is_some() =>
253                {
254                    state.left_pressed_position = None;
255                    shell.capture_event();
256                }
257                mouse::Event::CursorMoved { .. } => {
258                    if let Some(position) = cursor.position() {
259                        // We ignore motion if we do not possess drag content by now.
260                        if self.drag_content.is_none() {
261                            state.left_pressed_position = None;
262                            return;
263                        }
264                        if let Some(left_pressed_position) = state.left_pressed_position
265                            && position.distance(left_pressed_position) > self.drag_threshold
266                        {
267                            if let Some(on_start) = self.on_start.as_ref() {
268                                shell.publish(on_start.clone());
269                            }
270                            let offset = Vector::new(
271                                left_pressed_position.x - layout.bounds().x,
272                                left_pressed_position.y - layout.bounds().y,
273                            );
274                            self.start_dnd(clipboard, state.cached_bounds, offset);
275                            state.is_dragging = true;
276                            state.left_pressed_position = None;
277                        }
278                        if !cursor.is_over(layout.bounds()) {
279                            return;
280                        }
281                        shell.capture_event();
282                    }
283                }
284                _ => (),
285            },
286            Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => {
287                if state.is_dragging {
288                    if let Some(m) = self.on_cancelled.as_ref() {
289                        shell.publish(m.clone());
290                    }
291                    state.is_dragging = false;
292                    shell.capture_event();
293                }
294            }
295            Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => {
296                if state.is_dragging {
297                    if let Some(m) = self.on_finish.as_ref() {
298                        shell.publish(m.clone());
299                    }
300                    state.is_dragging = false;
301                    shell.capture_event();
302                }
303            }
304            _ => (),
305        }
306    }
307
308    fn mouse_interaction(
309        &self,
310        tree: &Tree,
311        layout: layout::Layout<'_>,
312        cursor_position: mouse::Cursor,
313        viewport: &Rectangle,
314        renderer: &crate::Renderer,
315    ) -> mouse::Interaction {
316        let state = tree.state.downcast_ref::<State>();
317        if state.is_dragging {
318            return mouse::Interaction::Grabbing;
319        }
320        self.container.as_widget().mouse_interaction(
321            &tree.children[0],
322            layout,
323            cursor_position,
324            viewport,
325            renderer,
326        )
327    }
328
329    fn draw(
330        &self,
331        tree: &Tree,
332        renderer: &mut crate::Renderer,
333        theme: &crate::Theme,
334        renderer_style: &renderer::Style,
335        layout: layout::Layout<'_>,
336        cursor_position: mouse::Cursor,
337        viewport: &Rectangle,
338    ) {
339        self.container.as_widget().draw(
340            &tree.children[0],
341            renderer,
342            theme,
343            renderer_style,
344            layout,
345            cursor_position,
346            viewport,
347        );
348    }
349
350    fn overlay<'b>(
351        &'b mut self,
352        tree: &'b mut Tree,
353        layout: layout::Layout<'b>,
354        renderer: &crate::Renderer,
355        viewport: &Rectangle,
356        translation: Vector,
357    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
358        self.container.as_widget_mut().overlay(
359            &mut tree.children[0],
360            layout,
361            renderer,
362            viewport,
363            translation,
364        )
365    }
366
367    fn drag_destinations(
368        &self,
369        state: &Tree,
370        layout: layout::Layout<'_>,
371        renderer: &crate::Renderer,
372        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
373    ) {
374        self.container.as_widget().drag_destinations(
375            &state.children[0],
376            layout,
377            renderer,
378            dnd_rectangles,
379        );
380    }
381
382    fn id(&self) -> Option<Id> {
383        Some(self.id.clone())
384    }
385
386    fn set_id(&mut self, id: Id) {
387        self.id = id;
388    }
389
390    #[cfg(feature = "a11y")]
391    /// get the a11y nodes for the widget
392    fn a11y_nodes(
393        &self,
394        layout: iced_core::Layout<'_>,
395        state: &Tree,
396        p: mouse::Cursor,
397    ) -> iced_accessibility::A11yTree {
398        let c_state = &state.children[0];
399        self.container.as_widget().a11y_nodes(layout, c_state, p)
400    }
401}
402
403impl<
404    'a,
405    Message: Clone + 'static,
406    D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
407> From<DndSource<'a, Message, D>> for Element<'a, Message>
408{
409    fn from(e: DndSource<'a, Message, D>) -> Element<'a, Message> {
410        Element::new(e)
411    }
412}
413
414/// Local state of the [`MouseListener`].
415#[derive(Debug, Default)]
416struct State {
417    left_pressed_position: Option<Point>,
418    is_dragging: bool,
419    cached_bounds: Rectangle,
420}
421
422impl State {
423    fn new() -> Self {
424        Self::default()
425    }
426}