Improve drag and drop to look and feel better

Mikayla created

WIP: Change rendering of drag and drop based on alt-modifier

Change summary

crates/collab_ui/src/collab_panel.rs      | 51 ++++++++++++++++++--
crates/drag_and_drop/src/drag_and_drop.rs | 60 ++++++++++++++++++++----
crates/project_panel/src/project_panel.rs |  2 
crates/workspace/src/pane.rs              |  2 
crates/workspace/src/workspace.rs         |  8 ++
5 files changed, 102 insertions(+), 21 deletions(-)

Detailed changes

crates/collab_ui/src/collab_panel.rs 🔗

@@ -33,7 +33,7 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     impl_actions,
-    platform::{CursorStyle, MouseButton, PromptLevel},
+    platform::{CursorStyle, ModifiersChangedEvent, MouseButton, PromptLevel},
     serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
     Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
@@ -1669,7 +1669,7 @@ impl CollabPanel {
         let mut is_dragged_over = false;
         if cx
             .global::<DragAndDrop<Workspace>>()
-            .currently_dragged::<Channel>(cx.window())
+            .currently_dragged::<DraggedChannel>(cx.window())
             .is_some()
             && self
                 .dragged_channel_target
@@ -1771,7 +1771,9 @@ impl CollabPanel {
                 )
         })
         .on_click(MouseButton::Left, move |_, this, cx| {
-            this.join_channel_chat(channel_id, cx);
+            if this.dragged_channel_target.take().is_none() {
+                this.join_channel_chat(channel_id, cx);
+            }
         })
         .on_click(MouseButton::Right, {
             let path = path.clone();
@@ -1817,16 +1819,32 @@ impl CollabPanel {
                     .currently_dragged::<DraggedChannel>(cx.window())
                     .is_some()
                 {
-                    this.dragged_channel_target = Some((channel.clone(), path.clone()));
+                    if let Some(dragged_channel_target) = &this.dragged_channel_target {
+                        if dragged_channel_target.0 != channel || dragged_channel_target.1 != path {
+                            this.dragged_channel_target = Some((channel.clone(), path.clone()));
+                            cx.notify();
+                        }
+                    } else {
+                        this.dragged_channel_target = Some((channel.clone(), path.clone()));
+                        cx.notify();
+                    }
                 }
             }
         })
         .as_draggable(
             (channel.clone(), path.parent_id()),
-            move |(channel, _), cx: &mut ViewContext<Workspace>| {
+            move |e, (channel, _), cx: &mut ViewContext<Workspace>| {
                 let theme = &theme::current(cx).collab_panel;
 
                 Flex::<Workspace>::row()
+                    .with_children(e.alt.then(|| {
+                        Svg::new("icons/plus.svg")
+                            .with_color(theme.channel_hash.color)
+                            .constrained()
+                            .with_width(theme.channel_hash.width)
+                            .aligned()
+                            .left()
+                    }))
                     .with_child(
                         Svg::new("icons/hash.svg")
                             .with_color(theme.channel_hash.color)
@@ -1840,11 +1858,17 @@ impl CollabPanel {
                             .contained()
                             .with_style(theme.channel_name.container)
                             .aligned()
-                            .left()
-                            .flex(1., true),
+                            .left(),
                     )
                     .align_children_center()
                     .contained()
+                    .with_background_color(
+                        theme
+                            .container
+                            .background_color
+                            .unwrap_or(gpui::color::Color::transparent_black()),
+                    )
+                    .contained()
                     .with_padding_left(
                         theme.channel_row.default_style().padding.left
                             + theme.channel_indent * depth as f32,
@@ -2816,6 +2840,19 @@ impl View for CollabPanel {
         self.has_focus = false;
     }
 
+    fn modifiers_changed(&mut self, _: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
+        if cx
+            .global::<DragAndDrop<Workspace>>()
+            .currently_dragged::<DraggedChannel>(cx.window())
+            .is_some()
+        {
+            cx.notify();
+            true
+        } else {
+            false
+        }
+    }
+
     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
         let theme = &theme::current(cx).collab_panel;
 

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -4,7 +4,7 @@ use collections::HashSet;
 use gpui::{
     elements::{Empty, MouseEventHandler, Overlay},
     geometry::{rect::RectF, vector::Vector2F},
-    platform::{CursorStyle, MouseButton},
+    platform::{CursorStyle, Modifiers, MouseButton},
     scene::{MouseDown, MouseDrag},
     AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
 };
@@ -21,12 +21,13 @@ enum State<V> {
         region: RectF,
     },
     Dragging {
+        modifiers: Modifiers,
         window: AnyWindowHandle,
         position: Vector2F,
         region_offset: Vector2F,
         region: RectF,
         payload: Rc<dyn Any + 'static>,
-        render: Rc<dyn Fn(Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
+        render: Rc<dyn Fn(&Modifiers, Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
     },
     Canceled,
 }
@@ -49,6 +50,7 @@ impl<V> Clone for State<V> {
                 region,
             },
             State::Dragging {
+                modifiers,
                 window,
                 position,
                 region_offset,
@@ -62,6 +64,7 @@ impl<V> Clone for State<V> {
                 region: region.clone(),
                 payload: payload.clone(),
                 render: render.clone(),
+                modifiers: modifiers.clone(),
             },
             State::Canceled => State::Canceled,
         }
@@ -111,6 +114,27 @@ impl<V: 'static> DragAndDrop<V> {
         })
     }
 
+    pub fn any_currently_dragged(&self, window: AnyWindowHandle) -> bool {
+        self.currently_dragged
+            .as_ref()
+            .map(|state| {
+                if let State::Dragging {
+                    window: window_dragged_from,
+                    ..
+                } = state
+                {
+                    if &window != window_dragged_from {
+                        return false;
+                    }
+
+                    true
+                } else {
+                    false
+                }
+            })
+            .unwrap_or(false)
+    }
+
     pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
         cx.update_global(|this: &mut Self, _| {
             this.currently_dragged = Some(State::Down {
@@ -124,7 +148,7 @@ impl<V: 'static> DragAndDrop<V> {
         event: MouseDrag,
         payload: Rc<T>,
         cx: &mut WindowContext,
-        render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
+        render: Rc<impl 'static + Fn(&Modifiers, &T, &mut ViewContext<V>) -> AnyElement<V>>,
     ) {
         let window = cx.window();
         cx.update_global(|this: &mut Self, cx| {
@@ -141,13 +165,14 @@ impl<V: 'static> DragAndDrop<V> {
                 }) => {
                     if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
                         this.currently_dragged = Some(State::Dragging {
+                            modifiers: event.modifiers,
                             window,
                             region_offset,
                             region,
                             position: event.position,
                             payload,
-                            render: Rc::new(move |payload, cx| {
-                                render(payload.downcast_ref::<T>().unwrap(), cx)
+                            render: Rc::new(move |modifiers, payload, cx| {
+                                render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
                             }),
                         });
                     } else {
@@ -163,13 +188,14 @@ impl<V: 'static> DragAndDrop<V> {
                     ..
                 }) => {
                     this.currently_dragged = Some(State::Dragging {
+                        modifiers: event.modifiers,
                         window,
                         region_offset,
                         region,
                         position: event.position,
                         payload,
-                        render: Rc::new(move |payload, cx| {
-                            render(payload.downcast_ref::<T>().unwrap(), cx)
+                        render: Rc::new(move |modifiers, payload, cx| {
+                            render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
                         }),
                     });
                 }
@@ -178,6 +204,19 @@ impl<V: 'static> DragAndDrop<V> {
         });
     }
 
+    pub fn update_modifiers(new_modifiers: Modifiers, cx: &mut ViewContext<V>) -> bool {
+        cx.update_global(|this: &mut Self, _| match &mut this.currently_dragged {
+            Some(state) => match state {
+                State::Dragging { modifiers, .. } => {
+                    *modifiers = new_modifiers;
+                    true
+                }
+                _ => false,
+            },
+            None => false,
+        })
+    }
+
     pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
         enum DraggedElementHandler {}
         cx.global::<Self>()
@@ -188,6 +227,7 @@ impl<V: 'static> DragAndDrop<V> {
                     State::Down { .. } => None,
                     State::DeadZone { .. } => None,
                     State::Dragging {
+                        modifiers,
                         window,
                         region_offset,
                         position,
@@ -205,7 +245,7 @@ impl<V: 'static> DragAndDrop<V> {
                                 MouseEventHandler::new::<DraggedElementHandler, _>(
                                     0,
                                     cx,
-                                    |_, cx| render(payload, cx),
+                                    |_, cx| render(&modifiers, payload, cx),
                                 )
                                 .with_cursor_style(CursorStyle::Arrow)
                                 .on_up(MouseButton::Left, |_, _, cx| {
@@ -295,7 +335,7 @@ pub trait Draggable<V> {
     fn as_draggable<D: View, P: Any>(
         self,
         payload: P,
-        render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
+        render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
     ) -> Self
     where
         Self: Sized;
@@ -305,7 +345,7 @@ impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
     fn as_draggable<D: View, P: Any>(
         self,
         payload: P,
-        render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
+        render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
     ) -> Self
     where
         Self: Sized,

crates/project_panel/src/project_panel.rs 🔗

@@ -1485,7 +1485,7 @@ impl ProjectPanel {
         .as_draggable(entry_id, {
             let row_container_style = theme.dragged_entry.container;
 
-            move |_, cx: &mut ViewContext<Workspace>| {
+            move |_, _, cx: &mut ViewContext<Workspace>| {
                 let theme = theme::current(cx).clone();
                 Self::render_entry_visual_element(
                     &details,

crates/workspace/src/pane.rs 🔗

@@ -1383,7 +1383,7 @@ impl Pane {
                         let theme = theme::current(cx).clone();
 
                         let detail = detail.clone();
-                        move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
+                        move |_, dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
                             let tab_style = &theme.workspace.tab_bar.dragged_tab;
                             Self::render_dragged_tab(
                                 &dragged_item.handle,

crates/workspace/src/workspace.rs 🔗

@@ -33,8 +33,8 @@ use gpui::{
     },
     impl_actions,
     platform::{
-        CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
-        WindowOptions,
+        CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
+        WindowBounds, WindowOptions,
     },
     AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
     ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
@@ -3807,6 +3807,10 @@ impl View for Workspace {
             cx.focus(&self.active_pane);
         }
     }
+
+    fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
+        DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
+    }
 }
 
 impl ViewId {