Handle project entry drop render & start fixing drag cancel issues

Julia and Kay Simmons created

Co-Authored-By: Kay Simmons <kay@zed.dev>

Change summary

crates/drag_and_drop/src/drag_and_drop.rs          | 253 ++++++++++-----
crates/drag_and_drop/src/shared_payloads.rs        |   6 
crates/project_panel/src/project_panel.rs          |  58 ++-
crates/workspace/src/pane/dragged_item_receiver.rs |  28 +
4 files changed, 224 insertions(+), 121 deletions(-)

Detailed changes

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -1,32 +1,47 @@
+pub mod shared_payloads;
+
 use std::{any::Any, rc::Rc};
 
 use collections::HashSet;
 use gpui::{
-    elements::{MouseEventHandler, Overlay},
+    elements::{Empty, MouseEventHandler, Overlay},
     geometry::{rect::RectF, vector::Vector2F},
     scene::MouseDrag,
     CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
     View, WeakViewHandle,
 };
 
-struct State<V: View> {
-    window_id: usize,
-    position: Vector2F,
-    region_offset: Vector2F,
-    region: RectF,
-    payload: Rc<dyn Any + 'static>,
-    render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+enum State<V: View> {
+    Dragging {
+        window_id: usize,
+        position: Vector2F,
+        region_offset: Vector2F,
+        region: RectF,
+        payload: Rc<dyn Any + 'static>,
+        render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+    },
+    Canceled,
 }
 
 impl<V: View> Clone for State<V> {
     fn clone(&self) -> Self {
-        Self {
-            window_id: self.window_id.clone(),
-            position: self.position.clone(),
-            region_offset: self.region_offset.clone(),
-            region: self.region.clone(),
-            payload: self.payload.clone(),
-            render: self.render.clone(),
+        match self {
+            State::Dragging {
+                window_id,
+                position,
+                region_offset,
+                region,
+                payload,
+                render,
+            } => Self::Dragging {
+                window_id: window_id.clone(),
+                position: position.clone(),
+                region_offset: region_offset.clone(),
+                region: region.clone(),
+                payload: payload.clone(),
+                render: render.clone(),
+            },
+            State::Canceled => State::Canceled,
         }
     }
 }
@@ -51,24 +66,27 @@ impl<V: View> DragAndDrop<V> {
     }
 
     pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
-        self.currently_dragged.as_ref().and_then(
-            |State {
-                 position,
-                 payload,
-                 window_id: window_dragged_from,
-                 ..
-             }| {
+        self.currently_dragged.as_ref().and_then(|state| {
+            if let State::Dragging {
+                position,
+                payload,
+                window_id: window_dragged_from,
+                ..
+            } = state
+            {
                 if &window_id != window_dragged_from {
                     return None;
                 }
 
                 payload
-                    .clone()
-                    .downcast::<T>()
-                    .ok()
+                    .is::<T>()
+                    .then(|| payload.clone().downcast::<T>().ok())
+                    .flatten()
                     .map(|payload| (position.clone(), payload))
-            },
-        )
+            } else {
+                None
+            }
+        })
     }
 
     pub fn dragging<T: Any>(
@@ -79,17 +97,27 @@ impl<V: View> DragAndDrop<V> {
     ) {
         let window_id = cx.window_id();
         cx.update_global::<Self, _, _>(|this, cx| {
-            let (region_offset, region) =
-                if let Some(previous_state) = this.currently_dragged.as_ref() {
-                    (previous_state.region_offset, previous_state.region)
-                } else {
-                    (
-                        event.region.origin() - event.prev_mouse_position,
-                        event.region,
-                    )
-                };
-
-            this.currently_dragged = Some(State {
+            this.notify_containers_for_window(window_id, cx);
+
+            if matches!(this.currently_dragged, Some(State::Canceled)) {
+                return;
+            }
+
+            let (region_offset, region) = if let Some(State::Dragging {
+                region_offset,
+                region,
+                ..
+            }) = this.currently_dragged.as_ref()
+            {
+                (*region_offset, *region)
+            } else {
+                (
+                    event.region.origin() - event.prev_mouse_position,
+                    event.region,
+                )
+            };
+
+            this.currently_dragged = Some(State::Dragging {
                 window_id,
                 region_offset,
                 region,
@@ -99,63 +127,114 @@ impl<V: View> DragAndDrop<V> {
                     render(payload.downcast_ref::<T>().unwrap(), cx)
                 }),
             });
-
-            this.notify_containers_for_window(window_id, cx);
         });
     }
 
     pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
-        let currently_dragged = cx.global::<Self>().currently_dragged.clone();
-
-        currently_dragged.and_then(
-            |State {
-                 window_id,
-                 region_offset,
-                 position,
-                 region,
-                 payload,
-                 render,
-             }| {
-                if cx.window_id() != window_id {
-                    return None;
+        enum DraggedElementHandler {}
+        cx.global::<Self>()
+            .currently_dragged
+            .clone()
+            .and_then(|state| {
+                match state {
+                    State::Dragging {
+                        window_id,
+                        region_offset,
+                        position,
+                        region,
+                        payload,
+                        render,
+                    } => {
+                        if cx.window_id() != window_id {
+                            return None;
+                        }
+
+                        dbg!("Rendered dragging state");
+                        let position = position + region_offset;
+                        Some(
+                            Overlay::new(
+                                MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
+                                    render(payload, cx)
+                                })
+                                .with_cursor_style(CursorStyle::Arrow)
+                                .on_up(MouseButton::Left, |_, cx| {
+                                    cx.defer(|cx| {
+                                        cx.update_global::<Self, _, _>(|this, cx| {
+                                            dbg!("Up with dragging state");
+                                            this.finish_dragging(cx)
+                                        });
+                                    });
+                                    cx.propagate_event();
+                                })
+                                .on_up_out(MouseButton::Left, |_, cx| {
+                                    cx.defer(|cx| {
+                                        cx.update_global::<Self, _, _>(|this, cx| {
+                                            dbg!("Up out with dragging state");
+                                            this.finish_dragging(cx)
+                                        });
+                                    });
+                                })
+                                // Don't block hover events or invalidations
+                                .with_hoverable(false)
+                                .constrained()
+                                .with_width(region.width())
+                                .with_height(region.height())
+                                .boxed(),
+                            )
+                            .with_anchor_position(position)
+                            .boxed(),
+                        )
+                    }
+
+                    State::Canceled => {
+                        dbg!("Rendered canceled state");
+                        Some(
+                            MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
+                                Empty::new()
+                                    .constrained()
+                                    .with_width(0.)
+                                    .with_height(0.)
+                                    .boxed()
+                            })
+                            .on_up(MouseButton::Left, |_, cx| {
+                                cx.defer(|cx| {
+                                    cx.update_global::<Self, _, _>(|this, _| {
+                                        dbg!("Up with canceled state");
+                                        this.currently_dragged = None;
+                                    });
+                                });
+                            })
+                            .on_up_out(MouseButton::Left, |_, cx| {
+                                cx.defer(|cx| {
+                                    cx.update_global::<Self, _, _>(|this, _| {
+                                        dbg!("Up out with canceled state");
+                                        this.currently_dragged = None;
+                                    });
+                                });
+                            })
+                            .boxed(),
+                        )
+                    }
                 }
+            })
+    }
 
-                let position = position + region_offset;
-
-                enum DraggedElementHandler {}
-                Some(
-                    Overlay::new(
-                        MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
-                            render(payload, cx)
-                        })
-                        .with_cursor_style(CursorStyle::Arrow)
-                        .on_up(MouseButton::Left, |_, cx| {
-                            cx.defer(|cx| {
-                                cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
-                            });
-                            cx.propagate_event();
-                        })
-                        .on_up_out(MouseButton::Left, |_, cx| {
-                            cx.defer(|cx| {
-                                cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
-                            });
-                        })
-                        // Don't block hover events or invalidations
-                        .with_hoverable(false)
-                        .constrained()
-                        .with_width(region.width())
-                        .with_height(region.height())
-                        .boxed(),
-                    )
-                    .with_anchor_position(position)
-                    .boxed(),
-                )
-            },
-        )
+    pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
+        if let Some(State::Dragging {
+            payload, window_id, ..
+        }) = &self.currently_dragged
+        {
+            if payload.is::<P>() {
+                let window_id = *window_id;
+                self.currently_dragged = Some(State::Canceled);
+                dbg!("Canceled");
+                self.notify_containers_for_window(window_id, cx);
+            }
+        }
     }
 
-    fn stop_dragging(&mut self, cx: &mut MutableAppContext) {
-        if let Some(State { window_id, .. }) = self.currently_dragged.take() {
+    fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
+        if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
             self.notify_containers_for_window(window_id, cx);
         }
     }

crates/project_panel/src/project_panel.rs 🔗

@@ -1,5 +1,5 @@
 use context_menu::{ContextMenu, ContextMenuItem};
-use drag_and_drop::Draggable;
+use drag_and_drop::{shared_payloads::DraggedProjectEntry, DragAndDrop, Draggable};
 use editor::{Cancel, Editor};
 use futures::stream::StreamExt;
 use gpui::{
@@ -72,8 +72,8 @@ pub enum ClipboardEntry {
     },
 }
 
-#[derive(Debug, PartialEq, Eq, Clone)]
-struct EntryDetails {
+#[derive(Debug, PartialEq, Eq)]
+pub struct EntryDetails {
     filename: String,
     path: Arc<Path>,
     depth: usize,
@@ -605,6 +605,10 @@ impl ProjectPanel {
                     cx.notify();
                 }
             }
+
+            cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
+                drag_and_drop.cancel_dragging::<DraggedProjectEntry>(cx);
+            })
         }
     }
 
@@ -1014,8 +1018,8 @@ impl ProjectPanel {
     }
 
     fn render_entry_visual_element<V: View>(
-        details: EntryDetails,
-        editor: &ViewHandle<Editor>,
+        details: &EntryDetails,
+        editor: Option<&ViewHandle<Editor>>,
         padding: f32,
         row_container_style: ContainerStyle,
         style: &ProjectPanelEntry,
@@ -1046,8 +1050,8 @@ impl ProjectPanel {
                 .with_width(style.icon_size)
                 .boxed(),
             )
-            .with_child(if show_editor {
-                ChildView::new(editor.clone(), cx)
+            .with_child(if show_editor && editor.is_some() {
+                ChildView::new(editor.unwrap().clone(), cx)
                     .contained()
                     .with_margin_left(style.icon_spacing)
                     .aligned()
@@ -1099,8 +1103,8 @@ impl ProjectPanel {
             };
 
             Self::render_entry_visual_element(
-                details.clone(),
-                editor,
+                &details,
+                Some(editor),
                 padding,
                 row_container_style,
                 &style,
@@ -1123,22 +1127,26 @@ impl ProjectPanel {
                 position: e.position,
             })
         })
-        .as_draggable(details.clone(), {
-            let editor = editor.clone();
-            let row_container_style = theme.dragged_entry.container;
-
-            move |payload, cx: &mut RenderContext<Workspace>| {
-                let theme = cx.global::<Settings>().theme.clone();
-                Self::render_entry_visual_element(
-                    payload.clone(),
-                    &editor,
-                    padding,
-                    row_container_style,
-                    &theme.project_panel.dragged_entry,
-                    cx,
-                )
-            }
-        })
+        .as_draggable(
+            DraggedProjectEntry {
+                path: details.path.clone(),
+            },
+            {
+                let row_container_style = theme.dragged_entry.container;
+
+                move |_, cx: &mut RenderContext<Workspace>| {
+                    let theme = cx.global::<Settings>().theme.clone();
+                    Self::render_entry_visual_element(
+                        &details,
+                        None,
+                        padding,
+                        row_container_style,
+                        &theme.project_panel.dragged_entry,
+                        cx,
+                    )
+                }
+            },
+        )
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -1,4 +1,4 @@
-use drag_and_drop::DragAndDrop;
+use drag_and_drop::{shared_payloads::DraggedProjectEntry, DragAndDrop};
 use gpui::{
     color::Color,
     elements::{Canvas, MouseEventHandler, ParentElement, Stack},
@@ -28,12 +28,18 @@ where
     MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
         // Observing hovered will cause a render when the mouse enters regardless
         // of if mouse position was accessed before
-        let hovered = state.hovered();
-        let drag_position = cx
-            .global::<DragAndDrop<Workspace>>()
-            .currently_dragged::<DraggedItem>(cx.window_id())
-            .filter(|_| hovered)
-            .map(|(drag_position, _)| drag_position);
+        let drag_position = if state.hovered() {
+            cx.global::<DragAndDrop<Workspace>>()
+                .currently_dragged::<DraggedItem>(cx.window_id())
+                .map(|(drag_position, _)| drag_position)
+                .or_else(|| {
+                    cx.global::<DragAndDrop<Workspace>>()
+                        .currently_dragged::<DraggedProjectEntry>(cx.window_id())
+                        .map(|(drag_position, _)| drag_position)
+                })
+        } else {
+            None
+        };
 
         Stack::new()
             .with_child(render_child(state, cx))
@@ -70,10 +76,14 @@ where
         }
     })
     .on_move(|_, cx| {
-        if cx
-            .global::<DragAndDrop<Workspace>>()
+        let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+
+        if drag_and_drop
             .currently_dragged::<DraggedItem>(cx.window_id())
             .is_some()
+            || drag_and_drop
+                .currently_dragged::<DraggedProjectEntry>(cx.window_id())
+                .is_some()
         {
             cx.notify();
         } else {