drag_and_drop.rs

  1use std::{any::Any, rc::Rc};
  2
  3use collections::HashSet;
  4use gpui::{
  5    elements::{Empty, MouseEventHandler, Overlay},
  6    geometry::{rect::RectF, vector::Vector2F},
  7    scene::{MouseDown, MouseDrag},
  8    CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
  9    View, WeakViewHandle,
 10};
 11
 12const DEAD_ZONE: f32 = 4.;
 13
 14enum State<V: View> {
 15    Down {
 16        region_offset: Vector2F,
 17        region: RectF,
 18    },
 19    DeadZone {
 20        region_offset: Vector2F,
 21        region: RectF,
 22    },
 23    Dragging {
 24        window_id: usize,
 25        position: Vector2F,
 26        region_offset: Vector2F,
 27        region: RectF,
 28        payload: Rc<dyn Any + 'static>,
 29        render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
 30    },
 31    Canceled,
 32}
 33
 34impl<V: View> Clone for State<V> {
 35    fn clone(&self) -> Self {
 36        match self {
 37            &State::Down {
 38                region_offset,
 39                region,
 40            } => State::Down {
 41                region_offset,
 42                region,
 43            },
 44            &State::DeadZone {
 45                region_offset,
 46                region,
 47            } => State::DeadZone {
 48                region_offset,
 49                region,
 50            },
 51            State::Dragging {
 52                window_id,
 53                position,
 54                region_offset,
 55                region,
 56                payload,
 57                render,
 58            } => Self::Dragging {
 59                window_id: window_id.clone(),
 60                position: position.clone(),
 61                region_offset: region_offset.clone(),
 62                region: region.clone(),
 63                payload: payload.clone(),
 64                render: render.clone(),
 65            },
 66            State::Canceled => State::Canceled,
 67        }
 68    }
 69}
 70
 71pub struct DragAndDrop<V: View> {
 72    containers: HashSet<WeakViewHandle<V>>,
 73    currently_dragged: Option<State<V>>,
 74}
 75
 76impl<V: View> Default for DragAndDrop<V> {
 77    fn default() -> Self {
 78        Self {
 79            containers: Default::default(),
 80            currently_dragged: Default::default(),
 81        }
 82    }
 83}
 84
 85impl<V: View> DragAndDrop<V> {
 86    pub fn register_container(&mut self, handle: WeakViewHandle<V>) {
 87        self.containers.insert(handle);
 88    }
 89
 90    pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
 91        self.currently_dragged.as_ref().and_then(|state| {
 92            if let State::Dragging {
 93                position,
 94                payload,
 95                window_id: window_dragged_from,
 96                ..
 97            } = state
 98            {
 99                if &window_id != window_dragged_from {
100                    return None;
101                }
102
103                payload
104                    .is::<T>()
105                    .then(|| payload.clone().downcast::<T>().ok())
106                    .flatten()
107                    .map(|payload| (position.clone(), payload))
108            } else {
109                None
110            }
111        })
112    }
113
114    pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
115        cx.update_global(|this: &mut Self, _| {
116            this.currently_dragged = Some(State::Down {
117                region_offset: event.position - event.region.origin(),
118                region: event.region,
119            });
120        })
121    }
122
123    pub fn dragging<T: Any>(
124        event: MouseDrag,
125        payload: Rc<T>,
126        cx: &mut EventContext,
127        render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
128    ) {
129        let window_id = cx.window_id();
130        cx.update_global(|this: &mut Self, cx| {
131            this.notify_containers_for_window(window_id, cx);
132
133            match this.currently_dragged.as_ref() {
134                Some(&State::Down {
135                    region_offset,
136                    region,
137                })
138                | Some(&State::DeadZone {
139                    region_offset,
140                    region,
141                }) => {
142                    if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
143                        this.currently_dragged = Some(State::Dragging {
144                            window_id,
145                            region_offset,
146                            region,
147                            position: event.position,
148                            payload,
149                            render: Rc::new(move |payload, cx| {
150                                render(payload.downcast_ref::<T>().unwrap(), cx)
151                            }),
152                        });
153                    } else {
154                        this.currently_dragged = Some(State::DeadZone {
155                            region_offset,
156                            region,
157                        })
158                    }
159                }
160                Some(&State::Dragging {
161                    region_offset,
162                    region,
163                    ..
164                }) => {
165                    this.currently_dragged = Some(State::Dragging {
166                        window_id,
167                        region_offset,
168                        region,
169                        position: event.position,
170                        payload,
171                        render: Rc::new(move |payload, cx| {
172                            render(payload.downcast_ref::<T>().unwrap(), cx)
173                        }),
174                    });
175                }
176                _ => {}
177            }
178        });
179    }
180
181    pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
182        enum DraggedElementHandler {}
183        cx.global::<Self>()
184            .currently_dragged
185            .clone()
186            .and_then(|state| {
187                match state {
188                    State::Down { .. } => None,
189                    State::DeadZone { .. } => None,
190                    State::Dragging {
191                        window_id,
192                        region_offset,
193                        position,
194                        region,
195                        payload,
196                        render,
197                    } => {
198                        if cx.window_id() != window_id {
199                            return None;
200                        }
201
202                        let position = position - region_offset;
203                        Some(
204                            Overlay::new(
205                                MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
206                                    render(payload, cx)
207                                })
208                                .with_cursor_style(CursorStyle::Arrow)
209                                .on_up(MouseButton::Left, |_, cx| {
210                                    cx.defer(|cx| {
211                                        cx.update_global::<Self, _, _>(|this, cx| {
212                                            this.finish_dragging(cx)
213                                        });
214                                    });
215                                    cx.propagate_event();
216                                })
217                                .on_up_out(MouseButton::Left, |_, cx| {
218                                    cx.defer(|cx| {
219                                        cx.update_global::<Self, _, _>(|this, cx| {
220                                            this.finish_dragging(cx)
221                                        });
222                                    });
223                                })
224                                // Don't block hover events or invalidations
225                                .with_hoverable(false)
226                                .constrained()
227                                .with_width(region.width())
228                                .with_height(region.height())
229                                .boxed(),
230                            )
231                            .with_anchor_position(position)
232                            .boxed(),
233                        )
234                    }
235
236                    State::Canceled => Some(
237                        MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
238                            Empty::new()
239                                .constrained()
240                                .with_width(0.)
241                                .with_height(0.)
242                                .boxed()
243                        })
244                        .on_up(MouseButton::Left, |_, cx| {
245                            cx.defer(|cx| {
246                                cx.update_global::<Self, _, _>(|this, _| {
247                                    this.currently_dragged = None;
248                                });
249                            });
250                        })
251                        .on_up_out(MouseButton::Left, |_, cx| {
252                            cx.defer(|cx| {
253                                cx.update_global::<Self, _, _>(|this, _| {
254                                    this.currently_dragged = None;
255                                });
256                            });
257                        })
258                        .boxed(),
259                    ),
260                }
261            })
262    }
263
264    pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
265        if let Some(State::Dragging {
266            payload, window_id, ..
267        }) = &self.currently_dragged
268        {
269            if payload.is::<P>() {
270                let window_id = *window_id;
271                self.currently_dragged = Some(State::Canceled);
272                self.notify_containers_for_window(window_id, cx);
273            }
274        }
275    }
276
277    fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
278        if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
279            self.notify_containers_for_window(window_id, cx);
280        }
281    }
282
283    fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut MutableAppContext) {
284        self.containers.retain(|container| {
285            if let Some(container) = container.upgrade(cx) {
286                if container.window_id() == window_id {
287                    container.update(cx, |_, cx| cx.notify());
288                }
289                true
290            } else {
291                false
292            }
293        });
294    }
295}
296
297pub trait Draggable {
298    fn as_draggable<V: View, P: Any>(
299        self,
300        payload: P,
301        render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
302    ) -> Self
303    where
304        Self: Sized;
305}
306
307impl<Tag> Draggable for MouseEventHandler<Tag> {
308    fn as_draggable<V: View, P: Any>(
309        self,
310        payload: P,
311        render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
312    ) -> Self
313    where
314        Self: Sized,
315    {
316        let payload = Rc::new(payload);
317        let render = Rc::new(render);
318        self.on_down(MouseButton::Left, move |e, cx| {
319            cx.propagate_event();
320            DragAndDrop::<V>::drag_started(e, cx);
321        })
322        .on_drag(MouseButton::Left, move |e, cx| {
323            let payload = payload.clone();
324            let render = render.clone();
325            DragAndDrop::<V>::dragging(e, payload, cx, render)
326        })
327    }
328}