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::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
ViewContext, VisualContext as _, WeakView, WindowContext,
Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
@ -30,7 +29,7 @@ use std::{
sync::Arc,
};
use theme::ActiveTheme as _;
use ui::{h_stack, v_stack, IconElement, Label};
use ui::{v_stack, IconElement, Label, ListItem};
use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@ -1335,13 +1334,19 @@ impl ProjectPanel {
}
}
fn render_entry_visual_element(
details: &EntryDetails,
editor: Option<&View<Editor>>,
padding: Pixels,
fn render_entry(
&self,
entry_id: ProjectEntryId,
details: EntryDetails,
// dragged_entry_destination: &mut Option<Arc<Path>>,
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 is_selected = self
.selection
.map_or(false, |selection| selection.entry_id == entry_id);
let theme = cx.theme();
let filename_text_color = details
@ -1354,14 +1359,17 @@ impl ProjectPanel {
})
.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 {
div().child(IconElement::from_path(icon.to_string()))
} else {
div()
})
.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())
} else {
div()
@ -1370,33 +1378,6 @@ impl ProjectPanel {
}
.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| {
if !show_editor {
if kind.is_dir() {
@ -1410,12 +1391,9 @@ impl ProjectPanel {
}
}
}))
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, event: &MouseDownEvent, cx| {
this.deploy_context_menu(event.position, entry_id, cx);
}),
)
.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
this.deploy_context_menu(event.position, entry_id, cx);
}))
// .on_drop::<ProjectEntryId>(|this, event, cx| {
// this.move_entry(
// *dragged_entry,

View file

@ -1,8 +1,10 @@
use std::rc::Rc;
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 std::rc::Rc;
use crate::{
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
@ -117,66 +119,6 @@ impl ListHeader {
self.meta = meta;
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)]
@ -238,12 +180,14 @@ pub struct ListItem {
selected: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: u32,
indent_level: usize,
indent_step_size: Pixels,
left_slot: Option<GraphicSlot>,
overflow: OverflowStyle,
toggle: Toggle,
variant: ListItemVariant,
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]>,
}
@ -254,11 +198,13 @@ impl ListItem {
disabled: false,
selected: false,
indent_level: 0,
indent_step_size: px(12.),
left_slot: None,
overflow: OverflowStyle::Hidden,
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
on_click: Default::default(),
on_click: None,
on_secondary_mouse_down: None,
children: SmallVec::new(),
}
}
@ -268,16 +214,29 @@ impl ListItem {
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 {
self.variant = variant;
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
}
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 {
self.toggle = toggle;
self
@ -328,14 +287,6 @@ impl RenderOnce for ListItem {
style.background = Some(cx.theme().colors().editor_background.into());
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
// .when(self.state == InteractionState::Focused, |this| {
// this.border()
@ -346,30 +297,45 @@ impl RenderOnce for ListItem {
.when(self.selected, |this| {
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(
div()
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
// .ml(rems(0.75 * self.indent_level as f32))
.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)),
)
}))
.ml(self.indent_level as f32 * self.indent_step_size)
.flex()
.gap_1()
.items_center()
.relative()
.child(disclosure_control(self.toggle))
.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!");
}),
)
.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!");
},
),
)
}
}