Add `ProjectPanel` component

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs               |   1 
crates/storybook2/src/stories/components/project_panel.rs |  30 +
crates/storybook2/src/stories/elements.rs                 |   1 
crates/storybook2/src/stories/elements/input.rs           |  26 +
crates/storybook2/src/story_selector.rs                   |   4 
crates/ui2/src/components.rs                              |   2 
crates/ui2/src/components/project_panel.rs                |  58 +++
crates/ui2/src/elements.rs                                |   2 
crates/ui2/src/elements/input.rs                          | 110 ++++++
crates/ui2/src/static_data.rs                             | 146 ++++++++
10 files changed, 379 insertions(+), 1 deletion(-)

Detailed changes

crates/storybook2/src/stories/components/project_panel.rs 🔗

@@ -0,0 +1,30 @@
+use std::marker::PhantomData;
+
+use ui::prelude::*;
+use ui::{Panel, ProjectPanel};
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct ProjectPanelStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ProjectPanelStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ProjectPanel<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+                Box::new(()),
+            ))
+    }
+}

crates/storybook2/src/stories/elements/input.rs 🔗

@@ -0,0 +1,26 @@
+use std::marker::PhantomData;
+
+use ui::prelude::*;
+use ui::Input;
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct InputStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> InputStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Input<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(div().flex().child(Input::new("Search")))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -14,6 +14,7 @@ use ui::prelude::*;
 pub enum ElementStory {
     Avatar,
     Icon,
+    Input,
     Label,
 }
 
@@ -24,6 +25,7 @@ impl ElementStory {
         match self {
             Self::Avatar => elements::avatar::AvatarStory::new().into_any(),
             Self::Icon => elements::icon::IconStory::new().into_any(),
+            Self::Input => elements::input::InputStory::new().into_any(),
             Self::Label => elements::label::LabelStory::new().into_any(),
         }
     }
@@ -35,6 +37,7 @@ pub enum ComponentStory {
     AssistantPanel,
     Buffer,
     Panel,
+    ProjectPanel,
 }
 
 impl ComponentStory {
@@ -47,6 +50,7 @@ impl ComponentStory {
             }
             Self::Buffer => components::buffer::BufferStory::new().into_any(),
             Self::Panel => components::panel::PanelStory::new().into_any(),
+            Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(),
         }
     }
 }

crates/ui2/src/components.rs 🔗

@@ -3,9 +3,11 @@ mod buffer;
 mod icon_button;
 mod list;
 mod panel;
+mod project_panel;
 
 pub use assistant_panel::*;
 pub use buffer::*;
 pub use icon_button::*;
 pub use list::*;
 pub use panel::*;
+pub use project_panel::*;

crates/ui2/src/components/project_panel.rs 🔗

@@ -0,0 +1,58 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{
+    static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
+    ListHeader,
+};
+
+#[derive(Element)]
+pub struct ProjectPanel<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+    scroll_state: ScrollState,
+}
+
+impl<S: 'static + Send + Sync + Clone> ProjectPanel<S> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            state_type: PhantomData,
+            scroll_state,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_col()
+            .w_full()
+            .h_full()
+            .px_2()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_56()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(ScrollState::default())
+                    .child(
+                        List::new(static_project_panel_single_items())
+                            .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled))
+                            .empty_message("No files in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    )
+                    .child(
+                        List::new(static_project_panel_project_items())
+                            .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled))
+                            .empty_message("No folders in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    ),
+            )
+            .child(
+                Input::new("Find something...")
+                    .value("buffe".to_string())
+                    .state(InteractionState::Focused),
+            )
+    }
+}

crates/ui2/src/elements.rs 🔗

@@ -1,11 +1,13 @@
 mod avatar;
 mod button;
 mod icon;
+mod input;
 mod label;
 mod stack;
 
 pub use avatar::*;
 pub use button::*;
 pub use icon::*;
+pub use input::*;
 pub use label::*;
 pub use stack::*;

crates/ui2/src/elements/input.rs 🔗

@@ -0,0 +1,110 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Default, PartialEq)]
+pub enum InputVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+#[derive(Element)]
+pub struct Input<S: 'static + Send + Sync> {
+    state_type: PhantomData<S>,
+    placeholder: &'static str,
+    value: String,
+    state: InteractionState,
+    variant: InputVariant,
+}
+
+impl<S: 'static + Send + Sync> Input<S> {
+    pub fn new(placeholder: &'static str) -> Self {
+        Self {
+            state_type: PhantomData,
+            placeholder,
+            value: "".to_string(),
+            state: InteractionState::default(),
+            variant: InputVariant::default(),
+        }
+    }
+
+    pub fn value(mut self, value: String) -> Self {
+        self.value = value;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn variant(mut self, variant: InputVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        let text_el;
+        let text_color;
+        let background_color_default;
+        let background_color_active;
+
+        let mut border_color_default = theme.middle.base.default.border;
+        let mut border_color_hover = theme.middle.base.hovered.border;
+        let mut border_color_active = theme.middle.base.pressed.border;
+        let border_color_focus = theme.middle.base.pressed.background;
+
+        match self.variant {
+            InputVariant::Ghost => {
+                background_color_default = theme.middle.base.default.background;
+                background_color_active = theme.middle.base.active.background;
+            }
+            InputVariant::Filled => {
+                background_color_default = theme.middle.on.default.background;
+                background_color_active = theme.middle.on.active.background;
+            }
+        };
+
+        if self.state == InteractionState::Focused {
+            border_color_default = theme.players[0].cursor;
+            border_color_hover = theme.players[0].cursor;
+            border_color_active = theme.players[0].cursor;
+        }
+
+        if self.state == InteractionState::Focused || self.state == InteractionState::Active {
+            text_el = self.value.clone();
+            text_color = theme.lowest.base.default.foreground;
+        } else {
+            text_el = self.placeholder.to_string().clone();
+            text_color = theme.lowest.base.disabled.foreground;
+        }
+
+        div()
+            .h_7()
+            .w_full()
+            .px_2()
+            .border()
+            .border_color(border_color_default)
+            .fill(background_color_default)
+            // .hover()
+            // .border_color(border_color_hover)
+            // .active()
+            // .border_color(border_color_active)
+            .fill(background_color_active)
+            .flex()
+            .items_center()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .text_sm()
+                    .text_color(text_color)
+                    .child(text_el)
+                    .child(div().text_color(theme.players[0].cursor).child("|")),
+            )
+    }
+}

crates/ui2/src/static_data.rs 🔗

@@ -1,8 +1,152 @@
 use crate::{
     Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText,
-    Theme,
+    Icon, Label, LabelColor, ListEntry, ListItem, Theme, ToggleState,
 };
 
+pub fn static_project_panel_project_items<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(0)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".config"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("assets"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("crates"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("activity_indicator"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("ai"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("audio"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("auto_update"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("breadcrumbs"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("call"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::NotToggled),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("src"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(3)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("derive_element.rs"))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(4),
+        ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("docs").color(LabelColor::Default))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("src").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(3)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("ui").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(4)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("component").color(LabelColor::Created))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(5)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".dockerignore"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Cargo.lock"))
+            .left_icon(Icon::FileLock.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Cargo.toml"))
+            .left_icon(Icon::FileToml.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Dockerfile"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Procfile"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(1),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_project_panel_single_items<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
+    vec![
+        ListEntry::new(Label::new("todo.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(0),
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(0),
+        ListEntry::new(Label::new("config.json"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(0),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
 pub fn empty_buffer_example<S: 'static + Send + Sync + Clone>() -> Buffer<S> {
     Buffer::new().set_rows(Some(BufferRows::default()))
 }