project panel: Fix rendering of groups of dragged project panel entries (#20686)

Piotr Osiewicz created

This PR introduces a new parameter for `on_drag` in gpui, which is an
offset from the element origin to the mouse event origin.

Release Notes:

- Fixed rendering of dragged project panel entries

Change summary

crates/collab_ui/src/collab_panel.rs      |  2 +-
crates/gpui/src/elements/div.rs           | 17 +++++++++++------
crates/project_panel/src/project_panel.rs | 24 ++++++++++++++++--------
crates/workspace/src/dock.rs              |  2 +-
crates/workspace/src/pane.rs              |  2 +-
5 files changed, 30 insertions(+), 17 deletions(-)

Detailed changes

crates/collab_ui/src/collab_panel.rs 🔗

@@ -2521,7 +2521,7 @@ impl CollabPanel {
             .flex()
             .w_full()
             .when(!channel.is_root_channel(), |el| {
-                el.on_drag(channel.clone(), move |channel, cx| {
+                el.on_drag(channel.clone(), move |channel, _, cx| {
                     cx.new_view(|_| DraggedChannelView {
                         channel: channel.clone(),
                         width,

crates/gpui/src/elements/div.rs 🔗

@@ -443,7 +443,7 @@ impl Interactivity {
     pub fn on_drag<T, W>(
         &mut self,
         value: T,
-        constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
+        constructor: impl Fn(&T, Point<Pixels>, &mut WindowContext) -> View<W> + 'static,
     ) where
         Self: Sized,
         T: 'static,
@@ -455,7 +455,9 @@ impl Interactivity {
         );
         self.drag_listener = Some((
             Box::new(value),
-            Box::new(move |value, cx| constructor(value.downcast_ref().unwrap(), cx).into()),
+            Box::new(move |value, offset, cx| {
+                constructor(value.downcast_ref().unwrap(), offset, cx).into()
+            }),
         ));
     }
 
@@ -966,14 +968,15 @@ pub trait StatefulInteractiveElement: InteractiveElement {
 
     /// On drag initiation, this callback will be used to create a new view to render the dragged value for a
     /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
-    /// the [`Self::on_drag_move`] API
+    /// the [`Self::on_drag_move`] API.
+    /// The callback also has access to the offset of triggering click from the origin of parent element.
     /// The fluent API equivalent to [`Interactivity::on_drag`]
     ///
     /// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
     fn on_drag<T, W>(
         mut self,
         value: T,
-        constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
+        constructor: impl Fn(&T, Point<Pixels>, &mut WindowContext) -> View<W> + 'static,
     ) -> Self
     where
         Self: Sized,
@@ -1056,7 +1059,8 @@ pub(crate) type ScrollWheelListener =
 
 pub(crate) type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
 
-pub(crate) type DragListener = Box<dyn Fn(&dyn Any, &mut WindowContext) -> AnyView + 'static>;
+pub(crate) type DragListener =
+    Box<dyn Fn(&dyn Any, Point<Pixels>, &mut WindowContext) -> AnyView + 'static>;
 
 type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
 
@@ -1818,7 +1822,8 @@ impl Interactivity {
                                 if let Some((drag_value, drag_listener)) = drag_listener.take() {
                                     *clicked_state.borrow_mut() = ElementClickedState::default();
                                     let cursor_offset = event.position - hitbox.origin;
-                                    let drag = (drag_listener)(drag_value.as_ref(), cx);
+                                    let drag =
+                                        (drag_listener)(drag_value.as_ref(), cursor_offset, cx);
                                     cx.active_drag = Some(AnyDrag {
                                         view: drag,
                                         value: drag_value,

crates/project_panel/src/project_panel.rs 🔗

@@ -240,6 +240,7 @@ struct DraggedProjectEntryView {
     selection: SelectedEntry,
     details: EntryDetails,
     width: Pixels,
+    click_offset: Point<Pixels>,
     selections: Arc<BTreeSet<SelectedEntry>>,
 }
 
@@ -2681,10 +2682,11 @@ impl ProjectPanel {
                     },
                 ))
             })
-            .on_drag(dragged_selection, move |selection, cx| {
+            .on_drag(dragged_selection, move |selection, click_offset, cx| {
                 cx.new_view(|_| DraggedProjectEntryView {
                     details: details.clone(),
                     width,
+                    click_offset,
                     selection: selection.active_selection,
                     selections: selection.marked_selections.clone(),
                 })
@@ -3559,15 +3561,21 @@ impl Render for DraggedProjectEntryView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let settings = ProjectPanelSettings::get_global(cx);
         let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
+
         h_flex().font(ui_font).map(|this| {
-            if self.selections.contains(&self.selection) {
-                this.flex_shrink()
-                    .p_1()
-                    .items_end()
-                    .rounded_md()
-                    .child(self.selections.len().to_string())
+            if self.selections.len() > 1 && self.selections.contains(&self.selection) {
+                this.flex_none()
+                    .w(self.width)
+                    .child(div().w(self.click_offset.x))
+                    .child(
+                        div()
+                            .p_1()
+                            .rounded_xl()
+                            .bg(cx.theme().colors().background)
+                            .child(Label::new(format!("{} entries", self.selections.len()))),
+                    )
             } else {
-                this.bg(cx.theme().colors().background).w(self.width).child(
+                this.w(self.width).bg(cx.theme().colors().background).child(
                     ListItem::new(self.selection.entry_id.to_proto() as usize)
                         .indent_level(self.details.depth)
                         .indent_step_size(px(settings.indent_size))

crates/workspace/src/dock.rs 🔗

@@ -605,7 +605,7 @@ impl Render for Dock {
             let create_resize_handle = || {
                 let handle = div()
                     .id("resize-handle")
-                    .on_drag(DraggedDock(position), |dock, cx| {
+                    .on_drag(DraggedDock(position), |dock, _, cx| {
                         cx.stop_propagation();
                         cx.new_view(|_| dock.clone())
                     })

crates/workspace/src/pane.rs 🔗

@@ -1950,7 +1950,7 @@ impl Pane {
                     is_active,
                     ix,
                 },
-                |tab, cx| cx.new_view(|_| tab.clone()),
+                |tab, _, cx| cx.new_view(|_| tab.clone()),
             )
             .drag_over::<DraggedTab>(|tab, _, cx| {
                 tab.bg(cx.theme().colors().drop_target_background)