Start work on dragging entries in the project panel

Max Brunsfeld created

Change summary

crates/collab_ui2/src/collab_panel.rs       |   2 
crates/project_panel2/src/project_panel.rs  | 215 ++++++++++++++--------
crates/ui2/src/components/list/list_item.rs |   8 
3 files changed, 141 insertions(+), 84 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -2557,7 +2557,7 @@ impl CollabPanel {
                 let channel = channel.clone();
                 move |cx| {
                     let channel = channel.clone();
-                    cx.build_view({ |cx| DraggedChannelView { channel, width } })
+                    cx.build_view(|cx| DraggedChannelView { channel, width })
                 }
             })
             .drag_over::<DraggedChannelView>(|style| {

crates/project_panel2/src/project_panel.rs 🔗

@@ -29,6 +29,7 @@ use std::{
     path::Path,
     sync::Arc,
 };
+use theme::ThemeSettings;
 use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
@@ -55,7 +56,7 @@ pub struct ProjectPanel {
     clipboard_entry: Option<ClipboardEntry>,
     _dragged_entry_destination: Option<Arc<Path>>,
     _workspace: WeakView<Workspace>,
-    width: Option<f32>,
+    width: Option<Pixels>,
     pending_serialization: Task<Option<()>>,
 }
 
@@ -86,7 +87,7 @@ pub enum ClipboardEntry {
     },
 }
 
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq, Clone)]
 pub struct EntryDetails {
     filename: String,
     icon: Option<Arc<str>>,
@@ -162,6 +163,12 @@ struct SerializedProjectPanel {
     width: Option<f32>,
 }
 
+struct DraggedProjectEntryView {
+    entry_id: ProjectEntryId,
+    details: EntryDetails,
+    width: Pixels,
+}
+
 impl ProjectPanel {
     fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         let project = workspace.project().clone();
@@ -236,7 +243,6 @@ impl ProjectPanel {
                 context_menu: None,
                 filename_editor,
                 clipboard_entry: None,
-                // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
                 _dragged_entry_destination: None,
                 _workspace: workspace.weak_handle(),
                 width: None,
@@ -331,7 +337,7 @@ impl ProjectPanel {
             let panel = ProjectPanel::new(workspace, cx);
             if let Some(serialized_panel) = serialized_panel {
                 panel.update(cx, |panel, cx| {
-                    panel.width = serialized_panel.width;
+                    panel.width = serialized_panel.width.map(px);
                     cx.notify();
                 });
             }
@@ -346,7 +352,9 @@ impl ProjectPanel {
                 KEY_VALUE_STORE
                     .write_kvp(
                         PROJECT_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedProjectPanel { width })?,
+                        serde_json::to_string(&SerializedProjectPanel {
+                            width: width.map(|p| p.0),
+                        })?,
                     )
                     .await?;
                 anyhow::Ok(())
@@ -1003,37 +1011,36 @@ impl ProjectPanel {
         }
     }
 
-    // todo!()
-    // fn move_entry(
-    //     &mut self,
-    //     entry_to_move: ProjectEntryId,
-    //     destination: ProjectEntryId,
-    //     destination_is_file: bool,
-    //     cx: &mut ViewContext<Self>,
-    // ) {
-    //     let destination_worktree = self.project.update(cx, |project, cx| {
-    //         let entry_path = project.path_for_entry(entry_to_move, cx)?;
-    //         let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
-
-    //         let mut destination_path = destination_entry_path.as_ref();
-    //         if destination_is_file {
-    //             destination_path = destination_path.parent()?;
-    //         }
-
-    //         let mut new_path = destination_path.to_path_buf();
-    //         new_path.push(entry_path.path.file_name()?);
-    //         if new_path != entry_path.path.as_ref() {
-    //             let task = project.rename_entry(entry_to_move, new_path, cx);
-    //             cx.foreground_executor().spawn(task).detach_and_log_err(cx);
-    //         }
-
-    //         Some(project.worktree_id_for_entry(destination, cx)?)
-    //     });
-
-    //     if let Some(destination_worktree) = destination_worktree {
-    //         self.expand_entry(destination_worktree, destination, cx);
-    //     }
-    // }
+    fn move_entry(
+        &mut self,
+        entry_to_move: ProjectEntryId,
+        destination: ProjectEntryId,
+        destination_is_file: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let destination_worktree = self.project.update(cx, |project, cx| {
+            let entry_path = project.path_for_entry(entry_to_move, cx)?;
+            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
+
+            let mut destination_path = destination_entry_path.as_ref();
+            if destination_is_file {
+                destination_path = destination_path.parent()?;
+            }
+
+            let mut new_path = destination_path.to_path_buf();
+            new_path.push(entry_path.path.file_name()?);
+            if new_path != entry_path.path.as_ref() {
+                let task = project.rename_entry(entry_to_move, new_path, cx);
+                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
+            }
+
+            Some(project.worktree_id_for_entry(destination, cx)?)
+        });
+
+        if let Some(destination_worktree) = destination_worktree {
+            self.expand_entry(destination_worktree, destination, cx);
+        }
+    }
 
     fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
         let mut entry_index = 0;
@@ -1349,15 +1356,15 @@ impl ProjectPanel {
         &self,
         entry_id: ProjectEntryId,
         details: EntryDetails,
-        // dragged_entry_destination: &mut Option<Arc<Path>>,
         cx: &mut ViewContext<Self>,
-    ) -> ListItem {
+    ) -> Stateful<Div> {
         let kind = details.kind;
         let settings = ProjectPanelSettings::get_global(cx);
         let show_editor = details.is_editing && !details.is_processing;
         let is_selected = self
             .selection
             .map_or(false, |selection| selection.entry_id == entry_id);
+        let width = self.width.unwrap_or(px(0.));
 
         let theme = cx.theme();
         let filename_text_color = details
@@ -1370,52 +1377,69 @@ impl ProjectPanel {
             })
             .unwrap_or(theme.status().info);
 
-        ListItem::new(entry_id.to_proto() as usize)
-            .indent_level(details.depth)
-            .indent_step_size(px(settings.indent_size))
-            .selected(is_selected)
-            .child(if let Some(icon) = &details.icon {
-                div().child(IconElement::from_path(icon.to_string()))
-            } else {
-                div()
+        div()
+            .id(entry_id.to_proto() as usize)
+            .on_drag({
+                let details = details.clone();
+                move |cx| {
+                    let details = details.clone();
+                    cx.build_view(|_| DraggedProjectEntryView {
+                        details,
+                        width,
+                        entry_id,
+                    })
+                }
+            })
+            .drag_over::<DraggedProjectEntryView>(|style| {
+                style.bg(cx.theme().colors().ghost_element_hover)
             })
+            .on_drop(cx.listener(
+                move |this, dragged_view: &View<DraggedProjectEntryView>, cx| {
+                    this.move_entry(dragged_view.read(cx).entry_id, entry_id, kind.is_file(), cx);
+                },
+            ))
             .child(
-                if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
-                    div().h_full().w_full().child(editor.clone())
-                } else {
-                    div()
-                        .text_color(filename_text_color)
-                        .child(Label::new(details.filename.clone()))
-                }
-                .ml_1(),
-            )
-            .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
-                if event.down.button == MouseButton::Right {
-                    return;
-                }
-                if !show_editor {
-                    if kind.is_dir() {
-                        this.toggle_expanded(entry_id, cx);
+                ListItem::new(entry_id.to_proto() as usize)
+                    .indent_level(details.depth)
+                    .indent_step_size(px(settings.indent_size))
+                    .selected(is_selected)
+                    .child(if let Some(icon) = &details.icon {
+                        div().child(IconElement::from_path(icon.to_string()))
                     } else {
-                        if event.down.modifiers.command {
-                            this.split_entry(entry_id, cx);
+                        div()
+                    })
+                    .child(
+                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
+                            div().h_full().w_full().child(editor.clone())
                         } else {
-                            this.open_entry(entry_id, event.up.click_count > 1, cx);
+                            div()
+                                .text_color(filename_text_color)
+                                .child(Label::new(details.filename.clone()))
                         }
-                    }
-                }
-            }))
-            .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
-                this.deploy_context_menu(event.position, entry_id, cx);
-            }))
-        // .on_drop::<ProjectEntryId>(|this, event, cx| {
-        //     this.move_entry(
-        //         *dragged_entry,
-        //         entry_id,
-        //         matches!(details.kind, EntryKind::File(_)),
-        //         cx,
-        //     );
-        // })
+                        .ml_1(),
+                    )
+                    .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
+                        if event.down.button == MouseButton::Right {
+                            return;
+                        }
+                        if !show_editor {
+                            if kind.is_dir() {
+                                this.toggle_expanded(entry_id, cx);
+                            } else {
+                                if event.down.modifiers.command {
+                                    this.split_entry(entry_id, cx);
+                                } else {
+                                    this.open_entry(entry_id, event.up.click_count > 1, cx);
+                                }
+                            }
+                        }
+                    }))
+                    .on_secondary_mouse_down(cx.listener(
+                        move |this, event: &MouseDownEvent, cx| {
+                            this.deploy_context_menu(event.position, entry_id, cx);
+                        },
+                    )),
+            )
     }
 
     fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
@@ -1430,7 +1454,6 @@ impl ProjectPanel {
         };
 
         dispatch_context.add(identifier);
-
         dispatch_context
     }
 }
@@ -1503,6 +1526,30 @@ impl Render for ProjectPanel {
     }
 }
 
+impl Render for DraggedProjectEntryView {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let settings = ProjectPanelSettings::get_global(cx);
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+        h_stack()
+            .font(ui_font)
+            .bg(cx.theme().colors().background)
+            .w(self.width)
+            .child(
+                ListItem::new(self.entry_id.to_proto() as usize)
+                    .indent_level(self.details.depth)
+                    .indent_step_size(px(settings.indent_size))
+                    .child(if let Some(icon) = &self.details.icon {
+                        div().child(IconElement::from_path(icon.to_string()))
+                    } else {
+                        div()
+                    })
+                    .child(Label::new(self.details.filename.clone())),
+            )
+    }
+}
+
 impl EventEmitter<Event> for ProjectPanel {}
 
 impl EventEmitter<PanelEvent> for ProjectPanel {}
@@ -1534,12 +1581,14 @@ impl Panel for ProjectPanel {
     }
 
     fn size(&self, cx: &WindowContext) -> f32 {
-        self.width
-            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
+        self.width.map_or_else(
+            || ProjectPanelSettings::get_global(cx).default_width,
+            |width| width.0,
+        )
     }
 
     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-        self.width = size;
+        self.width = size.map(px);
         self.serialize(cx);
         cx.notify();
     }

crates/ui2/src/components/list/list_item.rs 🔗

@@ -54,6 +54,14 @@ impl ListItem {
         self
     }
 
+    pub fn on_drag(
+        mut self,
+        handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.on_secondary_mouse_down = Some(Box::new(handler));
+        self
+    }
+
     pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
         self.tooltip = Some(Box::new(tooltip));
         self