freya_components/
drag_drop.rs

1use dioxus::prelude::*;
2use freya_elements::{
3    self as dioxus_elements,
4    events::MouseEvent,
5    MouseButton,
6};
7use freya_hooks::use_node_signal;
8use torin::prelude::CursorPoint;
9
10/// Properties for the [`DragProvider`] component.
11#[derive(Props, Clone, PartialEq)]
12pub struct DragProviderProps {
13    /// Inner children of the DragProvider.
14    children: Element,
15}
16
17/// Provide a common place for [`DragZone`]s and [`DropZone`]s to exchange their data.
18#[allow(non_snake_case)]
19pub fn DragProvider<T: 'static>(DragProviderProps { children }: DragProviderProps) -> Element {
20    use_context_provider::<Signal<Option<T>>>(|| Signal::new(None));
21    rsx!({ children })
22}
23
24/// Properties for the [`DragZone`] component.
25#[derive(Props, Clone, PartialEq)]
26pub struct DragZoneProps<T: Clone + 'static + PartialEq> {
27    /// Element visible when dragging the element. This follows the cursor.
28    drag_element: Element,
29    /// Inner children for the DropZone.
30    children: Element,
31    /// Data that will be handled to the destination [`DropZone`].
32    data: T,
33    /// Hide the [`DragZone`] children when dragging.
34    #[props(default = false)]
35    hide_while_dragging: bool,
36}
37
38/// Make the inner children draggable to other [`DropZone`].
39#[allow(non_snake_case)]
40pub fn DragZone<T: 'static + Clone + PartialEq>(
41    DragZoneProps {
42        data,
43        children,
44        drag_element,
45        hide_while_dragging,
46    }: DragZoneProps<T>,
47) -> Element {
48    let mut drags = use_context::<Signal<Option<T>>>();
49    let mut dragging = use_signal(|| false);
50    let mut pos = use_signal(CursorPoint::default);
51    let (node_reference, size) = use_node_signal();
52
53    let onglobalmousemove = move |e: MouseEvent| {
54        if *dragging.read() {
55            let size = size.read();
56            let coord = e.get_screen_coordinates();
57            pos.set(
58                (
59                    coord.x - size.area.min_x() as f64,
60                    coord.y - size.area.min_y() as f64,
61                )
62                    .into(),
63            );
64        }
65    };
66
67    let onmousedown = move |e: MouseEvent| {
68        if e.data.trigger_button != Some(MouseButton::Left) {
69            return;
70        }
71        let size = size.read();
72        let coord = e.get_screen_coordinates();
73        pos.set(
74            (
75                coord.x - size.area.min_x() as f64,
76                coord.y - size.area.min_y() as f64,
77            )
78                .into(),
79        );
80        dragging.set(true);
81        *drags.write() = Some(data.clone());
82    };
83
84    let onglobalclick = move |_: MouseEvent| {
85        if *dragging.read() {
86            dragging.set(false);
87            pos.set((0.0, 0.0).into());
88            *drags.write() = None;
89        }
90    };
91
92    rsx!(
93        rect {
94            reference: node_reference,
95            onglobalclick,
96            onglobalmousemove,
97            onmousedown,
98            if *dragging.read() {
99                rect {
100                    position: "absolute",
101                    width: "0",
102                    height: "0",
103                    offset_x: "{pos.read().x}",
104                    offset_y: "{pos.read().y}",
105                    {drag_element}
106                }
107            }
108            if !hide_while_dragging || !dragging() {
109                {children}
110            }
111        }
112    )
113}
114
115/// Properties for the [`DropZone`] component.
116#[derive(Props, PartialEq, Clone)]
117pub struct DropZoneProps<T: 'static + PartialEq + Clone> {
118    /// Inner children for the DropZone.
119    children: Element,
120    /// Handler for the `ondrop` event.
121    ondrop: EventHandler<T>,
122    /// Width of the [DropZone].
123    #[props(default = "auto".to_string())]
124    width: String,
125    /// Height of the [DropZone].
126    #[props(default = "auto".to_string())]
127    height: String,
128}
129
130/// Elements from [`DragZone`]s can be dropped here.
131#[allow(non_snake_case)]
132pub fn DropZone<T: 'static + Clone + PartialEq>(props: DropZoneProps<T>) -> Element {
133    let mut drags = use_context::<Signal<Option<T>>>();
134
135    let onmouseup = move |e: MouseEvent| {
136        e.stop_propagation();
137        if let Some(current_drags) = &*drags.read() {
138            props.ondrop.call(current_drags.clone());
139        }
140        if drags.read().is_some() {
141            *drags.write() = None;
142        }
143    };
144
145    rsx!(
146        rect {
147            onmouseup,
148            width: props.width,
149            height: props.height,
150            {props.children}
151        }
152    )
153}
154
155#[cfg(test)]
156mod test {
157    use freya::prelude::*;
158    use freya_testing::prelude::*;
159
160    #[tokio::test]
161    pub async fn drag_drop() {
162        fn drop_app() -> Element {
163            let mut state = use_signal::<bool>(|| false);
164
165            rsx!(
166                DragProvider::<bool> {
167                    rect {
168                        height: "50%",
169                        width: "100%",
170                        DragZone {
171                            data: true,
172                            drag_element: rsx!(
173                                label {
174                                    width: "200",
175                                    "Moving"
176                                }
177                            ),
178                            label {
179                                "Move"
180                            }
181                        }
182                    }
183                    DropZone {
184                        ondrop: move |data: bool| {
185                            state.set(data);
186                        },
187                        rect {
188                            height: "50%",
189                            width: "100%",
190                            label {
191                                "Enabled: {state.read()}"
192                            }
193                        }
194                    }
195                }
196            )
197        }
198
199        let mut utils = launch_test(drop_app);
200        let root = utils.root();
201        utils.wait_for_update().await;
202
203        utils.push_event(TestEvent::Mouse {
204            name: EventName::MouseDown,
205            cursor: (5.0, 5.0).into(),
206            button: Some(MouseButton::Left),
207        });
208
209        utils.wait_for_update().await;
210
211        utils.move_cursor((5., 5.)).await;
212
213        utils.move_cursor((5., 300.)).await;
214
215        assert_eq!(
216            root.get(0).get(0).get(0).get(0).get(0).text(),
217            Some("Moving")
218        );
219        assert_eq!(root.get(0).get(0).get(1).get(0).text(), Some("Move"));
220
221        utils.push_event(TestEvent::Mouse {
222            name: EventName::MouseUp,
223            cursor: (5.0, 300.0).into(),
224            button: Some(MouseButton::Left),
225        });
226
227        utils.wait_for_update().await;
228
229        assert_eq!(
230            root.get(1).get(0).get(0).get(0).text(),
231            Some("Enabled: true")
232        );
233    }
234
235    #[tokio::test]
236    pub async fn drag_drop_hide_while_dragging() {
237        fn drop_app() -> Element {
238            let mut state = use_signal::<bool>(|| false);
239
240            rsx!(
241                DragProvider::<bool> {
242                    rect {
243                        height: "50%",
244                        width: "100%",
245                        DragZone {
246                            data: true,
247                            hide_while_dragging: true,
248                            drag_element: rsx!(
249                                label {
250                                    width: "200",
251                                    "Moving"
252                                }
253                            ),
254                            label {
255                                "Move"
256                            }
257                        }
258                    },
259                    DropZone {
260                        ondrop: move |data: bool| {
261                            state.set(data);
262                        },
263                        rect {
264                            height: "50%",
265                            width: "100%",
266                            label {
267                                "Enabled: {state.read()}"
268                            }
269                        }
270                    }
271                }
272            )
273        }
274
275        let mut utils = launch_test(drop_app);
276        let root = utils.root();
277        utils.wait_for_update().await;
278
279        utils.push_event(TestEvent::Mouse {
280            name: EventName::MouseDown,
281            cursor: (5.0, 5.0).into(),
282            button: Some(MouseButton::Left),
283        });
284
285        utils.wait_for_update().await;
286
287        utils.move_cursor((5., 5.)).await;
288
289        utils.move_cursor((5., 300.)).await;
290
291        assert_eq!(
292            root.get(0).get(0).get(0).get(0).get(0).text(),
293            Some("Moving")
294        );
295        assert!(!root.get(0).get(0).get(1).is_visible());
296
297        utils.push_event(TestEvent::Mouse {
298            name: EventName::MouseUp,
299            cursor: (5.0, 300.0).into(),
300            button: Some(MouseButton::Left),
301        });
302
303        utils.wait_for_update().await;
304
305        assert_eq!(
306            root.get(1).get(0).get(0).get(0).text(),
307            Some("Enabled: true")
308        );
309    }
310}