diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7e6596f5a8..1807275cbc 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -79,6 +79,7 @@ pub struct ProjectPanel { /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// project entries (and all non-leaf nodes are guaranteed to be directories). ancestors: HashMap, + folded_directory_drag_target: Option, last_worktree_root_id: Option, last_selection_drag_over_entry: Option, last_external_paths_drag_over_entry: Option, @@ -107,6 +108,14 @@ pub struct ProjectPanel { hover_expand_task: Option>, } +#[derive(Copy, Clone, Debug)] +struct FoldedDirectoryDragTarget { + entry_id: ProjectEntryId, + index: usize, + /// Whether we are dragging over the delimiter rather than the component itself. + is_delimiter_target: bool, +} + #[derive(Clone, Debug)] struct EditState { worktree_id: WorktreeId, @@ -249,7 +258,6 @@ struct SerializedProjectPanel { struct DraggedProjectEntryView { selection: SelectedEntry, details: EntryDetails, - width: Pixels, click_offset: Point, selections: Arc>, } @@ -418,6 +426,7 @@ impl ProjectPanel { focus_handle, visible_entries: Default::default(), ancestors: Default::default(), + folded_directory_drag_target: None, last_worktree_root_id: Default::default(), last_external_paths_drag_over_entry: None, last_selection_drag_over_entry: None, @@ -3464,7 +3473,6 @@ impl ProjectPanel { .selection .map_or(false, |selection| selection.entry_id == entry_id); - let width = self.size(window, cx); let file_name = details.filename.clone(); let mut icon = details.icon.clone(); @@ -3523,6 +3531,8 @@ impl ProjectPanel { bg_hover_color }; + let folded_directory_drag_target = self.folded_directory_drag_target; + div() .id(entry_id.to_proto() as usize) .group(GROUP_NAME) @@ -3634,18 +3644,25 @@ impl ProjectPanel { move |selection, click_offset, _window, cx| { cx.new(|_| DraggedProjectEntryView { details: details.clone(), - width, click_offset, selection: selection.active_selection, selections: selection.marked_selections.clone(), }) }, ) - .drag_over::(move |style, _, _, _| style.bg(item_colors.drag_over)) + .drag_over::(move |style, _, _, _| { + if folded_directory_drag_target.is_some() { + return style; + } + style.bg(item_colors.drag_over) + }) .on_drop( cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.hover_expand_task.take(); + if folded_directory_drag_target.is_some() { + return; + } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), ) @@ -3832,15 +3849,51 @@ impl ProjectPanel { let active_index = components_len - 1 - folded_ancestors.current_ancestor_depth; - const DELIMITER: SharedString = + const DELIMITER: SharedString = SharedString::new_static(std::path::MAIN_SEPARATOR_STR); for (index, component) in components.into_iter().enumerate() { if index != 0 { - this = this.child( - Label::new(DELIMITER.clone()) - .single_line() - .color(filename_text_color), - ); + let delimiter_target_index = index - 1; + let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned(); + this = this.child( + div() + .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { + this.hover_scroll_task.take(); + this.folded_directory_drag_target = None; + if let Some(target_entry_id) = target_entry_id { + this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); + } + })) + .on_drag_move(cx.listener( + move |this, event: &DragMoveEvent, _, _| { + if event.bounds.contains(&event.event.position) { + this.folded_directory_drag_target = Some( + FoldedDirectoryDragTarget { + entry_id, + index: delimiter_target_index, + is_delimiter_target: true, + } + ); + } else { + let is_current_target = this.folded_directory_drag_target + .map_or(false, |target| + target.entry_id == entry_id && + target.index == delimiter_target_index && + target.is_delimiter_target + ); + if is_current_target { + this.folded_directory_drag_target = None; + } + } + + }, + )) + .child( + Label::new(DELIMITER.clone()) + .single_line() + .color(filename_text_color) + ) + ); } let id = SharedString::from(format!( "project_panel_path_component_{}_{index}", @@ -3859,6 +3912,47 @@ impl ProjectPanel { } } })) + .when(index != components_len - 1, |div|{ + let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); + div + .on_drag_move(cx.listener( + move |this, event: &DragMoveEvent, _, _| { + if event.bounds.contains(&event.event.position) { + this.folded_directory_drag_target = Some( + FoldedDirectoryDragTarget { + entry_id, + index, + is_delimiter_target: false, + } + ); + } else { + let is_current_target = this.folded_directory_drag_target + .as_ref() + .map_or(false, |target| + target.entry_id == entry_id && + target.index == index && + !target.is_delimiter_target + ); + if is_current_target { + this.folded_directory_drag_target = None; + } + } + }, + )) + .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| { + this.hover_scroll_task.take(); + this.folded_directory_drag_target = None; + if let Some(target_entry_id) = target_entry_id { + this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); + } + })) + .when(folded_directory_drag_target.map_or(false, |target| + target.entry_id == entry_id && + target.index == index + ), |this| { + this.bg(item_colors.drag_over) + }) + }) .child( Label::new(component) .single_line() @@ -4547,35 +4641,33 @@ impl Render for ProjectPanel { impl Render for DraggedProjectEntryView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> 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.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.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)) - .child(if let Some(icon) = &self.details.icon { - div().child(Icon::from_path(icon.clone())) + h_flex() + .font(ui_font) + .pl(self.click_offset.x + px(12.)) + .pt(self.click_offset.y + px(12.)) + .child( + div() + .flex() + .gap_1() + .items_center() + .py_1() + .px_2() + .rounded_lg() + .bg(cx.theme().colors().background) + .map(|this| { + if self.selections.len() > 1 && self.selections.contains(&self.selection) { + this.child(Label::new(format!("{} entries", self.selections.len()))) } else { - div() - }) - .child(Label::new(self.details.filename.clone())), - ) - } - }) + this.child(if let Some(icon) = &self.details.icon { + div().child(Icon::from_path(icon.clone())) + } else { + div() + }) + .child(Label::new(self.details.filename.clone())) + } + }), + ) } }