Use ListItems in the project panel (#3421)

This PR reworks the project panel to render its items using the
`ListItem` component.

There are a few hacks in here in order to get click handlers working for
the `ListItem`, but we'll want to get these fixed in GPUI.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2023-11-28 13:11:43 -05:00 committed by GitHub
parent 4a01726e5e
commit 9411898720
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 132 deletions

View file

@ -10,9 +10,8 @@ use anyhow::{anyhow, Result};
use gpui::{ use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
ViewContext, VisualContext as _, WeakView, WindowContext,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
use project::{ use project::{
@ -30,7 +29,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use theme::ActiveTheme as _; use theme::ActiveTheme as _;
use ui::{h_stack, v_stack, IconElement, Label}; use ui::{v_stack, IconElement, Label, ListItem};
use unicase::UniCase; use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
@ -1335,13 +1334,19 @@ impl ProjectPanel {
} }
} }
fn render_entry_visual_element( fn render_entry(
details: &EntryDetails, &self,
editor: Option<&View<Editor>>, entry_id: ProjectEntryId,
padding: Pixels, details: EntryDetails,
// dragged_entry_destination: &mut Option<Arc<Path>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Div { ) -> ListItem {
let kind = details.kind;
let settings = ProjectPanelSettings::get_global(cx);
let show_editor = details.is_editing && !details.is_processing; let show_editor = details.is_editing && !details.is_processing;
let is_selected = self
.selection
.map_or(false, |selection| selection.entry_id == entry_id);
let theme = cx.theme(); let theme = cx.theme();
let filename_text_color = details let filename_text_color = details
@ -1354,14 +1359,17 @@ impl ProjectPanel {
}) })
.unwrap_or(theme.status().info); .unwrap_or(theme.status().info);
h_stack() ListItem::new(entry_id.to_proto() as usize)
.indent_level(details.depth)
.indent_step_size(px(settings.indent_size))
.selected(is_selected)
.child(if let Some(icon) = &details.icon { .child(if let Some(icon) = &details.icon {
div().child(IconElement::from_path(icon.to_string())) div().child(IconElement::from_path(icon.to_string()))
} else { } else {
div() div()
}) })
.child( .child(
if let (Some(editor), true) = (editor, show_editor) { if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
div().w_full().child(editor.clone()) div().w_full().child(editor.clone())
} else { } else {
div() div()
@ -1370,33 +1378,6 @@ impl ProjectPanel {
} }
.ml_1(), .ml_1(),
) )
.pl(padding)
}
fn render_entry(
&self,
entry_id: ProjectEntryId,
details: EntryDetails,
// dragged_entry_destination: &mut Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
) -> Stateful<Div> {
let kind = details.kind;
let settings = ProjectPanelSettings::get_global(cx);
const INDENT_SIZE: Pixels = px(16.0);
let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
let show_editor = details.is_editing && !details.is_processing;
let is_selected = self
.selection
.map_or(false, |selection| selection.entry_id == entry_id);
Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
.id(entry_id.to_proto() as usize)
.w_full()
.cursor_pointer()
.when(is_selected, |this| {
this.bg(cx.theme().colors().element_selected)
})
.hover(|style| style.bg(cx.theme().colors().element_hover))
.on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
if !show_editor { if !show_editor {
if kind.is_dir() { if kind.is_dir() {
@ -1410,12 +1391,9 @@ impl ProjectPanel {
} }
} }
})) }))
.on_mouse_down( .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
MouseButton::Right, this.deploy_context_menu(event.position, entry_id, cx);
cx.listener(move |this, event: &MouseDownEvent, cx| { }))
this.deploy_context_menu(event.position, entry_id, cx);
}),
)
// .on_drop::<ProjectEntryId>(|this, event, cx| { // .on_drop::<ProjectEntryId>(|this, event, cx| {
// this.move_entry( // this.move_entry(
// *dragged_entry, // *dragged_entry,

View file

@ -1,8 +1,10 @@
use std::rc::Rc;
use gpui::{ use gpui::{
div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement, div, px, AnyElement, ClickEvent, Div, IntoElement, MouseButton, MouseDownEvent, Pixels,
Stateful, StatefulInteractiveElement,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::rc::Rc;
use crate::{ use crate::{
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle, disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
@ -117,66 +119,6 @@ impl ListHeader {
self.meta = meta; self.meta = meta;
self self
} }
// before_ship!("delete")
// fn render<V: 'static>(self, cx: &mut WindowContext) -> impl Element<V> {
// let disclosure_control = disclosure_control(self.toggle);
// let meta = match self.meta {
// Some(ListHeaderMeta::Tools(icons)) => div().child(
// h_stack()
// .gap_2()
// .items_center()
// .children(icons.into_iter().map(|i| {
// IconElement::new(i)
// .color(TextColor::Muted)
// .size(IconSize::Small)
// })),
// ),
// Some(ListHeaderMeta::Button(label)) => div().child(label),
// Some(ListHeaderMeta::Text(label)) => div().child(label),
// None => div(),
// };
// h_stack()
// .w_full()
// .bg(cx.theme().colors().surface_background)
// // TODO: Add focus state
// // .when(self.state == InteractionState::Focused, |this| {
// // this.border()
// // .border_color(cx.theme().colors().border_focused)
// // })
// .relative()
// .child(
// div()
// .h_5()
// .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
// .flex()
// .flex_1()
// .items_center()
// .justify_between()
// .w_full()
// .gap_1()
// .child(
// h_stack()
// .gap_1()
// .child(
// div()
// .flex()
// .gap_1()
// .items_center()
// .children(self.left_icon.map(|i| {
// IconElement::new(i)
// .color(TextColor::Muted)
// .size(IconSize::Small)
// }))
// .child(Label::new(self.label.clone()).color(TextColor::Muted)),
// )
// .child(disclosure_control),
// )
// .child(meta),
// )
// }
} }
#[derive(IntoElement, Clone)] #[derive(IntoElement, Clone)]
@ -238,12 +180,14 @@ pub struct ListItem {
selected: bool, selected: bool,
// TODO: Reintroduce this // TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility, // disclosure_control_style: DisclosureControlVisibility,
indent_level: u32, indent_level: usize,
indent_step_size: Pixels,
left_slot: Option<GraphicSlot>, left_slot: Option<GraphicSlot>,
overflow: OverflowStyle, overflow: OverflowStyle,
toggle: Toggle, toggle: Toggle,
variant: ListItemVariant, variant: ListItemVariant,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
} }
@ -254,11 +198,13 @@ impl ListItem {
disabled: false, disabled: false,
selected: false, selected: false,
indent_level: 0, indent_level: 0,
indent_step_size: px(12.),
left_slot: None, left_slot: None,
overflow: OverflowStyle::Hidden, overflow: OverflowStyle::Hidden,
toggle: Toggle::NotToggleable, toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(), variant: ListItemVariant::default(),
on_click: Default::default(), on_click: None,
on_secondary_mouse_down: None,
children: SmallVec::new(), children: SmallVec::new(),
} }
} }
@ -268,16 +214,29 @@ impl ListItem {
self self
} }
pub fn on_secondary_mouse_down(
mut self,
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.on_secondary_mouse_down = Some(Rc::new(handler));
self
}
pub fn variant(mut self, variant: ListItemVariant) -> Self { pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant; self.variant = variant;
self self
} }
pub fn indent_level(mut self, indent_level: u32) -> Self { pub fn indent_level(mut self, indent_level: usize) -> Self {
self.indent_level = indent_level; self.indent_level = indent_level;
self self
} }
pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
self.indent_step_size = indent_step_size;
self
}
pub fn toggle(mut self, toggle: Toggle) -> Self { pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle; self.toggle = toggle;
self self
@ -328,14 +287,6 @@ impl RenderOnce for ListItem {
style.background = Some(cx.theme().colors().editor_background.into()); style.background = Some(cx.theme().colors().editor_background.into());
style style
}) })
.on_click({
let on_click = self.on_click.clone();
move |event, cx| {
if let Some(on_click) = &on_click {
(on_click)(event, cx)
}
}
})
// TODO: Add focus state // TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| { // .when(self.state == InteractionState::Focused, |this| {
// this.border() // this.border()
@ -346,30 +297,45 @@ impl RenderOnce for ListItem {
.when(self.selected, |this| { .when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected) this.bg(cx.theme().colors().ghost_element_selected)
}) })
.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(move |event, cx| {
// HACK: GPUI currently fires `on_click` with any mouse button,
// but we only care about the left button.
if event.down.button == MouseButton::Left {
(on_click)(event, cx)
}
})
})
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(MouseButton::Right, move |event, cx| {
(on_mouse_down)(event, cx)
})
})
.child( .child(
div() div()
.when(self.variant == ListItemVariant::Inset, |this| this.px_2()) .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
// .ml(rems(0.75 * self.indent_level as f32)) .ml(self.indent_level as f32 * self.indent_step_size)
.children((0..self.indent_level).map(|_| {
div()
.w(px(4.))
.h_full()
.flex()
.justify_center()
.group_hover("", |style| style.bg(cx.theme().colors().border_focused))
.child(
h_stack()
.child(div().w_px().h_full())
.child(div().w_px().h_full().bg(cx.theme().colors().border)),
)
}))
.flex() .flex()
.gap_1() .gap_1()
.items_center() .items_center()
.relative() .relative()
.child(disclosure_control(self.toggle)) .child(disclosure_control(self.toggle))
.children(left_content) .children(left_content)
.children(self.children), .children(self.children)
// HACK: We need to attach the `on_click` handler to the child element in order to have the click
// event actually fire.
// Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
// outer `div`.
.id("on_click_hack")
.when_some(self.on_click, |this, on_click| {
this.on_click(move |event, cx| {
// HACK: GPUI currently fires `on_click` with any mouse button,
// but we only care about the left button.
if event.down.button == MouseButton::Left {
(on_click)(event, cx)
}
})
}),
) )
} }
} }

View file

@ -22,5 +22,13 @@ impl Render for ListItemStory {
println!("Clicked!"); println!("Clicked!");
}), }),
) )
.child(Story::label("With `on_secondary_mouse_down`"))
.child(
ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
|_event, _cx| {
println!("Right mouse down!");
},
),
)
} }
} }