From 94118987206bbfb5da747034faaa5839dff0e51a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 13:11:43 -0500 Subject: [PATCH] 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 --- crates/project_panel2/src/project_panel.rs | 66 +++----- crates/ui2/src/components/list.rs | 142 +++++++----------- .../ui2/src/components/stories/list_item.rs | 8 + 3 files changed, 84 insertions(+), 132 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index b027209870..d4c63e75bf 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -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>, - padding: Pixels, + fn render_entry( + &self, + entry_id: ProjectEntryId, + details: EntryDetails, + // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> 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>, - cx: &mut ViewContext, - ) -> Stateful
{ - 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::(|this, event, cx| { // this.move_entry( // *dragged_entry, diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 642903f09b..6683c9a0cf 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -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(self, cx: &mut WindowContext) -> impl Element { - // 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, overflow: OverflowStyle, toggle: Toggle, variant: ListItemVariant, on_click: Option>, + on_secondary_mouse_down: Option>, 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) + } + }) + }), ) } } diff --git a/crates/ui2/src/components/stories/list_item.rs b/crates/ui2/src/components/stories/list_item.rs index ee1b4d0be6..ec0da7f07e 100644 --- a/crates/ui2/src/components/stories/list_item.rs +++ b/crates/ui2/src/components/stories/list_item.rs @@ -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!"); + }, + ), + ) } }