Improve the appearance of project panel filename editor

* Always layout single-line editors with a fixed height
* Preserve directory chevron when editing folder names
* Allow theming the filename editor

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Max Brunsfeld 2022-05-02 13:19:58 -07:00
parent 333b4aaf4e
commit 8fdc5c9be3
15 changed files with 206 additions and 234 deletions

View file

@ -331,7 +331,8 @@
"context": "ProjectPanel", "context": "ProjectPanel",
"bindings": { "bindings": {
"left": "project_panel::CollapseSelectedEntry", "left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry" "right": "project_panel::ExpandSelectedEntry",
"f2": "project_panel::Rename"
} }
} }
] ]

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#26232a5c",
"text": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14
},
"selection": {
"cursor": "#576ddb",
"selection": "#576ddb3d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#e2dfe72e",
"text": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14
},
"selection": {
"cursor": "#576ddb",
"selection": "#576ddb3d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#ffffff1f",
"text": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14
},
"selection": {
"cursor": "#2472f2",
"selection": "#2472f23d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#0000000f",
"text": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14
},
"selection": {
"cursor": "#2472f2",
"selection": "#2472f23d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#0736425c",
"text": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14
},
"selection": {
"cursor": "#268bd2",
"selection": "#268bd23d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#eee8d52e",
"text": {
"family": "Zed Mono",
"color": "#073642",
"size": 14
},
"selection": {
"cursor": "#268bd2",
"selection": "#268bd23d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#2932565c",
"text": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14
},
"selection": {
"cursor": "#3d8fd1",
"selection": "#3d8fd13d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -972,6 +972,18 @@
"size": 14 "size": 14
} }
} }
},
"filename_editor": {
"background": "#dfe2f12e",
"text": {
"family": "Zed Mono",
"color": "#293256",
"size": 14
},
"selection": {
"cursor": "#3d8fd1",
"selection": "#3d8fd13d"
}
} }
}, },
"chat_panel": { "chat_panel": {

View file

@ -875,6 +875,12 @@ impl Element for EditorElement {
.max(constraint.min_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical))
.min(line_height * max_lines as f32), .min(line_height * max_lines as f32),
) )
} else if let EditorMode::SingleLine = snapshot.mode {
size.set_y(
line_height
.min(constraint.max_along(Axis::Vertical))
.max(constraint.min_along(Axis::Vertical)),
)
} else if size.y().is_infinite() { } else if size.y().is_infinite() {
size.set_y(scroll_height); size.set_y(scroll_height);
} }

View file

@ -225,6 +225,8 @@ impl DiagnosticSummary {
pub struct ProjectEntryId(usize); pub struct ProjectEntryId(usize);
impl ProjectEntryId { impl ProjectEntryId {
pub const MAX: Self = Self(usize::MAX);
pub fn new(counter: &AtomicUsize) -> Self { pub fn new(counter: &AtomicUsize) -> Self {
Self(counter.fetch_add(1, SeqCst)) Self(counter.fetch_add(1, SeqCst))
} }

View file

@ -1568,7 +1568,7 @@ pub struct Entry {
pub is_ignored: bool, pub is_ignored: bool,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntryKind { pub enum EntryKind {
PendingDir, PendingDir,
Dir, Dir,

View file

@ -11,7 +11,7 @@ use gpui::{
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use project::{Entry, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use settings::Settings; use settings::Settings;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
@ -25,6 +25,8 @@ use workspace::{
Workspace, Workspace,
}; };
const NEW_FILE_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
pub struct ProjectPanel { pub struct ProjectPanel {
project: ModelHandle<Project>, project: ModelHandle<Project>,
list: UniformListState, list: UniformListState,
@ -56,14 +58,7 @@ struct EntryDetails {
kind: EntryKind, kind: EntryKind,
is_expanded: bool, is_expanded: bool,
is_selected: bool, is_selected: bool,
} is_editing: bool,
#[derive(Debug, PartialEq, Eq)]
enum EntryKind {
File,
Dir,
FileRenameEditor,
NewFileEditor,
} }
#[derive(Clone)] #[derive(Clone)]
@ -122,8 +117,17 @@ impl ProjectPanel {
}) })
.detach(); .detach();
let editor = cx.add_view(|cx| Editor::single_line(None, cx)); let filename_editor = cx.add_view(|cx| {
cx.subscribe(&editor, |this, _, event, cx| { Editor::single_line(
Some(|theme| {
let mut style = theme.project_panel.filename_editor.clone();
style.container.background_color.take();
style
}),
cx,
)
});
cx.subscribe(&filename_editor, |this, _, event, cx| {
if let editor::Event::Blurred = event { if let editor::Event::Blurred = event {
this.editor_blurred(cx); this.editor_blurred(cx);
} }
@ -137,7 +141,7 @@ impl ProjectPanel {
expanded_dir_ids: Default::default(), expanded_dir_ids: Default::default(),
selection: None, selection: None,
edit_state: None, edit_state: None,
filename_editor: editor, filename_editor,
handle: cx.weak_handle(), handle: cx.weak_handle(),
}; };
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -399,8 +403,10 @@ impl ProjectPanel {
.path .path
.file_name() .file_name()
.map_or(String::new(), |s| s.to_string_lossy().to_string()); .map_or(String::new(), |s| s.to_string_lossy().to_string());
self.filename_editor self.filename_editor.update(cx, |editor, cx| {
.update(cx, |editor, cx| editor.set_text(filename, cx)); editor.set_text(filename, cx);
editor.select_all(&Default::default(), cx);
});
cx.focus(&self.filename_editor); cx.focus(&self.filename_editor);
self.update_visible_entries(None, cx); self.update_visible_entries(None, cx);
cx.notify(); cx.notify();
@ -542,7 +548,7 @@ impl ProjectPanel {
visible_worktree_entries.push(entry.clone()); visible_worktree_entries.push(entry.clone());
if Some(entry.id) == new_file_parent_id { if Some(entry.id) == new_file_parent_id {
visible_worktree_entries.push(Entry { visible_worktree_entries.push(Entry {
id: entry.id, id: NEW_FILE_ENTRY_ID,
kind: project::EntryKind::File(Default::default()), kind: project::EntryKind::File(Default::default()),
path: entry.path.join("\0").into(), path: entry.path.join("\0").into(),
inode: 0, inode: 0,
@ -662,30 +668,24 @@ impl ProjectPanel {
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),
depth: entry.path.components().count(), depth: entry.path.components().count(),
kind: if entry.is_dir() { kind: entry.kind,
EntryKind::Dir
} else {
EntryKind::File
},
is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
is_selected: self.selection.map_or(false, |e| { is_selected: self.selection.map_or(false, |e| {
e.worktree_id == snapshot.id() && e.entry_id == entry.id e.worktree_id == snapshot.id() && e.entry_id == entry.id
}), }),
is_editing: false,
}; };
if let Some(edit_state) = self.edit_state { if let Some(edit_state) = self.edit_state {
if edit_state.worktree_id == *worktree_id && edit_state.entry_id == entry.id
{
if edit_state.new_file { if edit_state.new_file {
if entry.is_file() { if entry.id == NEW_FILE_ENTRY_ID {
details.kind = EntryKind::NewFileEditor; details.is_editing = true;
details.filename = Default::default(); details.filename.clear();
details.is_expanded = false;
details.is_selected = false;
} }
} else { } else {
details.kind = EntryKind::FileRenameEditor; if entry.id == edit_state.entry_id {
} details.is_editing = true;
} }
};
} }
callback(entry.id, details, cx); callback(entry.id, details, cx);
} }
@ -702,21 +702,14 @@ impl ProjectPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> ElementBox { ) -> ElementBox {
let kind = details.kind; let kind = details.kind;
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
if kind == EntryKind::FileRenameEditor || kind == EntryKind::NewFileEditor {
return ChildView::new(editor.clone())
.constrained()
.with_height(theme.entry.default.height)
.contained()
.with_margin_left(
padding + theme.entry.default.icon_spacing + theme.entry.default.icon_size,
)
.boxed();
}
MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| { MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
let style = theme.entry.style_for(state, details.is_selected); let style = theme.entry.style_for(state, details.is_selected);
let row_container_style = if details.is_editing {
theme.filename_editor.container
} else {
style.container
};
Flex::row() Flex::row()
.with_child( .with_child(
ConstrainedBox::new(if kind == EntryKind::Dir { ConstrainedBox::new(if kind == EntryKind::Dir {
@ -739,18 +732,26 @@ impl ProjectPanel {
.with_width(style.icon_size) .with_width(style.icon_size)
.boxed(), .boxed(),
) )
.with_child( .with_child(if details.is_editing {
ChildView::new(editor.clone())
.contained()
.with_margin_left(theme.entry.default.icon_spacing)
.aligned()
.left()
.flex(1.0, true)
.boxed()
} else {
Label::new(details.filename, style.text.clone()) Label::new(details.filename, style.text.clone())
.contained() .contained()
.with_margin_left(style.icon_spacing) .with_margin_left(style.icon_spacing)
.aligned() .aligned()
.left() .left()
.boxed(), .boxed()
) })
.constrained() .constrained()
.with_height(theme.entry.default.height) .with_height(theme.entry.default.height)
.contained() .contained()
.with_style(style.container) .with_style(row_container_style)
.with_padding_left(padding) .with_padding_left(padding)
.boxed() .boxed()
}) })
@ -871,168 +872,43 @@ mod tests {
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
assert_eq!( assert_eq!(
visible_entry_details(&panel, 0..50, cx), visible_entries_as_strings(&panel, 0..50, cx),
&[ &[
EntryDetails { "v root1",
filename: "root1".to_string(), " > a",
depth: 0, " > b",
kind: EntryKind::Dir, " > C",
is_expanded: true, " .dockerignore",
is_selected: false, "v root2",
}, " > d",
EntryDetails { " > e",
filename: "a".to_string(), ]
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "b".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "C".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: ".dockerignore".to_string(),
depth: 1,
kind: EntryKind::File,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "root2".to_string(),
depth: 0,
kind: EntryKind::Dir,
is_expanded: true,
is_selected: false
},
EntryDetails {
filename: "d".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false
},
EntryDetails {
filename: "e".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false
},
],
); );
toggle_expand_dir(&panel, "root1/b", cx); toggle_expand_dir(&panel, "root1/b", cx);
assert_eq!( assert_eq!(
visible_entry_details(&panel, 0..50, cx), visible_entries_as_strings(&panel, 0..50, cx),
&[ &[
EntryDetails { "v root1",
filename: "root1".to_string(), " > a",
depth: 0, " v b <== selected",
kind: EntryKind::Dir, " > 3",
is_expanded: true, " > 4",
is_selected: false, " > C",
}, " .dockerignore",
EntryDetails { "v root2",
filename: "a".to_string(), " > d",
depth: 1, " > e",
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "b".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: true,
is_selected: true,
},
EntryDetails {
filename: "3".to_string(),
depth: 2,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "4".to_string(),
depth: 2,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "C".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: ".dockerignore".to_string(),
depth: 1,
kind: EntryKind::File,
is_expanded: false,
is_selected: false,
},
EntryDetails {
filename: "root2".to_string(),
depth: 0,
kind: EntryKind::Dir,
is_expanded: true,
is_selected: false
},
EntryDetails {
filename: "d".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false
},
EntryDetails {
filename: "e".to_string(),
depth: 1,
kind: EntryKind::Dir,
is_expanded: false,
is_selected: false
},
] ]
); );
assert_eq!( assert_eq!(
visible_entry_details(&panel, 5..8, cx), visible_entries_as_strings(&panel, 5..8, cx),
[ &[
EntryDetails { //
filename: "C".to_string(), " > C",
depth: 1, " .dockerignore",
kind: EntryKind::Dir, "v root2",
is_expanded: false,
is_selected: false
},
EntryDetails {
filename: ".dockerignore".to_string(),
depth: 1,
kind: EntryKind::File,
is_expanded: false,
is_selected: false
},
EntryDetails {
filename: "root2".to_string(),
depth: 0,
kind: EntryKind::Dir,
is_expanded: true,
is_selected: false
},
] ]
); );
} }
@ -1109,7 +985,7 @@ mod tests {
" > a", " > a",
" > b", " > b",
" > C", " > C",
" [NEW FILE EDITOR]", " [EDITOR: '']",
" .dockerignore", " .dockerignore",
"v root2", "v root2",
" > d", " > d",
@ -1151,7 +1027,7 @@ mod tests {
" v b <== selected", " v b <== selected",
" > 3", " > 3",
" > 4", " > 4",
" [NEW FILE EDITOR]", " [EDITOR: '']",
" > C", " > C",
" .dockerignore", " .dockerignore",
" the-new-filename", " the-new-filename",
@ -1192,7 +1068,7 @@ mod tests {
" v b", " v b",
" > 3", " > 3",
" > 4", " > 4",
" [RENAME EDITOR] <== selected", " [EDITOR: 'another-filename'] <== selected",
" > C", " > C",
" .dockerignore", " .dockerignore",
" the-new-filename", " the-new-filename",
@ -1265,19 +1141,17 @@ mod tests {
}); });
} }
fn visible_entry_details( fn visible_entries_as_strings(
panel: &ViewHandle<ProjectPanel>, panel: &ViewHandle<ProjectPanel>,
range: Range<usize>, range: Range<usize>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Vec<EntryDetails> { ) -> Vec<String> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut project_entries = HashSet::new(); let mut project_entries = HashSet::new();
let mut has_editor = false; let mut has_editor = false;
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.for_each_visible_entry(range, cx, |project_entry, details, _| { panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
if details.kind == EntryKind::NewFileEditor if details.is_editing {
|| details.kind == EntryKind::FileRenameEditor
{
assert!(!has_editor, "duplicate editor entry"); assert!(!has_editor, "duplicate editor entry");
has_editor = true; has_editor = true;
} else { } else {
@ -1288,21 +1162,7 @@ mod tests {
details details
); );
} }
result.push(details)
});
});
result
}
fn visible_entries_as_strings(
panel: &ViewHandle<ProjectPanel>,
range: Range<usize>,
cx: &mut TestAppContext,
) -> Vec<String> {
visible_entry_details(panel, range, cx)
.into_iter()
.map(|details| {
let indent = " ".repeat(details.depth); let indent = " ".repeat(details.depth);
let icon = if details.kind == EntryKind::Dir { let icon = if details.kind == EntryKind::Dir {
if details.is_expanded { if details.is_expanded {
@ -1313,10 +1173,9 @@ mod tests {
} else { } else {
" " " "
}; };
let name = if details.kind == EntryKind::FileRenameEditor { let editor_text = format!("[EDITOR: '{}']", details.filename);
"[RENAME EDITOR]" let name = if details.is_editing {
} else if details.kind == EntryKind::NewFileEditor { &editor_text
"[NEW FILE EDITOR]"
} else { } else {
&details.filename &details.filename
}; };
@ -1325,8 +1184,10 @@ mod tests {
} else { } else {
"" ""
}; };
format!("{indent}{icon}{name}{selected}") result.push(format!("{indent}{icon}{name}{selected}"));
}) });
.collect() });
result
} }
} }

View file

@ -204,11 +204,12 @@ pub struct ChatPanel {
pub hovered_sign_in_prompt: TextStyle, pub hovered_sign_in_prompt: TextStyle,
} }
#[derive(Debug, Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct ProjectPanel { pub struct ProjectPanel {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>, pub entry: Interactive<ProjectPanelEntry>,
pub filename_editor: FieldEditor,
pub indent_width: f32, pub indent_width: f32,
} }

View file

@ -1,6 +1,6 @@
import Theme from "../themes/theme"; import Theme from "../themes/theme";
import { panel } from "./app"; import { panel } from "./app";
import { backgroundColor, iconColor, text } from "./components"; import { backgroundColor, iconColor, player, text } from "./components";
export default function projectPanel(theme: Theme) { export default function projectPanel(theme: Theme) {
return { return {
@ -26,5 +26,10 @@ export default function projectPanel(theme: Theme) {
text: text(theme, "mono", "active", { size: "sm" }), text: text(theme, "mono", "active", { size: "sm" }),
} }
}, },
filenameEditor: {
background: backgroundColor(theme, 500, "active"),
text: text(theme, "mono", "primary", { size: "sm" }),
selection: player(theme, 1).selection,
},
}; };
} }