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 get_children(&mut self) -> &mut Vec<Element> {
60        &mut self.children
61    }
62}
63
64impl<T: Clone + PartialEq + 'static> KeyExt for DragZone<T> {
65    fn write_key(&mut self) -> &mut DiffKey {
66        &mut self.key
67    }
68}
69
70impl<T: Clone + PartialEq + 'static> LayoutExt for DragZone<T> {
71    fn get_layout(&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 mut drag_element_size = use_state(|| None::<Size2D>);
123        let data = self.data.clone();
124        let drag_threshold = self.drag_threshold;
125
126        let on_global_pointer_move = move |e: Event<PointerEventData>| match phase() {
127            DragPhase::Dragging { offset, .. } => {
128                phase.set(DragPhase::Dragging {
129                    position: e.global_location(),
130                    offset,
131                });
132            }
133            DragPhase::Pressing {
134                press_point,
135                offset,
136            } => {
137                let current = e.global_location();
138                let dx = current.x - press_point.x;
139                let dy = current.y - press_point.y;
140
141                if (dx * dx + dy * dy).sqrt() >= drag_threshold {
142                    phase.set(DragPhase::Dragging {
143                        position: current,
144                        offset,
145                    });
146                    *drags.write() = Some(data.clone());
147                }
148            }
149            DragPhase::Idle => {}
150        };
151
152        let on_pointer_down = move |e: Event<PointerEventData>| {
153            if e.data().button() != Some(MouseButton::Left) {
154                return;
155            }
156            phase.set(DragPhase::Pressing {
157                press_point: e.global_location(),
158                offset: e.element_location(),
159            });
160        };
161
162        let on_global_pointer_press = move |_: Event<PointerEventData>| {
163            if !matches!(phase(), DragPhase::Idle) {
164                phase.set(DragPhase::Idle);
165                *drags.write() = None;
166            }
167        };
168
169        let dragging = match phase() {
170            DragPhase::Dragging { position, offset } => Some((position, offset)),
171            _ => None,
172        };
173
174        rect()
175            .layout(self.layout.clone())
176            .on_global_pointer_press(on_global_pointer_press)
177            .on_global_pointer_move(on_global_pointer_move)
178            .maybe(self.enabled, |el| el.on_pointer_down(on_pointer_down))
179            .maybe_child((dragging.zip(self.drag_element.clone())).map(
180                |((position, offset), drag_element)| {
181                    let size = *drag_element_size.read();
182                    let anchor = size.map_or(offset, |size| {
183                        offset.min(CursorPoint::new(size.width as f64, size.height as f64))
184                    });
185                    let (x, y) = (position - anchor).to_f32().to_tuple();
186                    rect()
187                        .position(Position::new_global())
188                        .layer(Layer::Overlay)
189                        .interactive(false)
190                        .opacity(if size.is_some() { 1. } else { 0. })
191                        // Extend by 1. so that the cursor click can reach the drop zone
192                        .offset_x(x + 1.)
193                        .offset_y(y + 1.)
194                        .on_sized(move |e: Event<SizedEventData>| {
195                            drag_element_size.set_if_modified(Some(e.area.size))
196                        })
197                        .child(drag_element)
198                },
199            ))
200            .maybe(self.show_while_dragging || dragging.is_none(), |el| {
201                el.children(self.children.clone())
202            })
203    }
204
205    fn render_key(&self) -> DiffKey {
206        self.key.clone().or(self.default_key())
207    }
208}
209
210/// A container that receives the payload of a [`DragZone`] released over it.
211///
212/// # Example
213///
214/// ```rust
215/// # use freya::prelude::*;
216/// fn app() -> impl IntoElement {
217///     DropZone::new(|data: usize| println!("Dropped {data}")).child("Drop here!")
218/// }
219/// ```
220#[derive(PartialEq, Clone)]
221pub struct DropZone<T: 'static + PartialEq + Clone> {
222    children: Vec<Element>,
223    on_drop: EventHandler<T>,
224    on_drag_over: Option<EventHandler<bool>>,
225    layout: LayoutData,
226    key: DiffKey,
227}
228
229impl<T: Clone + PartialEq + 'static> ChildrenExt for DropZone<T> {
230    fn get_children(&mut self) -> &mut Vec<Element> {
231        &mut self.children
232    }
233}
234
235impl<T: Clone + PartialEq + 'static> KeyExt for DropZone<T> {
236    fn write_key(&mut self) -> &mut DiffKey {
237        &mut self.key
238    }
239}
240
241impl<T: Clone + PartialEq + 'static> LayoutExt for DropZone<T> {
242    fn get_layout(&mut self) -> &mut LayoutData {
243        &mut self.layout
244    }
245}
246
247impl<T: Clone + PartialEq + 'static> ContainerExt for DropZone<T> {}
248
249impl<T: PartialEq + Clone + 'static> DropZone<T> {
250    /// Create a drop zone, `on_drop` is called with the payload of the released [`DragZone`].
251    pub fn new(on_drop: impl Into<EventHandler<T>>) -> Self {
252        Self {
253            children: Vec::new(),
254            on_drop: on_drop.into(),
255            on_drag_over: None,
256            layout: LayoutData::default(),
257            key: DiffKey::default(),
258        }
259    }
260
261    /// Called with `true` when a drag enters this zone and `false` when it leaves or is dropped.
262    /// Only fires while a drag of `T` is in progress, so it is handy for showing drop previews.
263    pub fn on_drag_over(mut self, on_drag_over: impl Into<EventHandler<bool>>) -> Self {
264        self.on_drag_over = Some(on_drag_over.into());
265        self
266    }
267}
268
269impl<T: Clone + PartialEq + 'static> Component for DropZone<T> {
270    fn render(&self) -> impl IntoElement {
271        let mut drags = use_drag::<T>();
272        let on_drop = self.on_drop.clone();
273        let on_drag_over = self.on_drag_over.clone();
274
275        let on_mouse_up = {
276            let on_drag_over = on_drag_over.clone();
277            move |e: Event<MouseEventData>| {
278                e.stop_propagation();
279                let payload = (*drags.read()).clone();
280                if let Some(payload) = payload {
281                    on_drop.call(payload);
282                    *drags.write() = None;
283                    if let Some(on_drag_over) = &on_drag_over {
284                        on_drag_over.call(false);
285                    }
286                }
287            }
288        };
289
290        rect()
291            .layout(self.layout.clone())
292            .on_mouse_up(on_mouse_up)
293            .map(on_drag_over, move |el, on_drag_over| {
294                el.on_pointer_enter({
295                    let on_drag_over = on_drag_over.clone();
296                    move |_| {
297                        if drags.read().is_some() {
298                            on_drag_over.call(true);
299                        }
300                    }
301                })
302                .on_pointer_leave(move |_| {
303                    if drags.read().is_some() {
304                        on_drag_over.call(false);
305                    }
306                })
307            })
308            .children(self.children.clone())
309    }
310
311    fn render_key(&self) -> DiffKey {
312        self.key.clone().or(self.default_key())
313    }
314}