Skip to main content

iced_drop/widget/
droppable.rs

1//! Encapsulates a widget that can be dragged and dropped.
2use iced_core::layout::{Limits, Node};
3use iced_core::mouse::Cursor;
4use iced_core::renderer::Style;
5use iced_core::widget::tree::Tag;
6use iced_core::widget::{Id, Operation, Tree};
7use iced_core::{
8    Element, Event, Layout, Length, Pixels, Point, Rectangle, Size, Vector,
9    Widget, mouse, overlay, renderer, touch, window,
10};
11use std::fmt::Debug;
12use std::vec;
13
14/// An element that can be dragged and dropped on a [`DropZone`]
15pub struct Droppable<
16    'a,
17    Message,
18    Theme = iced_widget::Theme,
19    Renderer = iced_widget::Renderer,
20> where
21    Message: Clone,
22    Renderer: renderer::Renderer,
23{
24    content: Element<'a, Message, Theme, Renderer>,
25    id: Option<Id>,
26    drag_threshold: f32,
27    on_press: Option<Message>,
28    on_click: Option<Message>,
29    on_drop: Option<Box<dyn Fn(Point, Rectangle) -> Message + 'a>>,
30    on_drag: Option<Box<dyn Fn(Point, Rectangle) -> Message + 'a>>,
31    on_cancel: Option<Message>,
32    drag_mode: Option<(bool, bool)>,
33    drag_overlay: bool,
34    drag_hide: bool,
35    drag_center: bool,
36    drag_size: Option<Size>,
37    reset_delay: usize,
38    status: Option<Status>,
39}
40
41impl<'a, Message, Theme, Renderer> Droppable<'a, Message, Theme, Renderer>
42where
43    Message: Clone,
44    Renderer: renderer::Renderer,
45{
46    /// Creates a new [`Droppable`].
47    pub fn new(
48        content: impl Into<Element<'a, Message, Theme, Renderer>>,
49    ) -> Self {
50        Self {
51            content: content.into(),
52            id: None,
53            drag_threshold: 5.0,
54            on_press: None,
55            on_click: None,
56            on_drop: None,
57            on_drag: None,
58            on_cancel: None,
59            drag_mode: Some((true, true)),
60            drag_overlay: true,
61            drag_hide: false,
62            drag_center: false,
63            drag_size: None,
64            reset_delay: 0,
65            status: None,
66        }
67    }
68
69    /// Sets the unique identifier of the [`Droppable`].
70    pub fn id(mut self, id: Id) -> Self {
71        self.id = Some(id);
72        self
73    }
74
75    /// Sets the drag threshold of the [`Droppable`].
76    ///
77    /// This controls when [`on_press`] will be triggered (if set) after selecting a droppable and
78    /// moving the cursor.
79    ///
80    /// [`on_press`]: Droppable::on_press
81    pub fn drag_threshold(mut self, drag_threshold: impl Into<Pixels>) -> Self {
82        self.drag_threshold = drag_threshold.into().0;
83        self
84    }
85
86    /// Sets the message that will be produced when the [`Droppable`] is clicked.
87    ///
88    /// This will get triggered even if drag occurs.
89    pub fn on_click(mut self, message: Message) -> Self {
90        self.on_click = Some(message);
91        self
92    }
93
94    /// Sets the message that will be produced when the [`Droppable`] is pressed.
95    ///
96    /// This will get triggered only when not dragging.
97    pub fn on_press(mut self, message: Message) -> Self {
98        self.on_press = Some(message);
99        self
100    }
101
102    /// Sets the message that will be produced when the [`Droppable`] is dropped on a [`DropZone`].
103    ///
104    /// Unless this is set, the [`Droppable`] will be disabled.
105    pub fn on_drop<F>(mut self, message: F) -> Self
106    where
107        F: Fn(Point, Rectangle) -> Message + 'a,
108    {
109        self.on_drop = Some(Box::new(message));
110        self
111    }
112
113    /// Sets the message that will be produced when the [`Droppable`] is dragged.
114    pub fn on_drag<F>(mut self, message: F) -> Self
115    where
116        F: Fn(Point, Rectangle) -> Message + 'a,
117    {
118        self.on_drag = Some(Box::new(message));
119        self
120    }
121
122    /// Sets the message that will be produced when the user right clicks while dragging the [`Droppable`].
123    pub fn on_cancel(mut self, message: Message) -> Self {
124        self.on_cancel = Some(message);
125        self
126    }
127
128    /// Sets whether the [`Droppable`] should be drawn under the cursor while dragging.
129    pub fn drag_overlay(mut self, drag_overlay: bool) -> Self {
130        self.drag_overlay = drag_overlay;
131        self
132    }
133
134    /// Sets whether the [`Droppable`] should be hidden while dragging.
135    pub fn drag_hide(mut self, drag_hide: bool) -> Self {
136        self.drag_hide = drag_hide;
137        self
138    }
139
140    /// Sets whether the [`Droppable`] should be centered on the cursor while dragging.
141    pub fn drag_center(mut self, drag_center: bool) -> Self {
142        self.drag_center = drag_center;
143        self
144    }
145
146    // Sets whether the [`Droppable`] can be dragged along individual axes.
147    pub fn drag_mode(mut self, drag_x: bool, drag_y: bool) -> Self {
148        self.drag_mode = Some((drag_x, drag_y));
149        self
150    }
151
152    /// Sets whether the [`Droppable`] should be be resized to a given size while dragging.
153    pub fn drag_size(mut self, hide_size: Size) -> Self {
154        self.drag_size = Some(hide_size);
155        self
156    }
157
158    /// Sets the number of frames/layout calls to wait before resetting the size of the [`Droppable`] after dropping.
159    ///
160    /// This is useful for cases where the [`Droppable`] is being moved to a new location after some widget operation.
161    /// In this case, the [`Droppable`] will maintain the 'drag_size' for the given number of frames before resetting to its original size.
162    /// This prevents the [`Droppable`] from 'jumping' back to its original size before the new location is rendered which
163    /// prevents flickering.
164    ///
165    /// Warning: this should only be set if there is some noticeable flickering when the [`Droppable`] is dropped. That is, if the
166    /// [`Droppable`] returns to its original size before it's moved to its new location.
167    pub fn reset_delay(mut self, reset_delay: usize) -> Self {
168        self.reset_delay = reset_delay;
169        self
170    }
171}
172
173impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
174    for Droppable<'a, Message, Theme, Renderer>
175where
176    Message: Clone,
177    Renderer: renderer::Renderer,
178{
179    fn size(&self) -> Size<Length> {
180        self.content.as_widget().size()
181    }
182
183    fn layout(
184        &mut self,
185        tree: &mut Tree,
186        renderer: &Renderer,
187        limits: &Limits,
188    ) -> Node {
189        let state: &mut State = tree.state.downcast_mut::<State>();
190        let content_node = self.content.as_widget_mut().layout(
191            &mut tree.children[0],
192            renderer,
193            limits,
194        );
195
196        // Adjust the size of the original widget if it's being dragged or we're wating to reset the size
197        if let Some(new_size) = self.drag_size {
198            match state.action {
199                Action::Drag(_, _) => {
200                    return Node::with_children(
201                        new_size,
202                        content_node.children().to_vec(),
203                    );
204                }
205                Action::Wait(reveal_index) => {
206                    if reveal_index <= 1 {
207                        state.action = Action::None;
208                    } else {
209                        state.action = Action::Wait(reveal_index - 1);
210                    }
211
212                    return Node::with_children(
213                        new_size,
214                        content_node.children().to_vec(),
215                    );
216                }
217                _ => (),
218            }
219        }
220
221        content_node
222    }
223
224    fn draw(
225        &self,
226        tree: &Tree,
227        renderer: &mut Renderer,
228        theme: &Theme,
229        style: &Style,
230        layout: Layout<'_>,
231        cursor: Cursor,
232        viewport: &Rectangle,
233    ) {
234        let state: &State = tree.state.downcast_ref::<State>();
235        if let Action::Drag(_, _) = state.action
236            && self.drag_hide
237        {
238            return;
239        }
240
241        self.content.as_widget().draw(
242            &tree.children[0],
243            renderer,
244            theme,
245            style,
246            layout,
247            cursor,
248            viewport,
249        );
250    }
251
252    fn tag(&self) -> Tag {
253        Tag::of::<State>()
254    }
255
256    fn state(&self) -> iced_core::widget::tree::State {
257        iced_core::widget::tree::State::new(State::default())
258    }
259
260    fn children(&self) -> Vec<Tree> {
261        vec![Tree::new(&self.content)]
262    }
263
264    fn diff(&self, tree: &mut Tree) {
265        tree.diff_children(std::slice::from_ref(&self.content))
266    }
267
268    fn operate(
269        &mut self,
270        tree: &mut Tree,
271        layout: Layout<'_>,
272        renderer: &Renderer,
273        operation: &mut dyn Operation,
274    ) {
275        let state = tree.state.downcast_mut::<State>();
276        operation.custom(self.id.as_ref(), layout.bounds(), state);
277        operation.container(self.id.as_ref(), layout.bounds());
278        operation.traverse(&mut |operation| {
279            self.content.as_widget_mut().operate(
280                &mut tree.children[0],
281                layout,
282                renderer,
283                operation,
284            );
285        });
286    }
287
288    fn update(
289        &mut self,
290        tree: &mut Tree,
291        event: &Event,
292        layout: Layout<'_>,
293        cursor: Cursor,
294        _renderer: &Renderer,
295        _clipboard: &mut dyn iced_core::Clipboard,
296        shell: &mut iced_core::Shell<'_, Message>,
297        _viewport: &Rectangle,
298    ) {
299        let state = tree.state.downcast_mut::<State>();
300
301        if !matches!(state.action, Action::Drag(_, _)) {
302            // handle the on event of the content first, in case that the droppable is nested
303            self.content.as_widget_mut().update(
304                &mut tree.children[0],
305                event,
306                layout,
307                cursor,
308                _renderer,
309                _clipboard,
310                shell,
311                _viewport,
312            );
313            // this should really only be captured if the droppable is nested, or it contains some other
314            // widget that captures the event
315            if shell.is_event_captured() {
316                return;
317            }
318        }
319
320        if let Some(on_drop) = self.on_drop.as_deref() {
321            match event {
322                Event::Mouse(mouse::Event::ButtonPressed(
323                    mouse::Button::Left,
324                ))
325                | Event::Touch(touch::Event::FingerPressed { .. }) => {
326                    let bounds = layout.bounds();
327                    if cursor.is_over(bounds) {
328                        // select the droppable and store the position of the widget before dragging
329                        state.action =
330                            Action::Select(cursor.position().unwrap());
331                        state.widget_pos = bounds.position();
332                        state.overlay_bounds.width = bounds.width;
333                        state.overlay_bounds.height = bounds.height;
334
335                        if let Some(on_click) = self.on_click.clone() {
336                            shell.publish(on_click);
337                        }
338
339                        shell.capture_event();
340                    }
341                }
342                Event::Mouse(mouse::Event::ButtonPressed(
343                    mouse::Button::Right,
344                ))
345                | Event::Touch(touch::Event::FingerLost { .. }) => {
346                    if let Action::Drag(_, _) = state.action {
347                        state.action = Action::None;
348                        if let Some(on_cancel) = self.on_cancel.clone() {
349                            shell.publish(on_cancel);
350                        }
351
352                        shell.invalidate_layout();
353                        shell.request_redraw();
354                    }
355                }
356                Event::Mouse(mouse::Event::ButtonReleased(
357                    mouse::Button::Left,
358                ))
359                | Event::Touch(touch::Event::FingerLifted { .. }) => {
360                    match state.action {
361                        Action::Select(_) => {
362                            if let Some(on_press) = self.on_press.clone() {
363                                shell.publish(on_press);
364                            }
365
366                            state.action = Action::None;
367                        }
368                        Action::Drag(_, current) => {
369                            // send on drop msg
370                            let message =
371                                (on_drop)(current, state.overlay_bounds);
372                            shell.publish(message);
373
374                            if self.reset_delay == 0 {
375                                state.action = Action::None;
376                            } else {
377                                state.action = Action::Wait(self.reset_delay);
378                            }
379                        }
380                        _ => (),
381                    }
382                }
383                Event::Mouse(mouse::Event::CursorMoved { position })
384                | Event::Touch(touch::Event::FingerMoved {
385                    position,
386                    ..
387                }) => {
388                    let mut position = *position;
389
390                    let should_drag = match state.action {
391                        Action::Select(start) => {
392                            // Check if cursor has moved from the initial click position
393                            // (in case there was a micro-movement, might happen on trackpad)
394                            let distance = ((position.x - start.x).powi(2)
395                                + (position.y - start.y).powi(2))
396                            .sqrt();
397
398                            if distance >= self.drag_threshold {
399                                state.action = Action::Drag(start, position);
400                                true
401                            } else {
402                                false
403                            }
404                        }
405                        Action::Drag(start, _) => {
406                            state.action = Action::Drag(start, position);
407                            true
408                        }
409                        _ => false,
410                    };
411
412                    if should_drag && let Action::Drag(start, _) = state.action
413                    {
414                        // Apply drag mode constraints
415                        if let Some((drag_x, drag_y)) = self.drag_mode {
416                            position = Point {
417                                x: if drag_x { position.x } else { start.x },
418                                y: if drag_y { position.y } else { start.y },
419                            };
420                        }
421
422                        // update the position of the overlay since the cursor was moved
423                        if self.drag_center {
424                            state.overlay_bounds.x =
425                                position.x - state.overlay_bounds.width / 2.0;
426                            state.overlay_bounds.y =
427                                position.y - state.overlay_bounds.height / 2.0;
428                        } else {
429                            state.overlay_bounds.x =
430                                state.widget_pos.x + position.x - start.x;
431                            state.overlay_bounds.y =
432                                state.widget_pos.y + position.y - start.y;
433                        }
434
435                        // Send on_drag message
436                        if let Some(on_drag) = self.on_drag.as_deref() {
437                            let message =
438                                (on_drag)(position, state.overlay_bounds);
439                            shell.publish(message);
440                        }
441
442                        shell.request_redraw();
443                    }
444                }
445                _ => {}
446            }
447        }
448
449        let current_status = if self.on_drop.is_none() {
450            Status::Disabled
451        } else if cursor.is_over(layout.bounds()) {
452            if let Action::Drag(_, _) = state.action {
453                Status::Dragged
454            } else {
455                Status::Hovered
456            }
457        } else {
458            Status::Active
459        };
460
461        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
462            self.status = Some(current_status);
463        } else if self.status.is_some_and(|status| status != current_status) {
464            shell.request_redraw();
465        }
466    }
467
468    fn mouse_interaction(
469        &self,
470        tree: &Tree,
471        layout: Layout<'_>,
472        cursor: Cursor,
473        _viewport: &Rectangle,
474        _renderer: &Renderer,
475    ) -> mouse::Interaction {
476        let state = tree.state.downcast_ref::<State>();
477
478        if let Action::Drag(_, _) = state.action {
479            return mouse::Interaction::Grabbing;
480        }
481
482        let child_interact = self.content.as_widget().mouse_interaction(
483            &tree.children[0],
484            layout,
485            cursor,
486            _viewport,
487            _renderer,
488        );
489
490        if child_interact != mouse::Interaction::default() {
491            return child_interact;
492        }
493
494        if cursor.is_over(layout.bounds()) {
495            if self.on_drop.is_some() {
496                if self.on_press.is_some() {
497                    mouse::Interaction::Pointer
498                } else {
499                    mouse::Interaction::Grab
500                }
501            } else {
502                mouse::Interaction::NotAllowed
503            }
504        } else {
505            mouse::Interaction::default()
506        }
507    }
508
509    fn overlay<'b>(
510        &'b mut self,
511        tree: &'b mut Tree,
512        layout: Layout<'b>,
513        renderer: &Renderer,
514        _viewport: &Rectangle,
515        _translation: Vector,
516    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
517        let state: &mut State = tree.state.downcast_mut::<State>();
518        if self.drag_overlay
519            && let Action::Drag(_, _) = state.action
520        {
521            return Some(overlay::Element::new(Box::new(Overlay {
522                content: &mut self.content,
523                tree: &mut tree.children[0],
524                overlay_bounds: state.overlay_bounds,
525            })));
526        }
527        self.content.as_widget_mut().overlay(
528            &mut tree.children[0],
529            layout,
530            renderer,
531            _viewport,
532            _translation,
533        )
534    }
535}
536
537impl<'a, Message, Theme, Renderer> From<Droppable<'a, Message, Theme, Renderer>>
538    for Element<'a, Message, Theme, Renderer>
539where
540    Message: 'a + Clone,
541    Theme: 'a,
542    Renderer: 'a + renderer::Renderer,
543{
544    fn from(
545        droppable: Droppable<'a, Message, Theme, Renderer>,
546    ) -> Element<'a, Message, Theme, Renderer> {
547        Element::new(droppable)
548    }
549}
550
551#[derive(Default, Clone, Copy, PartialEq, Debug)]
552pub struct State {
553    widget_pos: Point,
554    overlay_bounds: Rectangle,
555    action: Action,
556}
557
558#[derive(Default, Clone, Copy, PartialEq, Debug)]
559pub enum Status {
560    #[default]
561    Active,
562    Hovered,
563    Dragged,
564    Disabled,
565}
566
567#[derive(Default, Clone, Copy, PartialEq, Debug)]
568pub enum Action {
569    #[default]
570    None,
571    /// (point clicked)
572    Select(Point),
573    /// (start pos, current pos)
574    Drag(Point, Point),
575    /// (frames to wait)
576    Wait(usize),
577}
578
579struct Overlay<'a, 'b, Message, Theme, Renderer>
580where
581    Renderer: renderer::Renderer,
582{
583    content: &'b mut Element<'a, Message, Theme, Renderer>,
584    tree: &'b mut Tree,
585    overlay_bounds: Rectangle,
586}
587
588impl<'a, 'b, Message, Theme, Renderer>
589    overlay::Overlay<Message, Theme, Renderer>
590    for Overlay<'a, 'b, Message, Theme, Renderer>
591where
592    Renderer: renderer::Renderer,
593{
594    fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> Node {
595        Widget::<Message, Theme, Renderer>::layout(
596            self.content.as_widget_mut(),
597            self.tree,
598            renderer,
599            &Limits::new(Size::ZERO, self.overlay_bounds.size()),
600        )
601        .move_to(self.overlay_bounds.position())
602    }
603
604    fn draw(
605        &self,
606        renderer: &mut Renderer,
607        theme: &Theme,
608        inherited_style: &Style,
609        layout: Layout<'_>,
610        cursor_position: Cursor,
611    ) {
612        Widget::<Message, Theme, Renderer>::draw(
613            self.content.as_widget(),
614            self.tree,
615            renderer,
616            theme,
617            inherited_style,
618            layout,
619            cursor_position,
620            &Rectangle::with_size(Size::INFINITE),
621        );
622    }
623}