mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-11 12:46:07 +00:00
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 <elliott.codes@gmail.com>
This commit is contained in:
parent
5c650cdcb2
commit
f314662048
1 changed files with 129 additions and 37 deletions
|
@ -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<ProjectEntryId, FoldedAncestors>,
|
||||
folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
|
||||
last_worktree_root_id: Option<ProjectEntryId>,
|
||||
last_selection_drag_over_entry: Option<ProjectEntryId>,
|
||||
last_external_paths_drag_over_entry: Option<ProjectEntryId>,
|
||||
|
@ -107,6 +108,14 @@ pub struct ProjectPanel {
|
|||
hover_expand_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[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<Pixels>,
|
||||
selections: Arc<BTreeSet<SelectedEntry>>,
|
||||
}
|
||||
|
@ -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::<DraggedSelection>(move |style, _, _, _| style.bg(item_colors.drag_over))
|
||||
.drag_over::<DraggedSelection>(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<DraggedSelection>, _, _| {
|
||||
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<DraggedSelection>, _, _| {
|
||||
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<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.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()))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue