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