diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 0b7061581518a16d49c677de10dd62ece65b24b4..d43dc29fe7d23249e4fbee40533afaffe312c91d 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -38,6 +38,7 @@ mod tab; mod tab_bar; mod toggle; mod tooltip; +mod tree_view_item; #[cfg(feature = "stories")] mod stories; @@ -82,6 +83,7 @@ pub use tab::*; pub use tab_bar::*; pub use toggle::*; pub use tooltip::*; +pub use tree_view_item::*; #[cfg(feature = "stories")] pub use stories::*; diff --git a/crates/ui/src/components/tree_view_item.rs b/crates/ui/src/components/tree_view_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..307fe1496b21913c5620ed7c871dd9679d422a41 --- /dev/null +++ b/crates/ui/src/components/tree_view_item.rs @@ -0,0 +1,293 @@ +use std::sync::Arc; + +use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent}; + +use crate::{Disclosure, prelude::*}; + +#[derive(IntoElement, RegisterComponent)] +pub struct TreeViewItem { + id: ElementId, + group_name: Option, + label: SharedString, + toggle: bool, + selected: bool, + disabled: bool, + focused: bool, + default_expanded: bool, + root_item: bool, + tooltip: Option AnyView + 'static>>, + on_click: Option>, + on_hover: Option>, + on_toggle: Option>, + on_secondary_mouse_down: Option>, +} + +impl TreeViewItem { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + id: id.into(), + group_name: None, + label: label.into(), + toggle: false, + selected: false, + disabled: false, + focused: false, + default_expanded: false, + root_item: false, + tooltip: None, + on_click: None, + on_hover: None, + on_toggle: None, + on_secondary_mouse_down: None, + } + } + + pub fn group_name(mut self, group_name: impl Into) -> Self { + self.group_name = Some(group_name.into()); + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } + + pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_hover = Some(Box::new(handler)); + self + } + + pub fn on_secondary_mouse_down( + mut self, + handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_secondary_mouse_down = Some(Box::new(handler)); + self + } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Box::new(tooltip)); + self + } + + pub fn toggle(mut self, toggle: bool) -> Self { + self.toggle = toggle; + self + } + + pub fn default_expanded(mut self, default_expanded: bool) -> Self { + self.default_expanded = default_expanded; + self + } + + pub fn on_toggle( + mut self, + on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_toggle = Some(Arc::new(on_toggle)); + self + } + + pub fn root_item(mut self, root_item: bool) -> Self { + self.root_item = root_item; + self + } + + pub fn focused(mut self, focused: bool) -> Self { + self.focused = focused; + self + } +} + +impl Disableable for TreeViewItem { + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Toggleable for TreeViewItem { + fn toggle_state(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + +impl RenderOnce for TreeViewItem { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let selected_bg = cx.theme().colors().element_active.opacity(0.5); + let selected_border = cx.theme().colors().border.opacity(0.6); + let focused_border = cx.theme().colors().border_focused; + let transparent_border = cx.theme().colors().border_transparent; + + let indentation_line = h_flex().size_7().flex_none().justify_center().child( + div() + .w_px() + .h_full() + .bg(cx.theme().colors().border.opacity(0.5)), + ); + + h_flex() + .id(self.id) + .when_some(self.group_name, |this, group| this.group(group)) + .w_full() + .child( + h_flex() + .id("inner_tree_view_item") + .group("tree_view_item") + .size_full() + .relative() + .map(|this| { + let label = self.label; + if self.root_item { + this.px_1() + .mb_1() + .gap_2p5() + .rounded_sm() + .border_1() + .map(|this| { + if self.focused && self.selected { + this.border_color(focused_border).bg(selected_bg) + } else if self.focused { + this.border_color(focused_border) + } else if self.selected { + this.border_color(selected_border).bg(selected_bg) + } else { + this.border_color(transparent_border) + } + }) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .child( + Disclosure::new("toggle", self.toggle) + .when_some( + self.on_toggle.clone(), + |disclosure, on_toggle| disclosure.on_toggle(on_toggle), + ) + .opened_icon(IconName::ChevronDown) + .closed_icon(IconName::ChevronRight), + ) + .child( + Label::new(label) + .when(!self.selected, |this| this.color(Color::Muted)), + ) + } else { + this.child(indentation_line).child( + h_flex() + .w_full() + .flex_grow() + .px_1() + .rounded_sm() + .border_1() + .map(|this| { + if self.focused && self.selected { + this.border_color(focused_border).bg(selected_bg) + } else if self.focused { + this.border_color(focused_border) + } else if self.selected { + this.border_color(selected_border).bg(selected_bg) + } else { + this.border_color(transparent_border) + } + }) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .child( + Label::new(label) + .when(!self.selected, |this| this.color(Color::Muted)), + ), + ) + } + }) + .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover)) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| this.cursor_pointer().on_click(on_click), + ) + .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { + this.on_mouse_down(MouseButton::Right, move |event, window, cx| { + (on_mouse_down)(event, window, cx) + }) + }) + .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)), + ) + } +} + +impl Component for TreeViewItem { + fn scope() -> ComponentScope { + ComponentScope::Navigation + } + + fn description() -> Option<&'static str> { + Some( + "A hierarchical list of items that may have a parent-child relationship where children can be toggled into view by expanding or collapsing their parent item.", + ) + } + + fn preview(_window: &mut Window, cx: &mut App) -> Option { + let container = || { + v_flex() + .p_2() + .w_64() + .border_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().panel_background) + }; + + Some( + example_group(vec![ + single_example( + "Basic Tree View", + container() + .child( + TreeViewItem::new("index-1", "Tree Item Root #1") + .root_item(true) + .toggle_state(true), + ) + .child(TreeViewItem::new("index-2", "Tree Item #2")) + .child(TreeViewItem::new("index-3", "Tree Item #3")) + .child(TreeViewItem::new("index-4", "Tree Item Root #2").root_item(true)) + .child(TreeViewItem::new("index-5", "Tree Item #5")) + .child(TreeViewItem::new("index-6", "Tree Item #6")) + .into_any_element(), + ), + single_example( + "Active Child", + container() + .child(TreeViewItem::new("index-1", "Tree Item Root #1").root_item(true)) + .child(TreeViewItem::new("index-2", "Tree Item #2").toggle_state(true)) + .child(TreeViewItem::new("index-3", "Tree Item #3")) + .into_any_element(), + ), + single_example( + "Focused Parent", + container() + .child( + TreeViewItem::new("index-1", "Tree Item Root #1") + .root_item(true) + .focused(true) + .toggle_state(true), + ) + .child(TreeViewItem::new("index-2", "Tree Item #2")) + .child(TreeViewItem::new("index-3", "Tree Item #3")) + .into_any_element(), + ), + single_example( + "Focused Child", + container() + .child( + TreeViewItem::new("index-1", "Tree Item Root #1") + .root_item(true) + .toggle_state(true), + ) + .child(TreeViewItem::new("index-2", "Tree Item #2").focused(true)) + .child(TreeViewItem::new("index-3", "Tree Item #3")) + .into_any_element(), + ), + ]) + .into_any_element(), + ) + } +}