Cargo.lock 🔗
@@ -4265,6 +4265,7 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"context_menu",
+ "drag_and_drop",
"editor",
"futures 0.3.24",
"gpui",
Julia and Kay Simmons created
Co-Authored-By: Kay Simmons <kay@zed.dev>
Cargo.lock | 1
crates/drag_and_drop/src/drag_and_drop.rs | 23 ++
crates/project_panel/Cargo.toml | 1
crates/project_panel/src/project_panel.rs | 168 +++++++++++++++---------
crates/theme/src/theme.rs | 1
styles/src/styleTree/projectPanel.ts | 17 ++
styles/src/styleTree/tabBar.ts | 2
7 files changed, 142 insertions(+), 71 deletions(-)
@@ -4265,6 +4265,7 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"context_menu",
+ "drag_and_drop",
"editor",
"futures 0.3.24",
"gpui",
@@ -3,7 +3,7 @@ use std::{any::Any, rc::Rc};
use collections::HashSet;
use gpui::{
elements::{MouseEventHandler, Overlay},
- geometry::vector::Vector2F,
+ geometry::{rect::RectF, vector::Vector2F},
scene::MouseDrag,
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
View, WeakViewHandle,
@@ -13,6 +13,7 @@ struct State<V: View> {
window_id: usize,
position: Vector2F,
region_offset: Vector2F,
+ region: RectF,
payload: Rc<dyn Any + 'static>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
}
@@ -23,6 +24,7 @@ impl<V: View> Clone for State<V> {
window_id: self.window_id.clone(),
position: self.position.clone(),
region_offset: self.region_offset.clone(),
+ region: self.region.clone(),
payload: self.payload.clone(),
render: self.render.clone(),
}
@@ -77,15 +79,20 @@ impl<V: View> DragAndDrop<V> {
) {
let window_id = cx.window_id();
cx.update_global::<Self, _, _>(|this, cx| {
- let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
- previous_state.region_offset
- } else {
- event.region.origin() - event.prev_mouse_position
- };
+ let (region_offset, region) =
+ if let Some(previous_state) = this.currently_dragged.as_ref() {
+ (previous_state.region_offset, previous_state.region)
+ } else {
+ (
+ event.region.origin() - event.prev_mouse_position,
+ event.region,
+ )
+ };
this.currently_dragged = Some(State {
window_id,
region_offset,
+ region,
position: event.position,
payload,
render: Rc::new(move |payload, cx| {
@@ -105,6 +112,7 @@ impl<V: View> DragAndDrop<V> {
window_id,
region_offset,
position,
+ region,
payload,
render,
}| {
@@ -134,6 +142,9 @@ impl<V: View> DragAndDrop<V> {
})
// Don't block hover events or invalidations
.with_hoverable(false)
+ .constrained()
+ .with_width(region.width())
+ .with_height(region.height())
.boxed(),
)
.with_anchor_position(position)
@@ -9,6 +9,7 @@ doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
+drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
@@ -1,12 +1,13 @@
use context_menu::{ContextMenu, ContextMenuItem};
+use drag_and_drop::Draggable;
use editor::{Cancel, Editor};
use futures::stream::StreamExt;
use gpui::{
actions,
anyhow::{anyhow, Result},
elements::{
- AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
- ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
+ AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
+ MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
impl_internal_actions, keymap,
@@ -25,6 +26,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
+use theme::ProjectPanelEntry;
use unicase::UniCase;
use workspace::Workspace;
@@ -70,9 +72,10 @@ pub enum ClipboardEntry {
},
}
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq, Clone)]
struct EntryDetails {
filename: String,
+ path: Arc<Path>,
depth: usize,
kind: EntryKind,
is_ignored: bool,
@@ -220,6 +223,7 @@ impl ProjectPanel {
this.update_visible_entries(None, cx);
this
});
+
cx.subscribe(&project_panel, {
let project_panel = project_panel.downgrade();
move |workspace, _, event, cx| match event {
@@ -950,14 +954,15 @@ impl ProjectPanel {
let end_ix = range.end.min(ix + visible_worktree_entries.len());
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
+ let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
.expanded_dir_ids
.get(&snapshot.id())
.map(Vec::as_slice)
.unwrap_or(&[]);
- let root_name = OsStr::new(snapshot.root_name());
- for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
- {
+
+ let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
+ for entry in &visible_worktree_entries[entry_range] {
let mut details = EntryDetails {
filename: entry
.path
@@ -965,6 +970,7 @@ impl ProjectPanel {
.unwrap_or(root_name)
.to_string_lossy()
.to_string(),
+ path: entry.path.clone(),
depth: entry.path.components().count(),
kind: entry.kind,
is_ignored: entry.is_ignored,
@@ -978,12 +984,14 @@ impl ProjectPanel {
.clipboard_entry
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
};
+
if let Some(edit_state) = &self.edit_state {
let is_edited_entry = if edit_state.is_new_entry {
entry.id == NEW_ENTRY_ID
} else {
entry.id == edit_state.entry_id
};
+
if is_edited_entry {
if let Some(processing_filename) = &edit_state.processing_filename {
details.is_processing = true;
@@ -1005,6 +1013,63 @@ impl ProjectPanel {
}
}
+ fn render_entry_visual_element<V: View>(
+ details: EntryDetails,
+ editor: &ViewHandle<Editor>,
+ padding: f32,
+ row_container_style: ContainerStyle,
+ style: &ProjectPanelEntry,
+ cx: &mut RenderContext<V>,
+ ) -> ElementBox {
+ let kind = details.kind;
+ let show_editor = details.is_editing && !details.is_processing;
+
+ Flex::row()
+ .with_child(
+ ConstrainedBox::new(if kind == EntryKind::Dir {
+ if details.is_expanded {
+ Svg::new("icons/chevron_down_8.svg")
+ .with_color(style.icon_color)
+ .boxed()
+ } else {
+ Svg::new("icons/chevron_right_8.svg")
+ .with_color(style.icon_color)
+ .boxed()
+ }
+ } else {
+ Empty::new().boxed()
+ })
+ .with_max_width(style.icon_size)
+ .with_max_height(style.icon_size)
+ .aligned()
+ .constrained()
+ .with_width(style.icon_size)
+ .boxed(),
+ )
+ .with_child(if show_editor {
+ ChildView::new(editor.clone(), cx)
+ .contained()
+ .with_margin_left(style.icon_spacing)
+ .aligned()
+ .left()
+ .flex(1.0, true)
+ .boxed()
+ } else {
+ Label::new(details.filename.clone(), style.text.clone())
+ .contained()
+ .with_margin_left(style.icon_spacing)
+ .aligned()
+ .left()
+ .boxed()
+ })
+ .constrained()
+ .with_height(style.height)
+ .contained()
+ .with_style(row_container_style)
+ .with_padding_left(padding)
+ .boxed()
+ }
+
fn render_entry(
entry_id: ProjectEntryId,
details: EntryDetails,
@@ -1013,69 +1078,34 @@ impl ProjectPanel {
cx: &mut RenderContext<Self>,
) -> ElementBox {
let kind = details.kind;
- let show_editor = details.is_editing && !details.is_processing;
- MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
- let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
+ let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
- let entry_style = if details.is_cut {
- &theme.cut_entry
- } else if details.is_ignored {
- &theme.ignored_entry
- } else {
- &theme.entry
- };
+ let entry_style = if details.is_cut {
+ &theme.cut_entry
+ } else if details.is_ignored {
+ &theme.ignored_entry
+ } else {
+ &theme.entry
+ };
- let style = entry_style.style_for(state, details.is_selected).clone();
+ let show_editor = details.is_editing && !details.is_processing;
+ MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
+ let style = entry_style.style_for(state, details.is_selected).clone();
let row_container_style = if show_editor {
theme.filename_editor.container
} else {
style.container
};
- Flex::row()
- .with_child(
- ConstrainedBox::new(if kind == EntryKind::Dir {
- if details.is_expanded {
- Svg::new("icons/chevron_down_8.svg")
- .with_color(style.icon_color)
- .boxed()
- } else {
- Svg::new("icons/chevron_right_8.svg")
- .with_color(style.icon_color)
- .boxed()
- }
- } else {
- Empty::new().boxed()
- })
- .with_max_width(style.icon_size)
- .with_max_height(style.icon_size)
- .aligned()
- .constrained()
- .with_width(style.icon_size)
- .boxed(),
- )
- .with_child(if show_editor {
- ChildView::new(editor.clone(), cx)
- .contained()
- .with_margin_left(theme.entry.default.icon_spacing)
- .aligned()
- .left()
- .flex(1.0, true)
- .boxed()
- } else {
- Label::new(details.filename, style.text.clone())
- .contained()
- .with_margin_left(style.icon_spacing)
- .aligned()
- .left()
- .boxed()
- })
- .constrained()
- .with_height(theme.entry.default.height)
- .contained()
- .with_style(row_container_style)
- .with_padding_left(padding)
- .boxed()
+
+ Self::render_entry_visual_element(
+ details.clone(),
+ editor,
+ padding,
+ row_container_style,
+ &style,
+ cx,
+ )
})
.on_click(MouseButton::Left, move |e, cx| {
if kind == EntryKind::Dir {
@@ -1093,6 +1123,22 @@ impl ProjectPanel {
position: e.position,
})
})
+ .as_draggable(details.clone(), {
+ let editor = editor.clone();
+ let row_container_style = theme.dragged_entry.container;
+
+ move |payload, cx: &mut RenderContext<Workspace>| {
+ let theme = cx.global::<Settings>().theme.clone();
+ Self::render_entry_visual_element(
+ payload.clone(),
+ &editor,
+ padding,
+ row_container_style,
+ &theme.project_panel.dragged_entry,
+ cx,
+ )
+ }
+ })
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -326,6 +326,7 @@ pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>,
+ pub dragged_entry: ProjectPanelEntry,
pub ignored_entry: Interactive<ProjectPanelEntry>,
pub cut_entry: Interactive<ProjectPanelEntry>,
pub filename_editor: FieldEditor,
@@ -1,14 +1,19 @@
import { ColorScheme } from "../themes/common/colorScheme";
-import { background, foreground, text } from "./components";
+import { withOpacity } from "../utils/color";
+import { background, border, foreground, text } from "./components";
export default function projectPanel(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
-
- let entry = {
+
+ let baseEntry = {
height: 24,
iconColor: foreground(layer, "variant"),
iconSize: 8,
iconSpacing: 8,
+ }
+
+ let entry = {
+ ...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }),
hover: {
background: background(layer, "variant", "hovered"),
@@ -28,6 +33,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
padding: { left: 12, right: 12, top: 6, bottom: 6 },
indentWidth: 8,
entry,
+ draggedEntry: {
+ ...baseEntry,
+ text: text(layer, "mono", "on", { size: "sm" }),
+ background: withOpacity(background(layer, "on"), 0.9),
+ border: border(layer),
+ },
ignoredEntry: {
...entry,
text: text(layer, "mono", "disabled"),
@@ -67,7 +67,7 @@ export default function tabBar(colorScheme: ColorScheme) {
const draggedTab = {
...activePaneActiveTab,
- background: withOpacity(tab.background, 0.95),
+ background: withOpacity(tab.background, 0.9),
border: undefined as any,
shadow: colorScheme.popoverShadow,
};