From f314662048982f71b3bb2f9c071be699f0f11b08 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Tue, 28 Jan 2025 14:12:10 +0530 Subject: [PATCH] project_panel: Add precise drag-and-drop for files onto folded directories (#22983) Closes #19192 1. Changed the drag overlay of entries for better visibility of where to drop. 2. Folded directories (except for the last folded one) will be highlighted as drop targets. 3. The delimiter between folded directories prevents the directory highlight from losing focus and acts as part of the directory to avoid flickering. This works just like VS Code does. [fold-drop.webm](https://github.com/user-attachments/assets/853f7c5e-3492-4f56-9736-6d0e3ef09325) Release Notes: - Added precise drag-and-drop for files onto folded directories in the Project Panel. --------- Co-authored-by: Marshall Bowers --- crates/project_panel/src/project_panel.rs | 166 +++++++++++++++++----- 1 file changed, 129 insertions(+), 37 deletions(-) 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())) + } + }), + ) } }