Use ListItem
s 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:
parent
4a01726e5e
commit
9411898720
3 changed files with 84 additions and 132 deletions
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue