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/// A container whose children can be dragged into a [`DropZone`] expecting the same payload type.
37///
38/// # Example
39///
40/// ```rust
41/// # use freya::prelude::*;
42/// fn app() -> impl IntoElement {
43///     DragZone::new(0).drag_element("Dragging").child("Drag me!")
44/// }
45/// ```
46#[derive(Clone, PartialEq)]
47pub struct DragZone<T: Clone + 'static + PartialEq> {
48    children: Vec<Element>,
49    drag_element: Option<Element>,
50    data: T,
51    show_while_dragging: bool,
52    drag_threshold: f64,
53    enabled: bool,
54    layout: LayoutData,
55    key: DiffKey,
56}
57
58impl<T: Clone + PartialEq + 'static> ChildrenExt for DragZone<T> {
59    fn children_mut(&mut self) -> &mut Vec<Element> {
60        &mut self.children
61    }
62}
63
64impl<T: Clone + PartialEq + 'static> KeyExt for DragZone<T> {
65    fn key_mut(&mut self) -> &mut DiffKey {
66        &mut self.key
67    }
68}
69
70impl<T: Clone + PartialEq + 'static> LayoutExt for DragZone<T> {
71    fn layout_mut(&mut self) -> &mut LayoutData {
72        &mut self.layout
73    }
74}
75
76impl<T: Clone + PartialEq + 'static> ContainerExt for DragZone<T> {}
77
78impl<T: Clone + PartialEq + 'static> DragZone<T> {
79    /// Create a drag zone carrying `data`, which is handed to the destination [`DropZone`].
80    pub fn new(data: T) -> Self {
81        Self {
82            data,
83            children: Vec::new(),
84            drag_element: None,
85            show_while_dragging: true,
86            drag_threshold: 4.0,
87            enabled: true,
88            layout: LayoutData::default(),
89            key: DiffKey::default(),
90        }
91    }
92
93    /// Whether the children stay visible while dragging. Defaults to `true`.
94    pub fn show_while_dragging(mut self, show_while_dragging: bool) -> Self {
95        self.show_while_dragging = show_while_dragging;
96        self
97    }
98
99    /// Element visible while dragging, which follows the cursor.
100    pub fn drag_element(mut self, drag_element: impl Into<Element>) -> Self {
101        self.drag_element = Some(drag_element.into());
102        self
103    }
104
105    /// Minimum distance in pixels the cursor must move before dragging starts. Defaults to `4.0`.
106    pub fn drag_threshold(mut self, drag_threshold: f64) -> Self {
107        self.drag_threshold = drag_threshold;
108        self
109    }
110
111    /// Whether dragging can start. Defaults to `true`.
112    pub fn enabled(mut self, enabled: bool) -> Self {
113        self.enabled = enabled;
114        self
115    }
116}
117
118impl<T: Clone + PartialEq> Component for DragZone<T> {
119    fn render(&self) -> impl IntoElement {
120        let mut drags = use_drag::<T>();
121        let mut phase = use_state(|| DragPhase::Idle);
122        let data = self.data.clone();
123        let drag_threshold = self.drag_threshold;
124
125        let on_global_pointer_move = move |e: Event<PointerEventData>| match phase() {
126            DragPhase::Dragging { offset, .. } => {
127                phase.set(DragPhase::Dragging {
128                    position: e.global_location(),
129                    offset,
130                });
131            }
132            DragPhase::Pressing {
133                press_point,
134                offset,
135            } => {
136                let current = e.global_location();
137                let dx = current.x - press_point.x;
138                let dy = current.y - press_point.y;
139
140                if (dx * dx + dy * dy).sqrt() >= drag_threshold {
141                    phase.set(DragPhase::Dragging {
142                        position: current,
143                        offset,
144                    });
145                    *drags.write() = Some(data.clone());
146                }
147            }
148            DragPhase::Idle => {}
149        };
150
151        let on_pointer_down = move |e: Event<PointerEventData>| {
152            if !e.data().is_primary() {
153                return;
154            }
155            phase.set(DragPhase::Pressing {
156                press_point: e.global_location(),
157                offset: e.element_location(),
158            });
159        };
160
161        let on_global_pointer_up = move |_: Event<PointerEventData>| {
162            if !matches!(phase(), DragPhase::Idle) {
163                phase.set(DragPhase::Idle);
164                *drags.write() = None;
165            }
166        };
167
168        let dragging = match phase() {
169            DragPhase::Dragging { position, offset } => Some((position, offset)),
170            _ => None,
171        };
172
173        rect()
174            .layout(self.layout.clone())
175            .on_global_pointer_up(on_global_pointer_up)
176            .on_global_pointer_move(on_global_pointer_move)
177            .maybe(self.enabled, |el| el.on_pointer_down(on_pointer_down))
178            .maybe_child((dragging.zip(self.drag_element.clone())).map(
179                |((position, offset), drag_element)| {
180                    let (x, y) = (position - offset).to_f32().to_tuple();
181                    rect()
182                        .position(Position::new_global().left(x + 1.).top(y + 1.))
183                        .layer(Layer::Overlay)
184                        .interactive(false)
185                        .min_width(Size::px(offset.x as f32))
186                        .cross_align(Alignment::End)
187                        .child(
188                            rect()
189                                .horizontal()
190                                .min_height(Size::px(offset.y as f32))
191                                .cross_align(Alignment::End)
192                                .child(drag_element),
193                        )
194                },
195            ))
196            .maybe(self.show_while_dragging || dragging.is_none(), |el| {
197                el.children(self.children.clone())
198            })
199    }
200
201    fn render_key(&self) -> DiffKey {
202        self.key.clone().or(self.default_key())
203    }
204}
205
206/// A container that receives the payload of a [`DragZone`] released over it.
207///
208/// # Example
209///
210/// ```rust
211/// # use freya::prelude::*;
212/// fn app() -> impl IntoElement {
213///     DropZone::new(|data: usize| println!("Dropped {data}")).child("Drop here!")
214/// }
215/// ```
216#[derive(PartialEq, Clone)]
217pub struct DropZone<T: 'static + PartialEq + Clone> {
218    children: Vec<Element>,
219    on_drop: EventHandler<T>,
220    on_drag_over: Option<EventHandler<bool>>,
221    layout: LayoutData,
222    key: DiffKey,
223}
224
225impl<T: Clone + PartialEq + 'static> ChildrenExt for DropZone<T> {
226    fn children_mut(&mut self) -> &mut Vec<Element> {
227        &mut self.children
228    }
229}
230
231impl<T: Clone + PartialEq + 'static> KeyExt for DropZone<T> {
232    fn key_mut(&mut self) -> &mut DiffKey {
233        &mut self.key
234    }
235}
236
237impl<T: Clone + PartialEq + 'static> LayoutExt for DropZone<T> {
238    fn layout_mut(&mut self) -> &mut LayoutData {
239        &mut self.layout
240    }
241}
242
243impl<T: Clone + PartialEq + 'static> ContainerExt for DropZone<T> {}
244
245impl<T: PartialEq + Clone + 'static> DropZone<T> {
246    /// Create a drop zone, `on_drop` is called with the payload of the released [`DragZone`].
247    pub fn new(on_drop: impl Into<EventHandler<T>>) -> Self {
248        Self {
249            children: Vec::new(),
250            on_drop: on_drop.into(),
251            on_drag_over: None,
252            layout: LayoutData::default(),
253            key: DiffKey::default(),
254        }
255    }
256
257    /// Called with `true` when a drag enters this zone and `false` when it leaves or is dropped.
258    /// Only fires while a drag of `T` is in progress, so it is handy for showing drop previews.
259    pub fn on_drag_over(mut self, on_drag_over: impl Into<EventHandler<bool>>) -> Self {
260        self.on_drag_over = Some(on_drag_over.into());
261        self
262    }
263}
264
265impl<T: Clone + PartialEq + 'static> Component for DropZone<T> {
266    fn render(&self) -> impl IntoElement {
267        let mut drags = use_drag::<T>();
268        let on_drop = self.on_drop.clone();
269        let on_drag_over = self.on_drag_over.clone();
270
271        let on_mouse_up = {
272            let on_drop = on_drop.clone();
273            let on_drag_over = on_drag_over.clone();
274            move |e: Event<MouseEventData>| {
275                e.stop_propagation();
276                let payload = (*drags.read()).clone();
277                if let Some(payload) = payload {
278                    on_drop.call(payload);
279                    *drags.write() = None;
280                    if let Some(on_drag_over) = &on_drag_over {
281                        on_drag_over.call(false);
282                    }
283                }
284            }
285        };
286
287        let on_touch_end = {
288            let on_drag_over = on_drag_over.clone();
289            move |e: Event<TouchEventData>| {
290                e.stop_propagation();
291                let payload = (*drags.read()).clone();
292                if let Some(payload) = payload {
293                    on_drop.call(payload);
294                    *drags.write() = None;
295                    if let Some(on_drag_over) = &on_drag_over {
296                        on_drag_over.call(false);
297                    }
298                }
299            }
300        };
301
302        rect()
303            .layout(self.layout.clone())
304            .on_mouse_up(on_mouse_up)
305            .on_touch_end(on_touch_end)
306            .map(on_drag_over, move |el, on_drag_over| {
307                el.on_pointer_enter({
308                    let on_drag_over = on_drag_over.clone();
309                    move |_| {
310                        if drags.read().is_some() {
311                            on_drag_over.call(true);
312                        }
313                    }
314                })
315                .on_pointer_leave(move |_| {
316                    if drags.read().is_some() {
317                        on_drag_over.call(false);
318                    }
319                })
320            })
321            .children(self.children.clone())
322    }
323
324    fn render_key(&self) -> DiffKey {
325        self.key.clone().or(self.default_key())
326    }
327}