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    /// Whether dragging can start. Defaults to `true`.
50    enabled: bool,
51    key: DiffKey,
52}
53
54impl<T: Clone + PartialEq + 'static> KeyExt for DragZone<T> {
55    fn write_key(&mut self) -> &mut DiffKey {
56        &mut self.key
57    }
58}
59
60impl<T: Clone + PartialEq + 'static> DragZone<T> {
61    pub fn new(data: T, children: impl Into<Element>) -> Self {
62        Self {
63            data,
64            children: children.into(),
65            drag_element: None,
66            show_while_dragging: true,
67            drag_threshold: 4.0,
68            enabled: true,
69            key: DiffKey::default(),
70        }
71    }
72
73    pub fn show_while_dragging(mut self, show_while_dragging: bool) -> Self {
74        self.show_while_dragging = show_while_dragging;
75        self
76    }
77
78    pub fn drag_element(mut self, drag_element: impl Into<Element>) -> Self {
79        self.drag_element = Some(drag_element.into());
80        self
81    }
82
83    pub fn drag_threshold(mut self, drag_threshold: f64) -> Self {
84        self.drag_threshold = drag_threshold;
85        self
86    }
87
88    /// Whether dragging can start. Defaults to `true`.
89    pub fn enabled(mut self, enabled: bool) -> Self {
90        self.enabled = enabled;
91        self
92    }
93}
94
95impl<T: Clone + PartialEq> Component for DragZone<T> {
96    fn render(&self) -> impl IntoElement {
97        let mut drags = use_drag::<T>();
98        let mut phase = use_state(|| DragPhase::Idle);
99        let mut drag_element_size = use_state(|| None::<Size2D>);
100        let data = self.data.clone();
101        let drag_threshold = self.drag_threshold;
102
103        let on_global_pointer_move = move |e: Event<PointerEventData>| match phase() {
104            DragPhase::Dragging { offset, .. } => {
105                phase.set(DragPhase::Dragging {
106                    position: e.global_location(),
107                    offset,
108                });
109            }
110            DragPhase::Pressing {
111                press_point,
112                offset,
113            } => {
114                let current = e.global_location();
115                let dx = current.x - press_point.x;
116                let dy = current.y - press_point.y;
117
118                if (dx * dx + dy * dy).sqrt() >= drag_threshold {
119                    phase.set(DragPhase::Dragging {
120                        position: current,
121                        offset,
122                    });
123                    *drags.write() = Some(data.clone());
124                }
125            }
126            DragPhase::Idle => {}
127        };
128
129        let on_pointer_down = move |e: Event<PointerEventData>| {
130            if e.data().button() != Some(MouseButton::Left) {
131                return;
132            }
133            phase.set(DragPhase::Pressing {
134                press_point: e.global_location(),
135                offset: e.element_location(),
136            });
137        };
138
139        let on_global_pointer_press = move |_: Event<PointerEventData>| {
140            if !matches!(phase(), DragPhase::Idle) {
141                phase.set(DragPhase::Idle);
142                *drags.write() = None;
143            }
144        };
145
146        let dragging = match phase() {
147            DragPhase::Dragging { position, offset } => Some((position, offset)),
148            _ => None,
149        };
150
151        rect()
152            .on_global_pointer_press(on_global_pointer_press)
153            .on_global_pointer_move(on_global_pointer_move)
154            .maybe(self.enabled, |rect| rect.on_pointer_down(on_pointer_down))
155            .maybe_child((dragging.zip(self.drag_element.clone())).map(
156                |((position, offset), drag_element)| {
157                    let size = *drag_element_size.read();
158                    let anchor = size.map_or(offset, |size| {
159                        offset.min(CursorPoint::new(size.width as f64, size.height as f64))
160                    });
161                    let (x, y) = (position - anchor).to_f32().to_tuple();
162                    rect()
163                        .position(Position::new_global())
164                        .layer(Layer::Overlay)
165                        .interactive(false)
166                        .opacity(if size.is_some() { 1. } else { 0. })
167                        // Extend by 1. so that the cursor click can reach the drop zone
168                        .offset_x(x + 1.)
169                        .offset_y(y + 1.)
170                        .on_sized(move |e: Event<SizedEventData>| {
171                            drag_element_size.set_if_modified(Some(e.area.size))
172                        })
173                        .child(drag_element)
174                },
175            ))
176            .maybe_child(
177                (self.show_while_dragging || dragging.is_none()).then(|| self.children.clone()),
178            )
179    }
180
181    fn render_key(&self) -> DiffKey {
182        self.key.clone().or(self.default_key())
183    }
184}
185
186#[derive(PartialEq, Clone)]
187pub struct DropZone<T: 'static + PartialEq + Clone> {
188    children: Element,
189    on_drop: EventHandler<T>,
190    on_drag_over: Option<EventHandler<bool>>,
191    width: Size,
192    height: Size,
193    key: DiffKey,
194}
195
196impl<T: Clone + PartialEq + 'static> KeyExt for DropZone<T> {
197    fn write_key(&mut self) -> &mut DiffKey {
198        &mut self.key
199    }
200}
201
202impl<T: PartialEq + Clone + 'static> DropZone<T> {
203    pub fn new(children: impl Into<Element>, on_drop: impl Into<EventHandler<T>>) -> Self {
204        Self {
205            children: children.into(),
206            on_drop: on_drop.into(),
207            on_drag_over: None,
208            width: Size::auto(),
209            height: Size::auto(),
210            key: DiffKey::default(),
211        }
212    }
213
214    /// Called with `true` when a drag enters this zone and `false` when it leaves or is dropped.
215    /// Only fires while a drag of `T` is in progress, so it is handy for showing drop previews.
216    pub fn on_drag_over(mut self, on_drag_over: impl Into<EventHandler<bool>>) -> Self {
217        self.on_drag_over = Some(on_drag_over.into());
218        self
219    }
220}
221
222impl<T: Clone + PartialEq + 'static> Component for DropZone<T> {
223    fn render(&self) -> impl IntoElement {
224        let mut drags = use_drag::<T>();
225        let on_drop = self.on_drop.clone();
226        let on_drag_over = self.on_drag_over.clone();
227
228        let on_mouse_up = {
229            let on_drag_over = on_drag_over.clone();
230            move |e: Event<MouseEventData>| {
231                e.stop_propagation();
232                if let Some(current_drags) = &*drags.read() {
233                    on_drop.call(current_drags.clone());
234                }
235                if drags.read().is_some() {
236                    *drags.write() = None;
237                    if let Some(on_drag_over) = &on_drag_over {
238                        on_drag_over.call(false);
239                    }
240                }
241            }
242        };
243
244        rect()
245            .on_mouse_up(on_mouse_up)
246            .width(self.width.clone())
247            .height(self.height.clone())
248            .map(on_drag_over, move |el, on_drag_over| {
249                el.on_pointer_enter({
250                    let on_drag_over = on_drag_over.clone();
251                    move |_| {
252                        if drags.read().is_some() {
253                            on_drag_over.call(true);
254                        }
255                    }
256                })
257                .on_pointer_leave(move |_| {
258                    if drags.read().is_some() {
259                        on_drag_over.call(false);
260                    }
261                })
262            })
263            .child(self.children.clone())
264    }
265
266    fn render_key(&self) -> DiffKey {
267        self.key.clone().or(self.default_key())
268    }
269}