Skip to main content

freya_components/
drag_drop.rs

1use freya_core::{
2    prelude::*,
3    scope_id::ScopeId,
4};
5use torin::prelude::*;
6
7#[derive(Clone, Copy)]
8enum DragPhase {
9    Idle,
10    Pressing {
11        press_point: CursorPoint,
12        offset: CursorPoint,
13    },
14    Dragging {
15        position: CursorPoint,
16        offset: CursorPoint,
17    },
18}
19
20/// Access the global drag state for payloads of type `T`.
21///
22/// Returns a [`State`] that holds `Some(payload)` while a [`DragZone`] of `T` is being dragged
23/// and `None` otherwise. Useful for components that need to react to ongoing drags (for example
24/// to display drop targets only while dragging).
25pub fn use_drag<T: 'static>() -> State<Option<T>> {
26    match try_consume_root_context() {
27        Some(s) => s,
28        None => {
29            let state = State::<Option<T>>::create_in_scope(None, ScopeId::ROOT);
30            provide_context_for_scope_id(state, ScopeId::ROOT);
31            state
32        }
33    }
34}
35
36/// Properties for the [`DragZone`] component.
37#[derive(Clone, PartialEq)]
38pub struct DragZone<T: Clone + 'static + PartialEq> {
39    /// Element visible when dragging the element. This follows the cursor.
40    drag_element: Option<Element>,
41    /// Inner children for the DropZone.
42    children: Element,
43    /// Data that will be handled to the destination [`DropZone`].
44    data: T,
45    /// Show the children when dragging. Defaults to `true`.
46    show_while_dragging: bool,
47    /// Minimum distance in pixels the cursor must move before dragging starts. Defaults to `4.0`.
48    drag_threshold: f64,
49    key: DiffKey,
50}
51
52impl<T: Clone + PartialEq + 'static> KeyExt for DragZone<T> {
53    fn write_key(&mut self) -> &mut DiffKey {
54        &mut self.key
55    }
56}
57
58impl<T: Clone + PartialEq + 'static> DragZone<T> {
59    pub fn new(data: T, children: impl Into<Element>) -> Self {
60        Self {
61            data,
62            children: children.into(),
63            drag_element: None,
64            show_while_dragging: true,
65            drag_threshold: 4.0,
66            key: DiffKey::default(),
67        }
68    }
69
70    pub fn show_while_dragging(mut self, show_while_dragging: bool) -> Self {
71        self.show_while_dragging = show_while_dragging;
72        self
73    }
74
75    pub fn drag_element(mut self, drag_element: impl Into<Element>) -> Self {
76        self.drag_element = Some(drag_element.into());
77        self
78    }
79
80    pub fn drag_threshold(mut self, drag_threshold: f64) -> Self {
81        self.drag_threshold = drag_threshold;
82        self
83    }
84}
85
86impl<T: Clone + PartialEq> Component for DragZone<T> {
87    fn render(&self) -> impl IntoElement {
88        let mut drags = use_drag::<T>();
89        let mut phase = use_state(|| DragPhase::Idle);
90        let data = self.data.clone();
91        let drag_threshold = self.drag_threshold;
92
93        let on_global_pointer_move = move |e: Event<PointerEventData>| match phase() {
94            DragPhase::Dragging { offset, .. } => {
95                phase.set(DragPhase::Dragging {
96                    position: e.global_location(),
97                    offset,
98                });
99            }
100            DragPhase::Pressing {
101                press_point,
102                offset,
103            } => {
104                let current = e.global_location();
105                let dx = current.x - press_point.x;
106                let dy = current.y - press_point.y;
107
108                if (dx * dx + dy * dy).sqrt() >= drag_threshold {
109                    phase.set(DragPhase::Dragging {
110                        position: current,
111                        offset,
112                    });
113                    *drags.write() = Some(data.clone());
114                }
115            }
116            DragPhase::Idle => {}
117        };
118
119        let on_pointer_down = move |e: Event<PointerEventData>| {
120            if e.data().button() != Some(MouseButton::Left) {
121                return;
122            }
123            phase.set(DragPhase::Pressing {
124                press_point: e.global_location(),
125                offset: e.element_location(),
126            });
127        };
128
129        let on_global_pointer_press = move |_: Event<PointerEventData>| {
130            if !matches!(phase(), DragPhase::Idle) {
131                phase.set(DragPhase::Idle);
132                *drags.write() = None;
133            }
134        };
135
136        let dragging = match phase() {
137            DragPhase::Dragging { position, offset } => Some((position, offset)),
138            _ => None,
139        };
140
141        rect()
142            .on_global_pointer_press(on_global_pointer_press)
143            .on_global_pointer_move(on_global_pointer_move)
144            .on_pointer_down(on_pointer_down)
145            .maybe_child((dragging.zip(self.drag_element.clone())).map(
146                |((position, offset), drag_element)| {
147                    let (x, y) = (position - offset).to_f32().to_tuple();
148                    rect()
149                        .position(Position::new_global())
150                        .layer(Layer::Overlay)
151                        .interactive(false)
152                        .width(Size::px(0.))
153                        .height(Size::px(0.))
154                        // Extend by 1. so that the cursor click can reach the drop zone
155                        .offset_x(x + 1.)
156                        .offset_y(y + 1.)
157                        .child(drag_element)
158                },
159            ))
160            .maybe_child(
161                (self.show_while_dragging || dragging.is_none()).then(|| self.children.clone()),
162            )
163    }
164
165    fn render_key(&self) -> DiffKey {
166        self.key.clone().or(self.default_key())
167    }
168}
169
170#[derive(PartialEq, Clone)]
171pub struct DropZone<T: 'static + PartialEq + Clone> {
172    children: Element,
173    on_drop: EventHandler<T>,
174    on_drag_over: Option<EventHandler<bool>>,
175    width: Size,
176    height: Size,
177    key: DiffKey,
178}
179
180impl<T: Clone + PartialEq + 'static> KeyExt for DropZone<T> {
181    fn write_key(&mut self) -> &mut DiffKey {
182        &mut self.key
183    }
184}
185
186impl<T: PartialEq + Clone + 'static> DropZone<T> {
187    pub fn new(children: impl Into<Element>, on_drop: impl Into<EventHandler<T>>) -> Self {
188        Self {
189            children: children.into(),
190            on_drop: on_drop.into(),
191            on_drag_over: None,
192            width: Size::auto(),
193            height: Size::auto(),
194            key: DiffKey::default(),
195        }
196    }
197
198    /// Called with `true` when a drag enters this zone and `false` when it leaves or is dropped.
199    /// Only fires while a drag of `T` is in progress, so it is handy for showing drop previews.
200    pub fn on_drag_over(mut self, on_drag_over: impl Into<EventHandler<bool>>) -> Self {
201        self.on_drag_over = Some(on_drag_over.into());
202        self
203    }
204}
205
206impl<T: Clone + PartialEq + 'static> Component for DropZone<T> {
207    fn render(&self) -> impl IntoElement {
208        let mut drags = use_drag::<T>();
209        let on_drop = self.on_drop.clone();
210        let on_drag_over = self.on_drag_over.clone();
211
212        let on_mouse_up = {
213            let on_drag_over = on_drag_over.clone();
214            move |e: Event<MouseEventData>| {
215                e.stop_propagation();
216                if let Some(current_drags) = &*drags.read() {
217                    on_drop.call(current_drags.clone());
218                }
219                if drags.read().is_some() {
220                    *drags.write() = None;
221                    if let Some(on_drag_over) = &on_drag_over {
222                        on_drag_over.call(false);
223                    }
224                }
225            }
226        };
227
228        rect()
229            .on_mouse_up(on_mouse_up)
230            .width(self.width.clone())
231            .height(self.height.clone())
232            .map(on_drag_over, move |el, on_drag_over| {
233                el.on_pointer_enter({
234                    let on_drag_over = on_drag_over.clone();
235                    move |_| {
236                        if drags.read().is_some() {
237                            on_drag_over.call(true);
238                        }
239                    }
240                })
241                .on_pointer_leave(move |_| {
242                    if drags.read().is_some() {
243                        on_drag_over.call(false);
244                    }
245                })
246            })
247            .child(self.children.clone())
248    }
249
250    fn render_key(&self) -> DiffKey {
251        self.key.clone().or(self.default_key())
252    }
253}