Add an initial set of GPUI2 components to the storybook (#2990)

Nate Butler created

This PR adds an initial set of components to `crates/storybook/src/ui`.

All changes still are contained to inside storybook. Merging to keep up
to date with main.

Change summary

assets/icons/stop_sharing.svg                     |   2 
crates/gpui/src/geometry.rs                       |  53 +
crates/gpui2/src/element.rs                       |  21 
crates/gpui2/src/elements/pressable.rs            |  11 
crates/gpui2/src/style.rs                         |  29 -
crates/gpui2_macros/src/styleable_helpers.rs      | 126 ++++
crates/storybook/src/components.rs                |   6 
crates/storybook/src/modules.rs                   |   3 
crates/storybook/src/prelude.rs                   |  55 ++
crates/storybook/src/storybook.rs                 |   3 
crates/storybook/src/ui.rs                        |  23 
crates/storybook/src/ui/component.rs              |   4 
crates/storybook/src/ui/component/facepile.rs     |  27 +
crates/storybook/src/ui/component/follow_group.rs |  52 ++
crates/storybook/src/ui/component/list_item.rs    |  88 +++
crates/storybook/src/ui/component/tab.rs          |   2 
crates/storybook/src/ui/element.rs                |   9 
crates/storybook/src/ui/element/avatar.rs         |  42 +
crates/storybook/src/ui/element/details.rs        |  36 +
crates/storybook/src/ui/element/icon.rs           |  73 +++
crates/storybook/src/ui/element/icon_button.rs    |  44 +
crates/storybook/src/ui/element/indicator.rs      |  32 +
crates/storybook/src/ui/element/input.rs          |  99 ++++
crates/storybook/src/ui/element/label.rs          |  49 ++
crates/storybook/src/ui/element/text_button.rs    |  81 +++
crates/storybook/src/ui/element/tool_divider.rs   |  19 
crates/storybook/src/ui/module.rs                 |   5 
crates/storybook/src/ui/module/chat_panel.rs      |  65 ++
crates/storybook/src/ui/module/project_panel.rs   |  97 ++++
crates/storybook/src/ui/module/status_bar.rs      | 146 ++++++
crates/storybook/src/ui/module/tab_bar.rs         |  23 
crates/storybook/src/ui/module/title_bar.rs       | 117 ++++
crates/storybook/src/ui/tracker.md                | 133 +++++
crates/storybook/src/workspace.rs                 | 408 ----------------
docs/ui/states.md                                 |  43 +
35 files changed, 1,527 insertions(+), 499 deletions(-)

Detailed changes

assets/icons/stop_sharing.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.70312 4L7.26046 2.97339C7.10239 2.60679 6.74141 2.36933 6.34219 2.36933H2.5C2.22386 2.36933 2 2.59319 2 2.86933V4.375V8" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>

crates/gpui/src/geometry.rs 🔗

@@ -1,12 +1,12 @@
-use std::fmt::Debug;
-
 use super::scene::{Path, PathVertex};
 use crate::{color::Color, json::ToJson};
+use derive_more::Neg;
 pub use pathfinder_geometry::*;
 use rect::RectF;
 use refineable::Refineable;
 use serde::{Deserialize, Deserializer};
 use serde_json::json;
+use std::fmt::Debug;
 use vector::{vec2f, Vector2F};
 
 pub struct PathBuilder {
@@ -194,8 +194,8 @@ where
 impl Size<DefiniteLength> {
     pub fn zero() -> Self {
         Self {
-            width: pixels(0.),
-            height: pixels(0.),
+            width: pixels(0.).into(),
+            height: pixels(0.).into(),
         }
     }
 
@@ -235,6 +235,17 @@ pub struct Edges<T: Clone + Default + Debug> {
     pub left: T,
 }
 
+impl<T: Clone + Default + Debug> Edges<T> {
+    pub fn uniform(value: T) -> Self {
+        Self {
+            top: value.clone(),
+            right: value.clone(),
+            bottom: value.clone(),
+            left: value.clone(),
+        }
+    }
+}
+
 impl Edges<Length> {
     pub fn auto() -> Self {
         Self {
@@ -247,10 +258,10 @@ impl Edges<Length> {
 
     pub fn zero() -> Self {
         Self {
-            top: pixels(0.),
-            right: pixels(0.),
-            bottom: pixels(0.),
-            left: pixels(0.),
+            top: pixels(0.).into(),
+            right: pixels(0.).into(),
+            bottom: pixels(0.).into(),
+            left: pixels(0.).into(),
         }
     }
 
@@ -270,10 +281,10 @@ impl Edges<Length> {
 impl Edges<DefiniteLength> {
     pub fn zero() -> Self {
         Self {
-            top: pixels(0.),
-            right: pixels(0.),
-            bottom: pixels(0.),
-            left: pixels(0.),
+            top: pixels(0.).into(),
+            right: pixels(0.).into(),
+            bottom: pixels(0.).into(),
+            left: pixels(0.).into(),
         }
     }
 
@@ -322,7 +333,7 @@ impl Edges<f32> {
     }
 }
 
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
 pub enum AbsoluteLength {
     Pixels(f32),
     Rems(f32),
@@ -360,7 +371,7 @@ impl Default for AbsoluteLength {
 }
 
 /// A non-auto length that can be defined in pixels, rems, or percent of parent.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
 pub enum DefiniteLength {
     Absolute(AbsoluteLength),
     Relative(f32), // 0. to 1.
@@ -404,7 +415,7 @@ impl Default for DefiniteLength {
 }
 
 /// A length that can be defined in pixels, rems, percent of parent, or auto.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
 pub enum Length {
     Definite(DefiniteLength),
     Auto,
@@ -419,16 +430,16 @@ impl std::fmt::Debug for Length {
     }
 }
 
-pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
-    DefiniteLength::Relative(fraction).into()
+pub fn relative(fraction: f32) -> DefiniteLength {
+    DefiniteLength::Relative(fraction)
 }
 
-pub fn rems<T: From<AbsoluteLength>>(rems: f32) -> T {
-    AbsoluteLength::Rems(rems).into()
+pub fn rems(rems: f32) -> AbsoluteLength {
+    AbsoluteLength::Rems(rems)
 }
 
-pub fn pixels<T: From<AbsoluteLength>>(pixels: f32) -> T {
-    AbsoluteLength::Pixels(pixels).into()
+pub fn pixels(pixels: f32) -> AbsoluteLength {
+    AbsoluteLength::Pixels(pixels)
 }
 
 pub fn auto() -> Length {

crates/gpui2/src/element.rs 🔗

@@ -34,6 +34,27 @@ pub trait Element<V: 'static>: 'static + IntoElement<V> {
             phase: ElementPhase::Init,
         }))
     }
+
+    /// Applies a given function `then` to the current element if `condition` is true.
+    /// This function is used to conditionally modify the element based on a given condition.
+    /// If `condition` is false, it just returns the current element as it is.
+    ///
+    /// # Parameters
+    /// - `self`: The current element
+    /// - `condition`: The boolean condition based on which `then` is applied to the element.
+    /// - `then`: A function that takes in the current element and returns a possibly modified element.
+    ///
+    /// # Return
+    /// It returns the potentially modified element.
+    fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+    where
+        Self: Sized,
+    {
+        if condition {
+            self = then(self);
+        }
+        self
+    }
 }
 
 /// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.

crates/gpui2/src/elements/pressable.rs 🔗

@@ -73,10 +73,15 @@ impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
                 if bounds.contains_point(event.position) {
                     pressed.set(true);
                     cx.repaint();
+                } else {
+                    cx.bubble_event();
                 }
-            } else if pressed.get() {
-                pressed.set(false);
-                cx.repaint();
+            } else {
+                if pressed.get() {
+                    pressed.set(false);
+                    cx.repaint();
+                }
+                cx.bubble_event();
             }
         });
 

crates/gpui2/src/style.rs 🔗

@@ -314,6 +314,8 @@ pub trait Styleable {
     }
 }
 
+use crate as gpui2;
+
 // Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
 //
 // Example:
@@ -322,33 +324,12 @@ pub trait Styleable {
 pub trait StyleHelpers: Styleable<Style = Style> {
     styleable_helpers!();
 
-    fn h(mut self, height: Length) -> Self
-    where
-        Self: Sized,
-    {
-        self.declared_style().size.height = Some(height);
-        self
-    }
-
-    /// size_{n}: Sets width & height to {n}
-    ///
-    /// Example:
-    /// size_1: Sets width & height to 1
-    fn size(mut self, size: Length) -> Self
-    where
-        Self: Sized,
-    {
-        self.declared_style().size.height = Some(size);
-        self.declared_style().size.width = Some(size);
-        self
-    }
-
     fn full(mut self) -> Self
     where
         Self: Sized,
     {
-        self.declared_style().size.width = Some(relative(1.));
-        self.declared_style().size.height = Some(relative(1.));
+        self.declared_style().size.width = Some(relative(1.).into());
+        self.declared_style().size.height = Some(relative(1.).into());
         self
     }
 
@@ -406,7 +387,7 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     {
         self.declared_style().flex_grow = Some(1.);
         self.declared_style().flex_shrink = Some(1.);
-        self.declared_style().flex_basis = Some(relative(0.));
+        self.declared_style().flex_basis = Some(relative(0.).into());
         self
     }
 

crates/gpui2_macros/src/styleable_helpers.rs 🔗

@@ -28,49 +28,100 @@ fn generate_methods() -> Vec<TokenStream2> {
     let mut methods = Vec::new();
 
     for (prefix, auto_allowed, fields) in box_prefixes() {
+        methods.push(generate_custom_value_setter(
+            prefix,
+            if auto_allowed {
+                quote! { Length }
+            } else {
+                quote! { DefiniteLength }
+            },
+            &fields,
+        ));
+
         for (suffix, length_tokens, doc_string) in box_suffixes() {
-            if auto_allowed || suffix != "auto" {
-                let method = generate_method(prefix, suffix, &fields, length_tokens, doc_string);
-                methods.push(method);
+            if suffix != "auto" || auto_allowed {
+                methods.push(generate_predefined_setter(
+                    prefix,
+                    suffix,
+                    &fields,
+                    &length_tokens,
+                    false,
+                    doc_string,
+                ));
+            }
+
+            if suffix != "auto" {
+                methods.push(generate_predefined_setter(
+                    prefix,
+                    suffix,
+                    &fields,
+                    &length_tokens,
+                    true,
+                    doc_string,
+                ));
             }
         }
     }
 
     for (prefix, fields) in corner_prefixes() {
+        methods.push(generate_custom_value_setter(
+            prefix,
+            quote! { AbsoluteLength },
+            &fields,
+        ));
+
         for (suffix, radius_tokens, doc_string) in corner_suffixes() {
-            let method = generate_method(prefix, suffix, &fields, radius_tokens, doc_string);
-            methods.push(method);
+            methods.push(generate_predefined_setter(
+                prefix,
+                suffix,
+                &fields,
+                &radius_tokens,
+                false,
+                doc_string,
+            ));
         }
     }
 
     for (prefix, fields) in border_prefixes() {
         for (suffix, width_tokens, doc_string) in border_suffixes() {
-            let method = generate_method(prefix, suffix, &fields, width_tokens, doc_string);
-            methods.push(method);
+            methods.push(generate_predefined_setter(
+                prefix,
+                suffix,
+                &fields,
+                &width_tokens,
+                false,
+                doc_string,
+            ));
         }
     }
-
     methods
 }
 
-fn generate_method(
-    prefix: &'static str,
-    suffix: &'static str,
+fn generate_predefined_setter(
+    name: &'static str,
+    length: &'static str,
     fields: &Vec<TokenStream2>,
-    length_tokens: TokenStream2,
+    length_tokens: &TokenStream2,
+    negate: bool,
     doc_string: &'static str,
 ) -> TokenStream2 {
-    let method_name = if suffix.is_empty() {
-        format_ident!("{}", prefix)
+    let (negation_prefix, negation_token) = if negate {
+        ("neg_", quote! { - })
+    } else {
+        ("", quote! {})
+    };
+
+    let method_name = if length.is_empty() {
+        format_ident!("{}{}", negation_prefix, name)
     } else {
-        format_ident!("{}_{}", prefix, suffix)
+        format_ident!("{}{}_{}", negation_prefix, name, length)
     };
 
     let field_assignments = fields
         .iter()
         .map(|field_tokens| {
             quote! {
-                style.#field_tokens = Some(gpui::geometry::#length_tokens);
+                style.#field_tokens = Some((#negation_token gpui2::geometry::#length_tokens).into());
             }
         })
         .collect::<Vec<_>>();
@@ -84,6 +135,41 @@ fn generate_method(
         }
     };
 
+    if negate {
+        dbg!(method.to_string());
+    }
+
+    method
+}
+
+fn generate_custom_value_setter(
+    prefix: &'static str,
+    length_type: TokenStream2,
+    fields: &Vec<TokenStream2>,
+) -> TokenStream2 {
+    let method_name = format_ident!("{}", prefix);
+
+    let mut iter = fields.into_iter();
+    let last = iter.next_back().unwrap();
+    let field_assignments = iter
+        .map(|field_tokens| {
+            quote! {
+                style.#field_tokens = Some(length.clone().into());
+            }
+        })
+        .chain(std::iter::once(quote! {
+            style.#last = Some(length.into());
+        }))
+        .collect::<Vec<_>>();
+
+    let method = quote! {
+        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui2::geometry::#length_type>) -> Self where Self: std::marker::Sized {
+            let mut style = self.declared_style();
+            #(#field_assignments)*
+            self
+        }
+    };
+
     method
 }
 
@@ -96,10 +182,10 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
             true,
             vec![quote! {size.width}, quote! {size.height}],
         ),
-        ("min_w", false, vec![quote! { min_size.width }]),
-        ("min_h", false, vec![quote! { min_size.height }]),
-        ("max_w", false, vec![quote! { max_size.width }]),
-        ("max_h", false, vec![quote! { max_size.height }]),
+        ("min_w", true, vec![quote! { min_size.width }]),
+        ("min_h", true, vec![quote! { min_size.height }]),
+        ("max_w", true, vec![quote! { max_size.width }]),
+        ("max_h", true, vec![quote! { max_size.height }]),
         (
             "m",
             true,

crates/storybook/src/components.rs 🔗

@@ -4,12 +4,6 @@ use gpui2::{
 };
 use std::{marker::PhantomData, rc::Rc};
 
-mod icon_button;
-mod tab;
-
-pub(crate) use icon_button::{icon_button, ButtonVariant};
-pub(crate) use tab::tab;
-
 struct ButtonHandlers<V, D> {
     click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
 }

crates/storybook/src/prelude.rs 🔗

@@ -0,0 +1,55 @@
+#[derive(Default, PartialEq)]
+pub enum ButtonVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+#[derive(Default, PartialEq)]
+pub enum InputVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum Shape {
+    #[default]
+    Circle,
+    RoundedRectangle,
+}
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum InteractionState {
+    #[default]
+    Enabled,
+    Hovered,
+    Active,
+    Focused,
+    Dragged,
+    Disabled,
+}
+
+impl InteractionState {
+    pub fn if_enabled(&self, enabled: bool) -> Self {
+        if enabled {
+            *self
+        } else {
+            InteractionState::Disabled
+        }
+    }
+}
+
+#[derive(Default, PartialEq)]
+pub enum SelectedState {
+    #[default]
+    Unselected,
+    PartiallySelected,
+    Selected,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ToggleState {
+    Toggled,
+    NotToggled,
+}

crates/storybook/src/storybook.rs 🔗

@@ -12,8 +12,9 @@ use simplelog::SimpleLogger;
 mod collab_panel;
 mod components;
 mod element_ext;
-mod modules;
+mod prelude;
 mod theme;
+mod ui;
 mod workspace;
 
 gpui2::actions! {

crates/storybook/src/ui.rs 🔗

@@ -0,0 +1,23 @@
+mod element;
+pub use element::avatar::*;
+pub use element::details::*;
+pub use element::icon::*;
+pub use element::icon_button::*;
+pub use element::indicator::*;
+pub use element::input::*;
+pub use element::label::*;
+pub use element::text_button::*;
+pub use element::tool_divider::*;
+
+mod component;
+pub use component::facepile::*;
+pub use component::follow_group::*;
+pub use component::list_item::*;
+pub use component::tab::*;
+
+mod module;
+pub use module::chat_panel::*;
+pub use module::project_panel::*;
+pub use module::status_bar::*;
+pub use module::tab_bar::*;
+pub use module::title_bar::*;

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

@@ -0,0 +1,27 @@
+use crate::{theme::theme, ui::Avatar};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct Facepile {
+    players: Vec<Avatar>,
+}
+
+pub fn facepile(players: Vec<Avatar>) -> Facepile {
+    Facepile { players }
+}
+
+impl Facepile {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_count = self.players.len();
+        let player_list = self.players.iter().enumerate().map(|(ix, player)| {
+            let isnt_last = ix < player_count - 1;
+            div()
+                .when(isnt_last, |div| div.neg_mr_1())
+                .child(player.clone())
+        });
+        div().p_1().flex().items_center().children(player_list)
+    }
+}

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

@@ -0,0 +1,52 @@
+use crate::theme::theme;
+use crate::ui::{facepile, indicator, Avatar};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct FollowGroup {
+    player: usize,
+    players: Vec<Avatar>,
+}
+
+pub fn follow_group(players: Vec<Avatar>) -> FollowGroup {
+    FollowGroup { player: 0, players }
+}
+
+impl FollowGroup {
+    pub fn player(mut self, player: usize) -> Self {
+        self.player = player;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_bg = theme.players[self.player].selection;
+
+        div()
+            .h_full()
+            .flex()
+            .flex_col()
+            .gap_px()
+            .justify_center()
+            .child(
+                div()
+                    .flex()
+                    .justify_center()
+                    .w_full()
+                    .child(indicator().player(self.player)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .h_6()
+                    .px_1()
+                    .rounded_lg()
+                    .fill(player_bg)
+                    .child(facepile(self.players.clone())),
+            )
+    }
+}

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

@@ -0,0 +1,88 @@
+use crate::prelude::{InteractionState, ToggleState};
+use crate::theme::theme;
+use crate::ui::{icon, IconAsset, Label};
+use gpui2::geometry::rems;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct ListItem {
+    label: Label,
+    left_icon: Option<IconAsset>,
+    indent_level: u32,
+    state: InteractionState,
+    toggle: Option<ToggleState>,
+}
+
+pub fn list_item(label: Label) -> ListItem {
+    ListItem {
+        label,
+        indent_level: 0,
+        left_icon: None,
+        state: InteractionState::default(),
+        toggle: None,
+    }
+}
+
+impl ListItem {
+    pub fn indent_level(mut self, indent_level: u32) -> Self {
+        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
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .fill(theme.middle.base.default.background)
+            .hover()
+            .fill(theme.middle.base.hovered.background)
+            .active()
+            .fill(theme.middle.base.pressed.background)
+            .relative()
+            .child(
+                div()
+                    .h_7()
+                    .px_2()
+                    // .ml(rems(0.75 * self.indent_level as f32))
+                    .children((0..self.indent_level).map(|_| {
+                        div().w(rems(0.75)).h_full().flex().justify_center().child(
+                            div()
+                                .w_px()
+                                .h_full()
+                                .fill(theme.middle.base.default.border)
+                                .hover()
+                                .fill(theme.middle.warning.default.border)
+                                .active()
+                                .fill(theme.middle.negative.default.border),
+                        )
+                    }))
+                    .flex()
+                    .gap_2()
+                    .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/components/tab.rs → crates/storybook/src/ui/component/tab.rs 🔗

@@ -4,7 +4,7 @@ use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
 #[derive(Element)]
-pub(crate) struct Tab {
+pub struct Tab {
     title: &'static str,
     enabled: bool,
 }

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

@@ -0,0 +1,9 @@
+pub(crate) mod avatar;
+pub(crate) mod details;
+pub(crate) mod icon;
+pub(crate) mod icon_button;
+pub(crate) mod indicator;
+pub(crate) mod input;
+pub(crate) mod label;
+pub(crate) mod text_button;
+pub(crate) mod tool_divider;

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

@@ -0,0 +1,42 @@
+use crate::prelude::Shape;
+use crate::theme::theme;
+use gpui2::elements::img;
+use gpui2::style::StyleHelpers;
+use gpui2::{ArcCow, IntoElement};
+use gpui2::{Element, ViewContext};
+
+#[derive(Element, Clone)]
+pub struct Avatar {
+    src: ArcCow<'static, str>,
+    shape: Shape,
+}
+
+pub fn avatar(src: impl Into<ArcCow<'static, str>>) -> Avatar {
+    Avatar {
+        src: src.into(),
+        shape: Shape::Circle,
+    }
+}
+
+impl Avatar {
+    pub fn shape(mut self, shape: Shape) -> Self {
+        self.shape = shape;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let mut img = img();
+
+        if self.shape == Shape::Circle {
+            img = img.rounded_full();
+        } else {
+            img = img.rounded_md();
+        }
+
+        img.uri(self.src.clone())
+            .size_4()
+            .fill(theme.middle.warning.default.foreground)
+    }
+}

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

@@ -0,0 +1,36 @@
+use crate::theme::theme;
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, ViewContext};
+use gpui2::{IntoElement, ParentElement};
+
+#[derive(Element, Clone)]
+pub struct Details {
+    text: &'static str,
+    meta: Option<&'static str>,
+}
+
+pub fn details(text: &'static str) -> Details {
+    Details { text, meta: None }
+}
+
+impl Details {
+    pub fn meta_text(mut self, meta: &'static str) -> Self {
+        self.meta = Some(meta);
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            // .flex()
+            // .w_full()
+            .p_1()
+            .gap_0p5()
+            .text_xs()
+            .text_color(theme.lowest.base.default.foreground)
+            .child(self.text.clone())
+            .children(self.meta.map(|m| m))
+    }
+}

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

@@ -0,0 +1,73 @@
+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,
+    ArrowLeft,
+    ArrowRight,
+    #[default]
+    ArrowUpRight,
+    Bolt,
+    Hash,
+    File,
+    Folder,
+    FolderOpen,
+    ChevronDown,
+    ChevronUp,
+    ChevronLeft,
+    ChevronRight,
+}
+
+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",
+        }
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct Icon {
+    asset: IconAsset,
+}
+
+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(self.asset.path())
+            .size_4()
+            .fill(theme.lowest.base.default.foreground)
+    }
+}

crates/storybook/src/components/icon_button.rs → crates/storybook/src/ui/element/icon_button.rs 🔗

@@ -1,3 +1,4 @@
+use crate::prelude::{ButtonVariant, InteractionState};
 use crate::theme::theme;
 use gpui2::elements::svg;
 use gpui2::style::{StyleHelpers, Styleable};
@@ -5,25 +6,42 @@ use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
 #[derive(Element)]
-pub(crate) struct IconButton {
+pub struct IconButton {
     path: &'static str,
     variant: ButtonVariant,
+    state: InteractionState,
 }
 
-#[derive(PartialEq)]
-pub enum ButtonVariant {
-    Ghost,
-    Filled,
-}
-
-pub fn icon_button<V: 'static>(path: &'static str, variant: ButtonVariant) -> impl Element<V> {
-    IconButton { path, variant }
+pub fn icon_button(path: &'static str) -> IconButton {
+    IconButton {
+        path,
+        variant: ButtonVariant::default(),
+        state: InteractionState::default(),
+    }
 }
 
 impl IconButton {
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
+        let icon_color;
+
+        if self.state == InteractionState::Disabled {
+            icon_color = theme.highest.base.disabled.foreground;
+        } else {
+            icon_color = theme.highest.base.default.foreground;
+        }
+
         let mut div = div();
         if self.variant == ButtonVariant::Filled {
             div = div.fill(theme.highest.on.default.background);
@@ -39,12 +57,6 @@ impl IconButton {
             .fill(theme.highest.base.hovered.background)
             .active()
             .fill(theme.highest.base.pressed.background)
-            .child(
-                svg()
-                    .path(self.path)
-                    .w_4()
-                    .h_4()
-                    .fill(theme.highest.variant.default.foreground),
-            )
+            .child(svg().path(self.path).w_4().h_4().fill(icon_color))
     }
 }

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

@@ -0,0 +1,32 @@
+use crate::theme::theme;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ViewContext};
+
+#[derive(Element)]
+pub struct Indicator {
+    player: usize,
+}
+
+pub fn indicator() -> Indicator {
+    Indicator { player: 0 }
+}
+
+impl Indicator {
+    pub fn player(mut self, player: usize) -> Self {
+        self.player = player;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_color = theme.players[self.player].cursor;
+
+        div()
+            .w_4()
+            .h_1()
+            .rounded_bl_sm()
+            .rounded_br_sm()
+            .fill(player_color)
+    }
+}

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

@@ -0,0 +1,99 @@
+use crate::prelude::{InputVariant, InteractionState};
+use crate::theme::theme;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct Input {
+    placeholder: &'static str,
+    value: String,
+    state: InteractionState,
+    variant: InputVariant,
+}
+
+pub fn input(placeholder: &'static str) -> Input {
+    Input {
+        placeholder,
+        value: "".to_string(),
+        state: InteractionState::default(),
+        variant: InputVariant::default(),
+    }
+}
+
+impl Input {
+    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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        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()
+            .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/storybook/src/ui/element/label.rs 🔗

@@ -0,0 +1,49 @@
+use crate::theme::theme;
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, ViewContext};
+use gpui2::{IntoElement, ParentElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum LabelColor {
+    #[default]
+    Default,
+    Created,
+    Modified,
+    Deleted,
+    Hidden,
+}
+
+#[derive(Element, Clone)]
+pub struct Label {
+    label: &'static str,
+    color: LabelColor,
+}
+
+pub fn label(label: &'static str) -> Label {
+    Label {
+        label,
+        color: LabelColor::Default,
+    }
+}
+
+impl Label {
+    pub fn color(mut self, color: LabelColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let color = match self.color {
+            LabelColor::Default => theme.lowest.base.default.foreground,
+            LabelColor::Created => theme.lowest.positive.default.foreground,
+            LabelColor::Modified => theme.lowest.warning.default.foreground,
+            LabelColor::Deleted => theme.lowest.negative.default.foreground,
+            LabelColor::Hidden => theme.lowest.variant.default.foreground,
+        };
+
+        div().text_sm().text_color(color).child(self.label.clone())
+    }
+}

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

@@ -0,0 +1,81 @@
+use crate::prelude::{ButtonVariant, InteractionState};
+use crate::theme::theme;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct TextButton {
+    label: &'static str,
+    variant: ButtonVariant,
+    state: InteractionState,
+}
+
+pub fn text_button(label: &'static str) -> TextButton {
+    TextButton {
+        label,
+        variant: ButtonVariant::default(),
+        state: InteractionState::default(),
+    }
+}
+
+impl TextButton {
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let text_color_default;
+        let text_color_hover;
+        let text_color_active;
+
+        let background_color_default;
+        let background_color_hover;
+        let background_color_active;
+
+        let div = div();
+
+        match self.variant {
+            ButtonVariant::Ghost => {
+                text_color_default = theme.lowest.base.default.foreground;
+                text_color_hover = theme.lowest.base.hovered.foreground;
+                text_color_active = theme.lowest.base.pressed.foreground;
+                background_color_default = theme.lowest.base.default.background;
+                background_color_hover = theme.lowest.base.hovered.background;
+                background_color_active = theme.lowest.base.pressed.background;
+            }
+            ButtonVariant::Filled => {
+                text_color_default = theme.lowest.base.default.foreground;
+                text_color_hover = theme.lowest.base.hovered.foreground;
+                text_color_active = theme.lowest.base.pressed.foreground;
+                background_color_default = theme.lowest.on.default.background;
+                background_color_hover = theme.lowest.on.hovered.background;
+                background_color_active = theme.lowest.on.pressed.background;
+            }
+        };
+        div.h_6()
+            .px_1()
+            .flex()
+            .items_center()
+            .justify_center()
+            .rounded_md()
+            .text_xs()
+            .text_color(text_color_default)
+            .fill(background_color_default)
+            .hover()
+            .text_color(text_color_hover)
+            .fill(background_color_hover)
+            .active()
+            .text_color(text_color_active)
+            .fill(background_color_active)
+            .child(self.label.clone())
+    }
+}

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

@@ -0,0 +1,19 @@
+use crate::theme::theme;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ViewContext};
+
+#[derive(Element)]
+pub struct ToolDivider {}
+
+pub fn tool_divider<V: 'static>() -> impl Element<V> {
+    ToolDivider {}
+}
+
+impl ToolDivider {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div().w_px().h_3().fill(theme.lowest.base.default.border)
+    }
+}

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

@@ -0,0 +1,5 @@
+pub(crate) mod chat_panel;
+pub(crate) mod project_panel;
+pub(crate) mod status_bar;
+pub(crate) mod tab_bar;
+pub(crate) mod title_bar;

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

@@ -0,0 +1,65 @@
+use std::marker::PhantomData;
+
+use crate::theme::theme;
+use crate::ui::icon_button;
+use gpui2::elements::div::ScrollState;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct ChatPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+pub fn chat_panel<V: 'static>(scroll_state: ScrollState) -> ChatPanel<V> {
+    ChatPanel {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> ChatPanel<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .h_full()
+            .flex()
+            // Header
+            .child(
+                div()
+                    .px_2()
+                    .flex()
+                    .gap_2()
+                    // Nav Buttons
+                    .child("#gpui2"),
+            )
+            // Chat Body
+            .child(
+                div()
+                    .w_full()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .child("body"),
+            )
+            // Composer
+            .child(
+                div()
+                    .px_2()
+                    .flex()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(icon_button("icons/plus.svg"))
+                            .child(icon_button("icons/split.svg")),
+                    ),
+            )
+    }
+}

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

@@ -0,0 +1,97 @@
+use crate::{
+    prelude::{InteractionState, ToggleState},
+    theme::theme,
+    ui::{details, input, label, list_item, IconAsset, LabelColor},
+};
+use gpui2::{
+    elements::{div, div::ScrollState},
+    style::StyleHelpers,
+    ParentElement, ViewContext,
+};
+use gpui2::{Element, IntoElement};
+use std::marker::PhantomData;
+
+#[derive(Element)]
+pub struct ProjectPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
+    ProjectPanel {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> ProjectPanel<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .w_56()
+            .h_full()
+            .flex()
+            .flex_col()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_56()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .child(details("This is a long string that should wrap when it keeps going for a long time.").meta_text("6 h ago)"))
+                    .child(
+                        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)
+                                        .set_toggle(ToggleState::NotToggled),
+                                    list_item(label("storybook").color(LabelColor::Modified))
+                                        .left_icon(IconAsset::FolderOpen.into())
+                                        .indent_level(0)
+                                        .set_toggle(ToggleState::Toggled),
+                                    list_item(label("docs").color(LabelColor::Default))
+                                        .left_icon(IconAsset::Folder.into())
+                                        .indent_level(1)
+                                        .set_toggle(ToggleState::Toggled),
+                                    list_item(label("src").color(LabelColor::Modified))
+                                        .left_icon(IconAsset::FolderOpen.into())
+                                        .indent_level(2)
+                                        .set_toggle(ToggleState::Toggled),
+                                    list_item(label("ui").color(LabelColor::Modified))
+                                        .left_icon(IconAsset::FolderOpen.into())
+                                        .indent_level(3)
+                                        .set_toggle(ToggleState::Toggled),
+                                    list_item(label("component").color(LabelColor::Created))
+                                        .left_icon(IconAsset::FolderOpen.into())
+                                        .indent_level(4)
+                                        .set_toggle(ToggleState::Toggled),
+                                    list_item(label("facepile.rs").color(LabelColor::Default))
+                                        .left_icon(IconAsset::File.into())
+                                        .indent_level(5),
+                                    list_item(label("follow_group.rs").color(LabelColor::Default))
+                                        .left_icon(IconAsset::File.into())
+                                        .indent_level(5),
+                                    list_item(label("list_item.rs").color(LabelColor::Created))
+                                        .left_icon(IconAsset::File.into())
+                                        .indent_level(5),
+                                    list_item(label("tab.rs").color(LabelColor::Default))
+                                        .left_icon(IconAsset::File.into())
+                                        .indent_level(5),
+                                ]
+                            })
+                            .take(10)
+                            .flatten(),
+                        ),
+                    ),
+            )
+            .child(
+                input("Find something...")
+                    .value("buffe".to_string())
+                    .state(InteractionState::Focused),
+            )
+    }
+}

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

@@ -0,0 +1,146 @@
+use std::marker::PhantomData;
+
+use crate::theme::{theme, Theme};
+use crate::ui::{icon_button, text_button, tool_divider};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Default, PartialEq)]
+pub enum Tool {
+    #[default]
+    ProjectPanel,
+    CollaborationPanel,
+    Terminal,
+    Assistant,
+    Feedback,
+    Diagnostics,
+}
+
+struct ToolGroup {
+    active_index: Option<usize>,
+    tools: Vec<Tool>,
+}
+
+impl Default for ToolGroup {
+    fn default() -> Self {
+        ToolGroup {
+            active_index: None,
+            tools: vec![],
+        }
+    }
+}
+
+#[derive(Element)]
+pub struct StatusBar<V: 'static> {
+    view_type: PhantomData<V>,
+    left_tools: Option<ToolGroup>,
+    right_tools: Option<ToolGroup>,
+    bottom_tools: Option<ToolGroup>,
+}
+
+pub fn status_bar<V: 'static>() -> StatusBar<V> {
+    StatusBar {
+        view_type: PhantomData,
+        left_tools: None,
+        right_tools: None,
+        bottom_tools: None,
+    }
+}
+
+impl<V: 'static> StatusBar<V> {
+    pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.left_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.right_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.bottom_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .py_0p5()
+            .px_1()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .fill(theme.lowest.base.default.background)
+            .child(self.left_tools(theme))
+            .child(self.right_tools(theme))
+    }
+
+    fn left_tools(&self, theme: &Theme) -> impl Element<V> {
+        div()
+            .flex()
+            .items_center()
+            .gap_1()
+            .child(icon_button("icons/project.svg"))
+            .child(icon_button("icons/hash.svg"))
+            .child(tool_divider())
+            .child(icon_button("icons/error.svg"))
+    }
+    fn right_tools(&self, theme: &Theme) -> impl Element<V> {
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(text_button("116:25"))
+                    .child(text_button("Rust")),
+            )
+            .child(tool_divider())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(icon_button("icons/copilot.svg"))
+                    .child(icon_button("icons/feedback.svg")),
+            )
+            .child(tool_divider())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(icon_button("icons/terminal.svg"))
+                    .child(icon_button("icons/conversations.svg"))
+                    .child(icon_button("icons/ai.svg")),
+            )
+    }
+}

crates/storybook/src/modules/tab_bar.rs → crates/storybook/src/ui/module/tab_bar.rs 🔗

@@ -1,7 +1,8 @@
 use std::marker::PhantomData;
 
-use crate::components::{icon_button, tab, ButtonVariant};
+use crate::prelude::InteractionState;
 use crate::theme::theme;
+use crate::ui::{icon_button, tab};
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
@@ -23,7 +24,8 @@ pub fn tab_bar<V: 'static>(scroll_state: ScrollState) -> TabBar<V> {
 impl<V: 'static> TabBar<V> {
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
-
+        let can_navigate_back = true;
+        let can_navigate_forward = false;
         div()
             .w_full()
             .flex()
@@ -40,15 +42,22 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled))
-                            .child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)),
+                            .child(
+                                icon_button("icons/arrow_left.svg")
+                                    .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
+                            )
+                            .child(
+                                icon_button("icons/arrow_right.svg").state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                ),
+                            ),
                     ),
             )
             .child(
                 div().w_0().flex_1().h_full().child(
                     div()
                         .flex()
-                        .gap_px()
+                        .gap_1()
                         .overflow_x_scroll(self.scroll_state.clone())
                         .child(tab("Cargo.toml", false))
                         .child(tab("Channels Panel", true))
@@ -74,8 +83,8 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/plus.svg", ButtonVariant::Ghost))
-                            .child(icon_button("icons/split.svg", ButtonVariant::Ghost)),
+                            .child(icon_button("icons/plus.svg"))
+                            .child(icon_button("icons/split.svg")),
                     ),
             )
     }

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

@@ -0,0 +1,117 @@
+use std::marker::PhantomData;
+
+use crate::prelude::Shape;
+use crate::theme::theme;
+use crate::ui::{avatar, follow_group, icon_button, text_button, tool_divider};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct TitleBar<V: 'static> {
+    view_type: PhantomData<V>,
+}
+
+pub fn title_bar<V: 'static>() -> TitleBar<V> {
+    TitleBar {
+        view_type: PhantomData,
+    }
+}
+
+impl<V: 'static> TitleBar<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_list = vec![
+            avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
+            avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
+        ];
+
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .h_8()
+            .fill(theme.lowest.base.default.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .h_full()
+                    .gap_4()
+                    .px_2()
+                    // === Traffic Lights === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .w_3()
+                                    .h_3()
+                                    .rounded_full()
+                                    .fill(theme.lowest.positive.default.foreground),
+                            )
+                            .child(
+                                div()
+                                    .w_3()
+                                    .h_3()
+                                    .rounded_full()
+                                    .fill(theme.lowest.warning.default.foreground),
+                            )
+                            .child(
+                                div()
+                                    .w_3()
+                                    .h_3()
+                                    .rounded_full()
+                                    .fill(theme.lowest.negative.default.foreground),
+                            ),
+                    )
+                    // === Project Info === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(text_button("maxbrunsfeld"))
+                            .child(text_button("zed"))
+                            .child(text_button("nate/gpui2-ui-components")),
+                    )
+                    .child(follow_group(player_list.clone()).player(0))
+                    .child(follow_group(player_list.clone()).player(1))
+                    .child(follow_group(player_list.clone()).player(2)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(icon_button("icons/stop_sharing.svg"))
+                            .child(icon_button("icons/exit.svg")),
+                    )
+                    .child(tool_divider())
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(icon_button("icons/mic.svg"))
+                            .child(icon_button("icons/speaker-loud.svg"))
+                            .child(icon_button("icons/desktop.svg")),
+                    )
+                    .child(
+                        div().px_2().flex().items_center().child(
+                            avatar("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .shape(Shape::RoundedRectangle),
+                        ),
+                    ),
+            )
+    }
+}

crates/storybook/src/ui/tracker.md 🔗

@@ -0,0 +1,133 @@
+* = Not in the app today
+
+## Template
+- [ ] Workspace
+- [ ] Title Bar
+- [ ] Project Panel
+- [ ] Collab Panel
+- [ ] Project Diagnosics
+- [ ] Project Search
+- [ ] Feedback Editor
+- [ ] Terminal
+- [ ] Assistant
+- [ ] Chat*
+- [ ] Notifications*
+- [ ] Status Bar
+- [ ] Panes
+- [ ] Pane
+- [ ] Editor
+- [ ] Tab Bar
+- [ ] Tool Bar
+- [ ] Buffer
+- [ ] Zoomed Editor (Modal)
+
+### Palettes
+- [ ] Project Files Palette (⌘-P)
+- [ ] Command Palette (⌘-SHIFT-P)
+- [ ] Recent Projects Palette (⌘-OPT-O)
+- [ ] Recent Branches Palette (⌘-OPT-B)
+- [ ] Project Symbols (⌘-T)
+- [ ] Theme Palette (⌘-K, ⌘-T)
+- [ ] Outline View (⌘-SHIFT-O)
+
+### Debug Views
+- [ ] LSP Tool
+- [ ] Syntax Tree
+
+## Modules
+
+### Title Bar
+- [ ] Traffic Lights
+- [ ] Host Menu
+- [ ] Project Menu
+- [ ] Branch Menu
+- [ ] Collaborators
+- [ ] Add Collaborator*
+- [ ] Project Controls
+- [ ] Call Controls
+- [ ] User Menu
+
+### Project Panel
+- [ ] Open Editors*
+- [ ] Open Files (Non-project files)
+- [ ] Project Files
+- [ ] Root Folder - Context Menu
+- [ ] Folder - Context Menu
+- [ ] File - Context Menu
+- [ ] Project Filter*
+
+### Collab Panel
+- [ ] Current Call
+- [ ] Channels
+- [ ] Channel - Context Menu
+- [ ] Contacts
+- [ ] Collab Filter
+
+### Project Diagnosics
+WIP
+
+### Feedback Editor
+- [ ] Feedback Header
+- [ ] Editor
+- [ ] Feedback Actions
+
+### Terminal
+- [ ] Terminal Toolbar*
+- [ ] Terminal Line
+- [ ] Terminal Input
+
+### Assistant
+- [ ] Toolbar
+- [ ] History / Past Conversations
+- [ ] Model Controls / Token Counter
+- [ ] Chat Editor
+
+### Chat
+WIP
+
+### Notifications
+WIP
+
+### Status Bar
+- [ ] Status Bar Tool (Icon)
+- [ ] Status Bar Tool (Text)
+- [ ] Status Bar Tool - Context Menu
+- [ ] Status Bar Tool - Popover Palette
+- [ ] Status Bar Tool - Popover Menu
+- [ ] Diagnostic Message
+- [ ] LSP Message
+- [ ] Update message (New version available, downloading, etc)
+
+### Panes/Pane
+
+- [ ] Editor
+- [ ] Split Divider/Control
+
+### Editor
+- [ ] Editor
+- [ ] Read-only Editor
+- [ ] Rendered Markdown View*
+
+### Tab Bar
+- [ ] Navigation History / Control
+- [ ] Tabs
+- [ ] Editor Controls (New, Split, Zoom)
+
+### Tool Bar
+- [ ] Breadcrumb
+- [ ] Editor Tool (Togglable)
+- [ ] Buffer Search
+
+### Buffer
+
+### Zoomed Editor (Modal)
+- [ ] Modal View
+
+### Palette
+- [ ] Input
+- [ ] Section Title
+- [ ] List
+
+## Components
+
+- [ ] Context Menu

crates/storybook/src/workspace.rs 🔗

@@ -1,7 +1,10 @@
-use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme};
+use crate::{
+    theme::theme,
+    ui::{chat_panel, project_panel, status_bar, tab_bar, title_bar},
+};
 use gpui2::{
-    elements::{div, div::ScrollState, img, svg},
-    style::{StyleHelpers, Styleable},
+    elements::{div, div::ScrollState},
+    style::StyleHelpers,
     Element, IntoElement, ParentElement, ViewContext,
 };
 
@@ -29,8 +32,8 @@ impl WorkspaceElement {
             .justify_start()
             .items_start()
             .text_color(theme.lowest.base.default.foreground)
-            .fill(theme.middle.base.default.background)
-            .child(titlebar())
+            .fill(theme.lowest.base.default.background)
+            .child(title_bar())
             .child(
                 div()
                     .flex_1()
@@ -38,7 +41,7 @@ impl WorkspaceElement {
                     .flex()
                     .flex_row()
                     .overflow_hidden()
-                    .child(collab_panel(self.left_scroll_state.clone()))
+                    .child(project_panel(self.left_scroll_state.clone()))
                     .child(
                         div()
                             .h_full()
@@ -52,397 +55,8 @@ impl WorkspaceElement {
                                     .child(tab_bar(self.tab_bar_scroll_state.clone())),
                             ),
                     )
-                    .child(collab_panel(self.right_scroll_state.clone())),
-            )
-            .child(statusbar())
-    }
-}
-
-#[derive(Element)]
-struct TitleBar;
-
-pub fn titlebar<V: 'static>() -> impl Element<V> {
-    TitleBar
-}
-
-impl TitleBar {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .justify_between()
-            .w_full()
-            .h_8()
-            .fill(theme.lowest.base.default.background)
-            .child(self.left_group(cx))
-            .child(self.right_group(cx))
-    }
-
-    fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_4()
-            .px_2()
-            // === Traffic Lights === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_2()
-                    .child(
-                        div()
-                            .w_3()
-                            .h_3()
-                            .rounded_full()
-                            .fill(theme.lowest.positive.default.foreground),
-                    )
-                    .child(
-                        div()
-                            .w_3()
-                            .h_3()
-                            .rounded_full()
-                            .fill(theme.lowest.warning.default.foreground),
-                    )
-                    .child(
-                        div()
-                            .w_3()
-                            .h_3()
-                            .rounded_full()
-                            .fill(theme.lowest.negative.default.foreground),
-                    ),
-            )
-            // === Project Info === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .child(
-                        div()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .px_2()
-                            .rounded_md()
-                            .hover()
-                            .fill(theme.lowest.base.hovered.background)
-                            .active()
-                            .fill(theme.lowest.base.pressed.background)
-                            .child(div().text_sm().child("project")),
-                    )
-                    .child(
-                        div()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .px_2()
-                            .rounded_md()
-                            .text_color(theme.lowest.variant.default.foreground)
-                            .hover()
-                            .fill(theme.lowest.base.hovered.background)
-                            .active()
-                            .fill(theme.lowest.base.pressed.background)
-                            .child(div().text_sm().child("branch")),
-                    ),
-            )
-    }
-
-    fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_3()
-            .px_2()
-            // === Actions === //
-            .child(
-                div().child(
-                    div().flex().items_center().gap_1().child(
-                        div().size_4().flex().items_center().justify_center().child(
-                            svg()
-                                .path("icons/exit.svg")
-                                .size_4()
-                                .fill(theme.lowest.base.default.foreground),
-                        ),
-                    ),
-                ),
-            )
-            .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
-            // === Comms === //
-            .child(
-                div().child(
-                    div()
-                        .flex()
-                        .items_center()
-                        .gap_px()
-                        .child(
-                            div()
-                                .px_2()
-                                .py_1()
-                                .rounded_md()
-                                .h_full()
-                                .flex()
-                                .items_center()
-                                .justify_center()
-                                .hover()
-                                .fill(theme.lowest.base.hovered.background)
-                                .active()
-                                .fill(theme.lowest.base.pressed.background)
-                                .child(
-                                    svg()
-                                        .path("icons/microphone.svg")
-                                        .size_3p5()
-                                        .fill(theme.lowest.base.default.foreground),
-                                ),
-                        )
-                        .child(
-                            div()
-                                .px_2()
-                                .py_1()
-                                .rounded_md()
-                                .h_full()
-                                .flex()
-                                .items_center()
-                                .justify_center()
-                                .hover()
-                                .fill(theme.lowest.base.hovered.background)
-                                .active()
-                                .fill(theme.lowest.base.pressed.background)
-                                .child(
-                                    svg()
-                                        .path("icons/speaker-loud.svg")
-                                        .size_3p5()
-                                        .fill(theme.lowest.base.default.foreground),
-                                ),
-                        )
-                        .child(
-                            div()
-                                .px_2()
-                                .py_1()
-                                .rounded_md()
-                                .h_full()
-                                .flex()
-                                .items_center()
-                                .justify_center()
-                                .hover()
-                                .fill(theme.lowest.base.hovered.background)
-                                .active()
-                                .fill(theme.lowest.base.pressed.background)
-                                .child(
-                                    svg()
-                                        .path("icons/desktop.svg")
-                                        .size_3p5()
-                                        .fill(theme.lowest.base.default.foreground),
-                                ),
-                        ),
-                ),
-            )
-            .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
-            // User Group
-            .child(
-                div().child(
-                    div()
-                        .px_1()
-                        .py_1()
-                        .flex()
-                        .items_center()
-                        .justify_center()
-                        .rounded_md()
-                        .gap_0p5()
-                        .hover()
-                        .fill(theme.lowest.base.hovered.background)
-                        .active()
-                        .fill(theme.lowest.base.pressed.background)
-                        .child(
-                            img()
-                                .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
-                                .size_4()
-                                .rounded_md()
-                                .fill(theme.middle.on.default.foreground),
-                        )
-                        .child(
-                            svg()
-                                .path("icons/caret_down.svg")
-                                .w_2()
-                                .h_2()
-                                .fill(theme.lowest.variant.default.foreground),
-                        ),
-                ),
-            )
-    }
-}
-
-// ================================================================================ //
-
-#[derive(Element)]
-struct StatusBar;
-
-pub fn statusbar<V: 'static>() -> impl Element<V> {
-    StatusBar
-}
-
-impl StatusBar {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .justify_between()
-            .w_full()
-            .h_8()
-            .fill(theme.lowest.base.default.background)
-            .child(self.left_group(cx))
-            .child(self.right_group(cx))
-    }
-
-    fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_4()
-            .px_2()
-            // === Tools === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/project.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.base.default.foreground),
-                            ),
-                    )
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/conversations.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.base.default.foreground),
-                            ),
-                    )
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/file_icons/notebook.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.accent.default.foreground),
-                            ),
-                    ),
-            )
-            // === Diagnostics === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_2()
-                    .child(
-                        div()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .gap_0p5()
-                            .px_1()
-                            .text_color(theme.lowest.variant.default.foreground)
-                            .hover()
-                            .fill(theme.lowest.base.hovered.background)
-                            .active()
-                            .fill(theme.lowest.base.pressed.background)
-                            .child(
-                                svg()
-                                    .path("icons/error.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.negative.default.foreground),
-                            )
-                            .child(div().text_sm().child("2")),
-                    )
-                    .child(
-                        div()
-                            .text_sm()
-                            .text_color(theme.lowest.variant.default.foreground)
-                            .child("Something is wrong"),
-                    ),
-            )
-    }
-
-    fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_4()
-            .px_2()
-            // === Tools === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/check_circle.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.base.default.foreground),
-                            ),
-                    )
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/copilot.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.accent.default.foreground),
-                            ),
-                    ),
+                    .child(chat_panel(self.right_scroll_state.clone())),
             )
+            .child(status_bar())
     }
 }

docs/ui/states.md 🔗

@@ -0,0 +1,43 @@
+## Interaction State
+
+**Enabled**
+
+An enabled state communicates an interactive component or element.
+
+**Disabled**
+
+A disabled state communicates a inoperable component or element.
+
+**Hover**
+
+A hover state communicates when a user has placed a cursor above an interactive element.
+
+**Focused**
+
+A focused state communicates when a user has highlighted an element, using an input method such as a keyboard or voice.
+
+**Activated**
+
+An activated state communicates a highlighted destination, whether initiated by the user or by default.
+
+**Pressed**
+
+A pressed state communicates a user tap.
+
+**Dragged**
+
+A dragged state communicates when a user presses and moves an element.
+
+## Selected State
+
+**Unselected**
+
+dfa
+
+**Partially Selected**
+
+daf
+
+**Selected**
+
+dfa