Start dragging project panel entries

Co-Authored-By: Kay Simmons <kay@zed.dev>
This commit is contained in:
Julia 2022-11-07 17:00:01 -05:00
parent 1d6af4cf20
commit 847376a4f5
7 changed files with 142 additions and 71 deletions

1
Cargo.lock generated
View file

@ -4265,6 +4265,7 @@ name = "project_panel"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"context_menu", "context_menu",
"drag_and_drop",
"editor", "editor",
"futures 0.3.24", "futures 0.3.24",
"gpui", "gpui",

View file

@ -3,7 +3,7 @@ use std::{any::Any, rc::Rc};
use collections::HashSet; use collections::HashSet;
use gpui::{ use gpui::{
elements::{MouseEventHandler, Overlay}, elements::{MouseEventHandler, Overlay},
geometry::vector::Vector2F, geometry::{rect::RectF, vector::Vector2F},
scene::MouseDrag, scene::MouseDrag,
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext, CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
View, WeakViewHandle, View, WeakViewHandle,
@ -13,6 +13,7 @@ struct State<V: View> {
window_id: usize, window_id: usize,
position: Vector2F, position: Vector2F,
region_offset: Vector2F, region_offset: Vector2F,
region: RectF,
payload: Rc<dyn Any + 'static>, payload: Rc<dyn Any + 'static>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>, 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(), window_id: self.window_id.clone(),
position: self.position.clone(), position: self.position.clone(),
region_offset: self.region_offset.clone(), region_offset: self.region_offset.clone(),
region: self.region.clone(),
payload: self.payload.clone(), payload: self.payload.clone(),
render: self.render.clone(), render: self.render.clone(),
} }
@ -77,15 +79,20 @@ impl<V: View> DragAndDrop<V> {
) { ) {
let window_id = cx.window_id(); let window_id = cx.window_id();
cx.update_global::<Self, _, _>(|this, cx| { cx.update_global::<Self, _, _>(|this, cx| {
let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() { let (region_offset, region) =
previous_state.region_offset if let Some(previous_state) = this.currently_dragged.as_ref() {
} else { (previous_state.region_offset, previous_state.region)
event.region.origin() - event.prev_mouse_position } else {
}; (
event.region.origin() - event.prev_mouse_position,
event.region,
)
};
this.currently_dragged = Some(State { this.currently_dragged = Some(State {
window_id, window_id,
region_offset, region_offset,
region,
position: event.position, position: event.position,
payload, payload,
render: Rc::new(move |payload, cx| { render: Rc::new(move |payload, cx| {
@ -105,6 +112,7 @@ impl<V: View> DragAndDrop<V> {
window_id, window_id,
region_offset, region_offset,
position, position,
region,
payload, payload,
render, render,
}| { }| {
@ -134,6 +142,9 @@ impl<V: View> DragAndDrop<V> {
}) })
// Don't block hover events or invalidations // Don't block hover events or invalidations
.with_hoverable(false) .with_hoverable(false)
.constrained()
.with_width(region.width())
.with_height(region.height())
.boxed(), .boxed(),
) )
.with_anchor_position(position) .with_anchor_position(position)

View file

@ -9,6 +9,7 @@ doctest = false
[dependencies] [dependencies]
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" } menu = { path = "../menu" }

View file

@ -1,12 +1,13 @@
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use drag_and_drop::Draggable;
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use gpui::{ use gpui::{
actions, actions,
anyhow::{anyhow, Result}, anyhow::{anyhow, Result},
elements::{ elements::{
AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
}, },
geometry::vector::Vector2F, geometry::vector::Vector2F,
impl_internal_actions, keymap, impl_internal_actions, keymap,
@ -25,6 +26,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use theme::ProjectPanelEntry;
use unicase::UniCase; use unicase::UniCase;
use workspace::Workspace; use workspace::Workspace;
@ -70,9 +72,10 @@ pub enum ClipboardEntry {
}, },
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Clone)]
struct EntryDetails { struct EntryDetails {
filename: String, filename: String,
path: Arc<Path>,
depth: usize, depth: usize,
kind: EntryKind, kind: EntryKind,
is_ignored: bool, is_ignored: bool,
@ -220,6 +223,7 @@ impl ProjectPanel {
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
this this
}); });
cx.subscribe(&project_panel, { cx.subscribe(&project_panel, {
let project_panel = project_panel.downgrade(); let project_panel = project_panel.downgrade();
move |workspace, _, event, cx| match event { move |workspace, _, event, cx| match event {
@ -950,14 +954,15 @@ impl ProjectPanel {
let end_ix = range.end.min(ix + visible_worktree_entries.len()); 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) { if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot(); let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self let expanded_entry_ids = self
.expanded_dir_ids .expanded_dir_ids
.get(&snapshot.id()) .get(&snapshot.id())
.map(Vec::as_slice) .map(Vec::as_slice)
.unwrap_or(&[]); .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 { let mut details = EntryDetails {
filename: entry filename: entry
.path .path
@ -965,6 +970,7 @@ impl ProjectPanel {
.unwrap_or(root_name) .unwrap_or(root_name)
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),
path: entry.path.clone(),
depth: entry.path.components().count(), depth: entry.path.components().count(),
kind: entry.kind, kind: entry.kind,
is_ignored: entry.is_ignored, is_ignored: entry.is_ignored,
@ -978,12 +984,14 @@ impl ProjectPanel {
.clipboard_entry .clipboard_entry
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
}; };
if let Some(edit_state) = &self.edit_state { if let Some(edit_state) = &self.edit_state {
let is_edited_entry = if edit_state.is_new_entry { let is_edited_entry = if edit_state.is_new_entry {
entry.id == NEW_ENTRY_ID entry.id == NEW_ENTRY_ID
} else { } else {
entry.id == edit_state.entry_id entry.id == edit_state.entry_id
}; };
if is_edited_entry { if is_edited_entry {
if let Some(processing_filename) = &edit_state.processing_filename { if let Some(processing_filename) = &edit_state.processing_filename {
details.is_processing = true; 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( fn render_entry(
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
details: EntryDetails, details: EntryDetails,
@ -1013,69 +1078,34 @@ impl ProjectPanel {
cx: &mut RenderContext<Self>, cx: &mut RenderContext<Self>,
) -> ElementBox { ) -> ElementBox {
let kind = details.kind; let kind = details.kind;
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 show_editor = details.is_editing && !details.is_processing; let show_editor = details.is_editing && !details.is_processing;
MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| { MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
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 style = entry_style.style_for(state, details.is_selected).clone(); let style = entry_style.style_for(state, details.is_selected).clone();
let row_container_style = if show_editor { let row_container_style = if show_editor {
theme.filename_editor.container theme.filename_editor.container
} else { } else {
style.container style.container
}; };
Flex::row()
.with_child( Self::render_entry_visual_element(
ConstrainedBox::new(if kind == EntryKind::Dir { details.clone(),
if details.is_expanded { editor,
Svg::new("icons/chevron_down_8.svg") padding,
.with_color(style.icon_color) row_container_style,
.boxed() &style,
} else { cx,
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()
}) })
.on_click(MouseButton::Left, move |e, cx| { .on_click(MouseButton::Left, move |e, cx| {
if kind == EntryKind::Dir { if kind == EntryKind::Dir {
@ -1093,6 +1123,22 @@ impl ProjectPanel {
position: e.position, 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) .with_cursor_style(CursorStyle::PointingHand)
.boxed() .boxed()
} }

View file

@ -326,6 +326,7 @@ pub struct ProjectPanel {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>, pub entry: Interactive<ProjectPanelEntry>,
pub dragged_entry: ProjectPanelEntry,
pub ignored_entry: Interactive<ProjectPanelEntry>, pub ignored_entry: Interactive<ProjectPanelEntry>,
pub cut_entry: Interactive<ProjectPanelEntry>, pub cut_entry: Interactive<ProjectPanelEntry>,
pub filename_editor: FieldEditor, pub filename_editor: FieldEditor,

View file

@ -1,14 +1,19 @@
import { ColorScheme } from "../themes/common/colorScheme"; 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) { export default function projectPanel(colorScheme: ColorScheme) {
let layer = colorScheme.middle; let layer = colorScheme.middle;
let entry = { let baseEntry = {
height: 24, height: 24,
iconColor: foreground(layer, "variant"), iconColor: foreground(layer, "variant"),
iconSize: 8, iconSize: 8,
iconSpacing: 8, iconSpacing: 8,
}
let entry = {
...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }), text: text(layer, "mono", "variant", { size: "sm" }),
hover: { hover: {
background: background(layer, "variant", "hovered"), background: background(layer, "variant", "hovered"),
@ -28,6 +33,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
padding: { left: 12, right: 12, top: 6, bottom: 6 }, padding: { left: 12, right: 12, top: 6, bottom: 6 },
indentWidth: 8, indentWidth: 8,
entry, entry,
draggedEntry: {
...baseEntry,
text: text(layer, "mono", "on", { size: "sm" }),
background: withOpacity(background(layer, "on"), 0.9),
border: border(layer),
},
ignoredEntry: { ignoredEntry: {
...entry, ...entry,
text: text(layer, "mono", "disabled"), text: text(layer, "mono", "disabled"),

View file

@ -67,7 +67,7 @@ export default function tabBar(colorScheme: ColorScheme) {
const draggedTab = { const draggedTab = {
...activePaneActiveTab, ...activePaneActiveTab,
background: withOpacity(tab.background, 0.95), background: withOpacity(tab.background, 0.9),
border: undefined as any, border: undefined as any,
shadow: colorScheme.popoverShadow, shadow: colorScheme.popoverShadow,
}; };