Make list_item `toggleable`, improve optional `left_icon` on list item

Nate Butler and Julia created

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>

Change summary

crates/storybook/src/prelude.rs                 |  6 ++
crates/storybook/src/ui/component/list_item.rs  | 30 ++++++++----
crates/storybook/src/ui/element/icon.rs         | 46 +++++++++++++-----
crates/storybook/src/ui/module/project_panel.rs | 21 ++++++--
4 files changed, 73 insertions(+), 30 deletions(-)

Detailed changes

crates/storybook/src/prelude.rs 🔗

@@ -47,3 +47,9 @@ pub enum SelectedState {
     PartiallySelected,
     Selected,
 }
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ToggleState {
+    Toggled,
+    NotToggled,
+}

crates/storybook/src/ui/component/list_item.rs 🔗

@@ -1,4 +1,4 @@
-use crate::prelude::InteractionState;
+use crate::prelude::{InteractionState, ToggleState};
 use crate::theme::theme;
 use crate::ui::{icon, IconAsset, Label};
 use gpui2::geometry::rems;
@@ -12,6 +12,7 @@ pub struct ListItem {
     left_icon: Option<IconAsset>,
     indent_level: f32,
     state: InteractionState,
+    toggle: Option<ToggleState>,
 }
 
 pub fn list_item(label: Label) -> ListItem {
@@ -20,6 +21,7 @@ pub fn list_item(label: Label) -> ListItem {
         indent_level: 0.0,
         left_icon: None,
         state: InteractionState::default(),
+        toggle: None,
     }
 }
 
@@ -28,10 +30,17 @@ impl ListItem {
         self.indent_level = indent_level;
         self
     }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggle = Some(toggle);
+        self
+    }
+
     pub fn left_icon(mut self, left_icon: Option<IconAsset>) -> Self {
         self.left_icon = left_icon;
         self
     }
+
     pub fn state(mut self, state: InteractionState) -> Self {
         self.state = state;
         self
@@ -40,7 +49,7 @@ impl ListItem {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        let mut el = div()
+        div()
             .fill(theme.middle.base.default.background)
             .hover()
             .fill(theme.middle.base.hovered.background)
@@ -53,13 +62,14 @@ impl ListItem {
                     .ml(rems(0.75 * self.indent_level.clone()))
                     .flex()
                     .gap_2()
-                    .items_center(),
-            );
-
-        if self.left_icon.is_some() {
-            el = el.child(icon(self.left_icon.clone().unwrap()))
-        }
-
-        el.child(self.label.clone())
+                    .items_center()
+                    .children(match self.toggle {
+                        Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
+                        Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
+                        None => None,
+                    })
+                    .children(self.left_icon.map(|i| icon(i)))
+                    .child(self.label.clone()),
+            )
     }
 }

crates/storybook/src/ui/element/icon.rs 🔗

@@ -1,11 +1,13 @@
-use std::borrow::Cow;
-
 use crate::theme::theme;
 use gpui2::elements::svg;
 use gpui2::style::StyleHelpers;
 use gpui2::IntoElement;
 use gpui2::{Element, ViewContext};
 
+// Icon::Hash
+// icon(IconAsset::Hash).color(IconColor::Warning)
+// Icon::new(IconAsset::Hash).color(IconColor::Warning)
+
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconAsset {
     Ai,
@@ -18,19 +20,29 @@ pub enum IconAsset {
     File,
     Folder,
     FolderOpen,
+    ChevronDown,
+    ChevronUp,
+    ChevronLeft,
+    ChevronRight,
 }
 
-pub fn icon_asset(asset: IconAsset) -> impl Into<Cow<'static, str>> {
-    match asset {
-        IconAsset::Ai => "icons/ai.svg",
-        IconAsset::ArrowLeft => "icons/arrow_left.svg",
-        IconAsset::ArrowRight => "icons/arrow_right.svg",
-        IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
-        IconAsset::Bolt => "icons/bolt.svg",
-        IconAsset::Hash => "icons/hash.svg",
-        IconAsset::File => "icons/file_icons/file.svg",
-        IconAsset::Folder => "icons/file_icons/folder.svg",
-        IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
+impl IconAsset {
+    pub fn path(self) -> &'static str {
+        match self {
+            IconAsset::Ai => "icons/ai.svg",
+            IconAsset::ArrowLeft => "icons/arrow_left.svg",
+            IconAsset::ArrowRight => "icons/arrow_right.svg",
+            IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
+            IconAsset::Bolt => "icons/bolt.svg",
+            IconAsset::Hash => "icons/hash.svg",
+            IconAsset::ChevronDown => "icons/chevron_down.svg",
+            IconAsset::ChevronUp => "icons/chevron_up.svg",
+            IconAsset::ChevronLeft => "icons/chevron_left.svg",
+            IconAsset::ChevronRight => "icons/chevron_right.svg",
+            IconAsset::File => "icons/file_icons/file.svg",
+            IconAsset::Folder => "icons/file_icons/folder.svg",
+            IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
+        }
     }
 }
 
@@ -43,12 +55,18 @@ pub fn icon(asset: IconAsset) -> Icon {
     Icon { asset }
 }
 
+// impl Icon {
+//     pub fn new(asset: IconAsset) -> Icon {
+//         Icon { asset }
+//     }
+// }
+
 impl Icon {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
         svg()
-            .path(icon_asset(self.asset))
+            .path(self.asset.path())
             .size_4()
             .fill(theme.lowest.base.default.foreground)
     }

crates/storybook/src/ui/module/project_panel.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    prelude::InteractionState,
+    prelude::{InteractionState, ToggleState},
     theme::theme,
     ui::{input, label, list_item, IconAsset, LabelColor},
 };
@@ -44,21 +44,30 @@ impl<V: 'static> ProjectPanel<V> {
                         div().flex().flex_col().children(
                             std::iter::repeat_with(|| {
                                 vec![
+                                    list_item(label("sqlez").color(LabelColor::Modified))
+                                        .left_icon(IconAsset::FolderOpen.into())
+                                        .indent_level(0.0)
+                                        .set_toggle(ToggleState::NotToggled),
                                     list_item(label("storybook").color(LabelColor::Modified))
                                         .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(0.0),
+                                        .indent_level(0.0)
+                                        .set_toggle(ToggleState::Toggled),
                                     list_item(label("docs").color(LabelColor::Default))
                                         .left_icon(IconAsset::Folder.into())
-                                        .indent_level(1.0),
+                                        .indent_level(1.0)
+                                        .set_toggle(ToggleState::Toggled),
                                     list_item(label("src").color(LabelColor::Modified))
                                         .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(2.0),
+                                        .indent_level(2.0)
+                                        .set_toggle(ToggleState::Toggled),
                                     list_item(label("ui").color(LabelColor::Modified))
                                         .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(3.0),
+                                        .indent_level(3.0)
+                                        .set_toggle(ToggleState::Toggled),
                                     list_item(label("component").color(LabelColor::Created))
                                         .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(4.0),
+                                        .indent_level(4.0)
+                                        .set_toggle(ToggleState::Toggled),
                                     list_item(label("facepile.rs").color(LabelColor::Default))
                                         .left_icon(IconAsset::File.into())
                                         .indent_level(5.0),