Mainline GPUI2 UI work (#3062)

Marshall Bowers , Nate Butler , Max Brunsfeld , Marshall Bowers , Piotr Osiewicz , Nate , and Mikayla created

This PR mainlines the current state of new GPUI2-based UI from the
`gpui2-ui` branch.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Nate <nate@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

Cargo.lock                                                 |   5 
crates/gpui2/src/element.rs                                |  25 
crates/storybook/Cargo.toml                                |   2 
crates/storybook/src/collab_panel.rs                       | 177 --
crates/storybook/src/stories.rs                            |   1 
crates/storybook/src/stories/components.rs                 |  14 
crates/storybook/src/stories/components/assistant_panel.rs |  16 
crates/storybook/src/stories/components/breadcrumb.rs      |  12 
crates/storybook/src/stories/components/buffer.rs          |  34 
crates/storybook/src/stories/components/chat_panel.rs      |  34 
crates/storybook/src/stories/components/collab_panel.rs    |  16 
crates/storybook/src/stories/components/context_menu.rs    |  21 
crates/storybook/src/stories/components/facepile.rs        |  41 
crates/storybook/src/stories/components/keybinding.rs      |  64 
crates/storybook/src/stories/components/palette.rs         |  53 
crates/storybook/src/stories/components/panel.rs           |  24 
crates/storybook/src/stories/components/project_panel.rs   |  16 
crates/storybook/src/stories/components/status_bar.rs      |  16 
crates/storybook/src/stories/components/tab.rs             |  91 +
crates/storybook/src/stories/components/tab_bar.rs         |  16 
crates/storybook/src/stories/components/terminal.rs        |  16 
crates/storybook/src/stories/components/title_bar.rs       |  16 
crates/storybook/src/stories/components/toolbar.rs         |  12 
crates/storybook/src/stories/components/traffic_lights.rs  |  16 
crates/storybook/src/stories/elements.rs                   |   4 
crates/storybook/src/stories/elements/avatar.rs            |  17 
crates/storybook/src/stories/elements/button.rs            | 192 ++
crates/storybook/src/stories/elements/icon.rs              |  19 
crates/storybook/src/stories/elements/input.rs             |  16 
crates/storybook/src/stories/elements/label.rs             |  18 
crates/storybook/src/stories/kitchen_sink.rs               |  46 
crates/storybook/src/story.rs                              |  28 
crates/storybook/src/story_selector.rs                     | 122 +
crates/storybook/src/storybook.rs                          | 119 +
crates/storybook/src/workspace.rs                          |  56 
crates/ui/Cargo.toml                                       |   3 
crates/ui/docs/_project.md                                 |  13 
crates/ui/docs/elevation.md                                |   0 
crates/ui/src/children.rs                                  |   7 
crates/ui/src/components.rs                                | 227 +-
crates/ui/src/components/assistant_panel.rs                |  91 +
crates/ui/src/components/breadcrumb.rs                     |  19 
crates/ui/src/components/buffer.rs                         | 229 +++
crates/ui/src/components/chat_panel.rs                     | 139 +
crates/ui/src/components/collab_panel.rs                   | 122 
crates/ui/src/components/command_palette.rs                |  20 
crates/ui/src/components/context_menu.rs                   |  65 
crates/ui/src/components/editor.rs                         |  25 
crates/ui/src/components/facepile.rs                       |  22 
crates/ui/src/components/follow_group.rs                   |  52 
crates/ui/src/components/icon_button.rs                    |  25 
crates/ui/src/components/keybinding.rs                     | 158 ++
crates/ui/src/components/list.rs                           | 516 ++++++
crates/ui/src/components/list_item.rs                      | 112 -
crates/ui/src/components/list_section_header.rs            |  88 -
crates/ui/src/components/palette.rs                        | 116 
crates/ui/src/components/palette_item.rs                   |  63 
crates/ui/src/components/panel.rs                          | 146 +
crates/ui/src/components/panes.rs                          | 132 +
crates/ui/src/components/player_stack.rs                   |  66 
crates/ui/src/components/project_panel.rs                  | 111 
crates/ui/src/components/status_bar.rs                     |  49 
crates/ui/src/components/tab.rs                            | 137 +
crates/ui/src/components/tab_bar.rs                        |  87 
crates/ui/src/components/terminal.rs                       |  77 +
crates/ui/src/components/title_bar.rs                      |  69 
crates/ui/src/components/toolbar.rs                        |  26 
crates/ui/src/components/traffic_lights.rs                 |  76 
crates/ui/src/components/workspace.rs                      | 140 +
crates/ui/src/elements.rs                                  |  10 
crates/ui/src/elements/avatar.rs                           |  15 
crates/ui/src/elements/button.rs                           | 203 ++
crates/ui/src/elements/details.rs                          |  13 
crates/ui/src/elements/icon.rs                             | 140 +
crates/ui/src/elements/indicator.rs                        |  33 
crates/ui/src/elements/input.rs                            |  29 
crates/ui/src/elements/label.rs                            | 144 +
crates/ui/src/elements/player.rs                           | 132 +
crates/ui/src/elements/stack.rs                            |  31 
crates/ui/src/elements/text_button.rs                      |  82 -
crates/ui/src/elements/tool_divider.rs                     |  13 
crates/ui/src/lib.rs                                       |   5 
crates/ui/src/prelude.rs                                   | 211 ++
crates/ui/src/static_data.rs                               | 590 ++++++-
crates/ui/src/tokens.rs                                    |   7 
85 files changed, 4,648 insertions(+), 1,613 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7398,8 +7398,10 @@ name = "storybook"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "chrono",
  "clap 4.4.4",
  "gpui2",
+ "itertools 0.11.0",
  "log",
  "rust-embed",
  "serde",
@@ -8631,9 +8633,12 @@ name = "ui"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "chrono",
  "gpui2",
  "serde",
  "settings",
+ "smallvec",
+ "strum",
  "theme",
 ]
 

crates/gpui2/src/element.rs 🔗

@@ -198,6 +198,31 @@ pub trait ParentElement<V: 'static> {
         );
         self
     }
+
+    // HACK: This is a temporary hack to get children working for the purposes
+    // of building UI on top of the current version of gpui2.
+    //
+    // We'll (hopefully) be moving away from this in the future.
+    fn children_any<I>(mut self, children: I) -> Self
+    where
+        I: IntoIterator<Item = AnyElement<V>>,
+        Self: Sized,
+    {
+        self.children_mut().extend(children.into_iter());
+        self
+    }
+
+    // HACK: This is a temporary hack to get children working for the purposes
+    // of building UI on top of the current version of gpui2.
+    //
+    // We'll (hopefully) be moving away from this in the future.
+    fn child_any(mut self, children: AnyElement<V>) -> Self
+    where
+        Self: Sized,
+    {
+        self.children_mut().push(children);
+        self
+    }
 }
 
 pub trait IntoElement<V: 'static> {

crates/storybook/Cargo.toml 🔗

@@ -11,7 +11,9 @@ path = "src/storybook.rs"
 [dependencies]
 anyhow.workspace = true
 clap = { version = "4.4", features = ["derive", "string"] }
+chrono = "0.4"
 gpui2 = { path = "../gpui2" }
+itertools = "0.11.0"
 log.workspace = true
 rust-embed.workspace = true
 serde.workspace = true

crates/storybook/src/collab_panel.rs 🔗

@@ -1,177 +0,0 @@
-use gpui2::{
-    elements::{div, div::ScrollState, img, svg},
-    style::{StyleHelpers, Styleable},
-    ArcCow, Element, IntoElement, ParentElement, ViewContext,
-};
-use std::marker::PhantomData;
-use ui::{theme, Theme};
-
-#[derive(Element)]
-pub struct CollabPanelElement<V: 'static> {
-    view_type: PhantomData<V>,
-    scroll_state: ScrollState,
-}
-
-// When I improve child view rendering, I'd like to have V implement a trait  that
-// provides the scroll state, among other things.
-pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
-    CollabPanelElement {
-        view_type: PhantomData,
-        scroll_state,
-    }
-}
-
-impl<V: 'static> CollabPanelElement<V> {
-    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        // Panel
-        div()
-            .w_64()
-            .h_full()
-            .flex()
-            .flex_col()
-            .font("Zed Sans Extended")
-            .text_color(theme.middle.base.default.foreground)
-            .border_color(theme.middle.base.default.border)
-            .border()
-            .fill(theme.middle.base.default.background)
-            .child(
-                div()
-                    .w_full()
-                    .flex()
-                    .flex_col()
-                    .overflow_y_scroll(self.scroll_state.clone())
-                    // List Container
-                    .child(
-                        div()
-                            .fill(theme.lowest.base.default.background)
-                            .pb_1()
-                            .border_color(theme.lowest.base.default.border)
-                            .border_b()
-                            //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
-                            // .group()
-                            // List Section Header
-                            .child(self.list_section_header("#CRDB", true, &theme))
-                            // List Item Large
-                            .child(self.list_item(
-                                "http://github.com/maxbrunsfeld.png?s=50",
-                                "maxbrunsfeld",
-                                &theme,
-                            )),
-                    )
-                    .child(
-                        div()
-                            .py_2()
-                            .flex()
-                            .flex_col()
-                            .child(self.list_section_header("CHANNELS", true, &theme)),
-                    )
-                    .child(
-                        div()
-                            .py_2()
-                            .flex()
-                            .flex_col()
-                            .child(self.list_section_header("CONTACTS", true, &theme))
-                            .children(
-                                std::iter::repeat_with(|| {
-                                    vec![
-                                        self.list_item(
-                                            "http://github.com/as-cii.png?s=50",
-                                            "as-cii",
-                                            &theme,
-                                        ),
-                                        self.list_item(
-                                            "http://github.com/nathansobo.png?s=50",
-                                            "nathansobo",
-                                            &theme,
-                                        ),
-                                        self.list_item(
-                                            "http://github.com/maxbrunsfeld.png?s=50",
-                                            "maxbrunsfeld",
-                                            &theme,
-                                        ),
-                                    ]
-                                })
-                                .take(10)
-                                .flatten(),
-                            ),
-                    ),
-            )
-            .child(
-                div()
-                    .h_7()
-                    .px_2()
-                    .border_t()
-                    .border_color(theme.middle.variant.default.border)
-                    .flex()
-                    .items_center()
-                    .child(
-                        div()
-                            .text_sm()
-                            .text_color(theme.middle.variant.default.foreground)
-                            .child("Find..."),
-                    ),
-            )
-    }
-
-    fn list_section_header(
-        &self,
-        label: impl Into<ArcCow<'static, str>>,
-        expanded: bool,
-        theme: &Theme,
-    ) -> impl Element<V> {
-        div()
-            .h_7()
-            .px_2()
-            .flex()
-            .justify_between()
-            .items_center()
-            .child(div().flex().gap_1().text_sm().child(label))
-            .child(
-                div().flex().h_full().gap_1().items_center().child(
-                    svg()
-                        .path(if expanded {
-                            "icons/caret_down.svg"
-                        } else {
-                            "icons/caret_up.svg"
-                        })
-                        .w_3p5()
-                        .h_3p5()
-                        .fill(theme.middle.variant.default.foreground),
-                ),
-            )
-    }
-
-    fn list_item(
-        &self,
-        avatar_uri: impl Into<ArcCow<'static, str>>,
-        label: impl Into<ArcCow<'static, str>>,
-        theme: &Theme,
-    ) -> impl Element<V> {
-        div()
-            .h_7()
-            .px_2()
-            .flex()
-            .items_center()
-            .hover()
-            .fill(theme.lowest.variant.hovered.background)
-            .active()
-            .fill(theme.lowest.variant.pressed.background)
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .text_sm()
-                    .child(
-                        img()
-                            .uri(avatar_uri)
-                            .size_3p5()
-                            .rounded_full()
-                            .fill(theme.middle.positive.default.foreground),
-                    )
-                    .child(label),
-            )
-    }
-}

crates/storybook/src/stories/components.rs 🔗

@@ -1,4 +1,18 @@
+pub mod assistant_panel;
 pub mod breadcrumb;
+pub mod buffer;
+pub mod chat_panel;
+pub mod collab_panel;
+pub mod context_menu;
 pub mod facepile;
+pub mod keybinding;
+pub mod palette;
+pub mod panel;
+pub mod project_panel;
+pub mod status_bar;
+pub mod tab;
+pub mod tab_bar;
+pub mod terminal;
+pub mod title_bar;
 pub mod toolbar;
 pub mod traffic_lights;

crates/storybook/src/stories/components/assistant_panel.rs 🔗

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::AssistantPanel;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct AssistantPanelStory {}
+
+impl AssistantPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, AssistantPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(AssistantPanel::new())
+    }
+}

crates/storybook/src/stories/components/breadcrumb.rs 🔗

@@ -1,5 +1,5 @@
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-use ui::breadcrumb;
+use ui::prelude::*;
+use ui::Breadcrumb;
 
 use crate::story::Story;
 
@@ -8,9 +8,9 @@ pub struct BreadcrumbStory {}
 
 impl BreadcrumbStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        Story::container()
-            .child(Story::title_for::<_, ui::Breadcrumb>())
-            .child(Story::label("Default"))
-            .child(breadcrumb())
+        Story::container(cx)
+            .child(Story::title_for::<_, Breadcrumb>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Breadcrumb::new())
     }
 }

crates/storybook/src/stories/components/buffer.rs 🔗

@@ -0,0 +1,34 @@
+use gpui2::geometry::rems;
+use ui::prelude::*;
+use ui::{
+    empty_buffer_example, hello_world_rust_buffer_example,
+    hello_world_rust_buffer_with_status_example, Buffer,
+};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct BufferStory {}
+
+impl BufferStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Buffer<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
+            .child(Story::label(cx, "Hello World (Rust)"))
+            .child(
+                div()
+                    .w(rems(64.))
+                    .h_96()
+                    .child(hello_world_rust_buffer_example(cx)),
+            )
+            .child(Story::label(cx, "Hello World (Rust) with Status"))
+            .child(
+                div()
+                    .w(rems(64.))
+                    .h_96()
+                    .child(hello_world_rust_buffer_with_status_example(cx)),
+            )
+    }
+}

crates/storybook/src/stories/components/chat_panel.rs 🔗

@@ -0,0 +1,34 @@
+use chrono::DateTime;
+use ui::prelude::*;
+use ui::{ChatMessage, ChatPanel};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ChatPanelStory {}
+
+impl ChatPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ChatPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ChatPanel::new(ScrollState::default()))
+            .child(Story::label(cx, "With Mesages"))
+            .child(ChatPanel::new(ScrollState::default()).with_messages(vec![
+                    ChatMessage::new(
+                        "osiewicz".to_string(),
+                        "is this thing on?".to_string(),
+                        DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                            .unwrap()
+                            .naive_local(),
+                    ),
+                    ChatMessage::new(
+                        "maxdeviant".to_string(),
+                        "Reading you loud and clear!".to_string(),
+                        DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                            .unwrap()
+                            .naive_local(),
+                    ),
+                ]))
+    }
+}

crates/storybook/src/stories/components/collab_panel.rs 🔗

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::CollabPanel;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct CollabPanelStory {}
+
+impl CollabPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, CollabPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(CollabPanel::new(ScrollState::default()))
+    }
+}

crates/storybook/src/stories/components/context_menu.rs 🔗

@@ -0,0 +1,21 @@
+use ui::prelude::*;
+use ui::{ContextMenu, ContextMenuItem, Label};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ContextMenuStory {}
+
+impl ContextMenuStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            //.fill(theme.middle.base.default.background)
+            .child(Story::title_for::<_, ContextMenu>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ContextMenu::new([
+                ContextMenuItem::header("Section header"),
+                ContextMenuItem::Separator,
+                ContextMenuItem::entry(Label::new("Some entry")),
+            ]))
+    }
+}

crates/storybook/src/stories/components/facepile.rs 🔗

@@ -1,8 +1,5 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 use ui::prelude::*;
-use ui::{avatar, facepile, theme};
+use ui::{static_players, Facepile};
 
 use crate::story::Story;
 
@@ -11,40 +8,18 @@ pub struct FacepileStory {}
 
 impl FacepileStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
+        let players = static_players();
 
-        let avatars = vec![
-            avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
-            avatar("https://avatars.githubusercontent.com/u/482957?v=4"),
-            avatar("https://avatars.githubusercontent.com/u/1789?v=4"),
-        ];
-
-        Story::container()
-            .child(Story::title_for::<_, ui::Facepile>())
-            .child(Story::label("Default"))
+        Story::container(cx)
+            .child(Story::title_for::<_, ui::Facepile>(cx))
+            .child(Story::label(cx, "Default"))
             .child(
                 div()
                     .flex()
                     .gap_3()
-                    .child(facepile(avatars.clone().into_iter().take(1)))
-                    .child(facepile(avatars.clone().into_iter().take(2)))
-                    .child(facepile(avatars.clone().into_iter().take(3))),
+                    .child(Facepile::new(players.clone().into_iter().take(1)))
+                    .child(Facepile::new(players.clone().into_iter().take(2)))
+                    .child(Facepile::new(players.clone().into_iter().take(3))),
             )
-            .child(Story::label("Rounded rectangle avatars"))
-            .child({
-                let shape = Shape::RoundedRectangle;
-
-                let avatars = avatars
-                    .clone()
-                    .into_iter()
-                    .map(|avatar| avatar.shape(Shape::RoundedRectangle));
-
-                div()
-                    .flex()
-                    .gap_3()
-                    .child(facepile(avatars.clone().take(1)))
-                    .child(facepile(avatars.clone().take(2)))
-                    .child(facepile(avatars.clone().take(3)))
-            })
     }
 }

crates/storybook/src/stories/components/keybinding.rs 🔗

@@ -0,0 +1,64 @@
+use itertools::Itertools;
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{Keybinding, ModifierKey, ModifierKeys};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct KeybindingStory {}
+
+impl KeybindingStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let all_modifier_permutations = ModifierKey::iter().permutations(2);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Keybinding>(cx))
+            .child(Story::label(cx, "Single Key"))
+            .child(Keybinding::new("Z".to_string(), ModifierKeys::new()))
+            .child(Story::label(cx, "Single Key with Modifier"))
+            .child(
+                div()
+                    .flex()
+                    .gap_3()
+                    .children(ModifierKey::iter().map(|modifier| {
+                        Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier))
+                    })),
+            )
+            .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
+            .child(
+                div().flex().flex_col().children(
+                    all_modifier_permutations
+                        .chunks(4)
+                        .into_iter()
+                        .map(|chunk| {
+                            div()
+                                .flex()
+                                .gap_4()
+                                .py_3()
+                                .children(chunk.map(|permutation| {
+                                    let mut modifiers = ModifierKeys::new();
+
+                                    for modifier in permutation {
+                                        modifiers = modifiers.add(modifier);
+                                    }
+
+                                    Keybinding::new("X".to_string(), modifiers)
+                                }))
+                        }),
+                ),
+            )
+            .child(Story::label(cx, "Single Key with All Modifiers"))
+            .child(Keybinding::new("Z".to_string(), ModifierKeys::all()))
+            .child(Story::label(cx, "Chord"))
+            .child(Keybinding::new_chord(
+                ("A".to_string(), ModifierKeys::new()),
+                ("Z".to_string(), ModifierKeys::new()),
+            ))
+            .child(Story::label(cx, "Chord with Modifier"))
+            .child(Keybinding::new_chord(
+                ("A".to_string(), ModifierKeys::new().control(true)),
+                ("Z".to_string(), ModifierKeys::new().shift(true)),
+            ))
+    }
+}

crates/storybook/src/stories/components/palette.rs 🔗

@@ -0,0 +1,53 @@
+use ui::prelude::*;
+use ui::{Keybinding, ModifierKeys, Palette, PaletteItem};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct PaletteStory {}
+
+impl PaletteStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Palette<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Palette::new(ScrollState::default()))
+            .child(Story::label(cx, "With Items"))
+            .child(
+                Palette::new(ScrollState::default())
+                    .placeholder("Execute a command...")
+                    .items(vec![
+                        PaletteItem::new("theme selector: toggle").keybinding(
+                            Keybinding::new_chord(
+                                ("k".to_string(), ModifierKeys::new().command(true)),
+                                ("t".to_string(), ModifierKeys::new().command(true)),
+                            ),
+                        ),
+                        PaletteItem::new("assistant: inline assist").keybinding(Keybinding::new(
+                            "enter".to_string(),
+                            ModifierKeys::new().command(true),
+                        )),
+                        PaletteItem::new("assistant: quote selection").keybinding(Keybinding::new(
+                            ">".to_string(),
+                            ModifierKeys::new().command(true),
+                        )),
+                        PaletteItem::new("assistant: toggle focus").keybinding(Keybinding::new(
+                            "?".to_string(),
+                            ModifierKeys::new().command(true),
+                        )),
+                        PaletteItem::new("auto update: check"),
+                        PaletteItem::new("auto update: view release notes"),
+                        PaletteItem::new("branches: open recent").keybinding(Keybinding::new(
+                            "b".to_string(),
+                            ModifierKeys::new().command(true).alt(true),
+                        )),
+                        PaletteItem::new("chat panel: toggle focus"),
+                        PaletteItem::new("cli: install"),
+                        PaletteItem::new("client: sign in"),
+                        PaletteItem::new("client: sign out"),
+                        PaletteItem::new("editor: cancel")
+                            .keybinding(Keybinding::new("escape".to_string(), ModifierKeys::new())),
+                    ]),
+            )
+    }
+}

crates/storybook/src/stories/components/panel.rs 🔗

@@ -0,0 +1,24 @@
+use ui::prelude::*;
+use ui::{Label, Panel};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct PanelStory {}
+
+impl PanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Panel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| {
+                    (0..100)
+                        .map(|ix| Label::new(format!("Item {}", ix + 1)).into_any())
+                        .collect()
+                },
+                Box::new(()),
+            ))
+    }
+}

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

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::ProjectPanel;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ProjectPanelStory {}
+
+impl ProjectPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ProjectPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ProjectPanel::new(ScrollState::default()))
+    }
+}

crates/storybook/src/stories/components/status_bar.rs 🔗

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::StatusBar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct StatusBarStory {}
+
+impl StatusBarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, StatusBar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(StatusBar::new())
+    }
+}

crates/storybook/src/stories/components/tab.rs 🔗

@@ -0,0 +1,91 @@
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Tab};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TabStory {}
+
+impl TabStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let git_statuses = GitStatus::iter();
+        let fs_statuses = FileSystemStatus::iter();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Tab>(cx))
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Default"))
+                        .child(Tab::new()),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack().gap_2().child(Story::label(cx, "Current")).child(
+                        h_stack()
+                            .gap_4()
+                            .child(Tab::new().title("Current".to_string()).current(true))
+                            .child(Tab::new().title("Not Current".to_string()).current(false)),
+                    ),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Titled"))
+                        .child(Tab::new().title("label".to_string())),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "With Icon"))
+                        .child(
+                            Tab::new()
+                                .title("label".to_string())
+                                .icon(Some(ui::Icon::Envelope)),
+                        ),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Close Side"))
+                        .child(
+                            h_stack()
+                                .gap_4()
+                                .child(
+                                    Tab::new()
+                                        .title("Left".to_string())
+                                        .close_side(IconSide::Left),
+                                )
+                                .child(Tab::new().title("Right".to_string())),
+                        ),
+                ),
+            )
+            .child(
+                v_stack()
+                    .gap_2()
+                    .child(Story::label(cx, "Git Status"))
+                    .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
+                        Tab::new()
+                            .title(git_status.to_string())
+                            .git_status(git_status)
+                    }))),
+            )
+            .child(
+                v_stack()
+                    .gap_2()
+                    .child(Story::label(cx, "File System Status"))
+                    .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
+                        Tab::new().title(fs_status.to_string()).fs_status(fs_status)
+                    }))),
+            )
+    }
+}

crates/storybook/src/stories/components/tab_bar.rs 🔗

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::TabBar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TabBarStory {}
+
+impl TabBarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TabBar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TabBar::new(ScrollState::default()))
+    }
+}

crates/storybook/src/stories/components/terminal.rs 🔗

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::Terminal;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TerminalStory {}
+
+impl TerminalStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Terminal>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Terminal::new())
+    }
+}

crates/storybook/src/stories/components/title_bar.rs 🔗

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::TitleBar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TitleBarStory {}
+
+impl TitleBarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TitleBar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TitleBar::new(cx))
+    }
+}

crates/storybook/src/stories/components/toolbar.rs 🔗

@@ -1,5 +1,5 @@
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-use ui::toolbar;
+use ui::prelude::*;
+use ui::Toolbar;
 
 use crate::story::Story;
 
@@ -8,9 +8,9 @@ pub struct ToolbarStory {}
 
 impl ToolbarStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        Story::container()
-            .child(Story::title_for::<_, ui::Toolbar>())
-            .child(Story::label("Default"))
-            .child(toolbar())
+        Story::container(cx)
+            .child(Story::title_for::<_, Toolbar>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Toolbar::new())
     }
 }

crates/storybook/src/stories/components/traffic_lights.rs 🔗

@@ -1,5 +1,5 @@
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-use ui::{theme, traffic_lights};
+use ui::prelude::*;
+use ui::TrafficLights;
 
 use crate::story::Story;
 
@@ -8,11 +8,11 @@ pub struct TrafficLightsStory {}
 
 impl TrafficLightsStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        Story::container()
-            .child(Story::title_for::<_, ui::TrafficLights>())
-            .child(Story::label("Default"))
-            .child(traffic_lights())
+        Story::container(cx)
+            .child(Story::title_for::<_, TrafficLights>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TrafficLights::new())
+            .child(Story::label(cx, "Unfocused"))
+            .child(TrafficLights::new().window_has_focus(false))
     }
 }

crates/storybook/src/stories/elements/avatar.rs 🔗

@@ -1,6 +1,5 @@
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 use ui::prelude::*;
-use ui::{avatar, theme};
+use ui::Avatar;
 
 use crate::story::Story;
 
@@ -9,17 +8,15 @@ pub struct AvatarStory {}
 
 impl AvatarStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        Story::container()
-            .child(Story::title_for::<_, ui::Avatar>())
-            .child(Story::label("Default"))
-            .child(avatar(
+        Story::container(cx)
+            .child(Story::title_for::<_, ui::Avatar>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Avatar::new(
                 "https://avatars.githubusercontent.com/u/1714999?v=4",
             ))
-            .child(Story::label("Rounded rectangle"))
+            .child(Story::label(cx, "Rounded rectangle"))
             .child(
-                avatar("https://avatars.githubusercontent.com/u/1714999?v=4")
+                Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
                     .shape(Shape::RoundedRectangle),
             )
     }

crates/storybook/src/stories/elements/button.rs 🔗

@@ -0,0 +1,192 @@
+use gpui2::elements::div;
+use gpui2::geometry::rems;
+use gpui2::{Element, IntoElement, ViewContext};
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Button, Icon, IconPosition, Label};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ButtonStory {}
+
+impl ButtonStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let states = InteractionState::iter();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Button<V>>(cx))
+            .child(
+                div()
+                    .flex()
+                    .gap_8()
+                    .child(
+                        div()
+                            .child(Story::label(cx, "Ghost (Default)"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Ghost)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Ghost – Left Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Ghost)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Left)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Ghost – Right Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Ghost)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Right)
+                                            .state(state),
+                                    )
+                            }))),
+                    )
+                    .child(
+                        div()
+                            .child(Story::label(cx, "Filled"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Filled – Left Button"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Left)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Filled – Right Button"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Right)
+                                            .state(state),
+                                    )
+                            }))),
+                    )
+                    .child(
+                        div()
+                            .child(Story::label(cx, "Fixed With"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state)
+                                            .width(Some(rems(6.).into())),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Fixed With – Left Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Left)
+                                            .width(Some(rems(6.).into())),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Fixed With – Right Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Right)
+                                            .width(Some(rems(6.).into())),
+                                    )
+                            }))),
+                    ),
+            )
+            .child(Story::label(cx, "Button with `on_click`"))
+            .child(
+                Button::new("Label")
+                    .variant(ButtonVariant::Ghost)
+                    // NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire.
+                    // So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire.
+                    .on_click(|_view, _cx| println!("Button clicked.")),
+            )
+    }
+}

crates/storybook/src/stories/elements/icon.rs 🔗

@@ -0,0 +1,19 @@
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{Icon, IconElement};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct IconStory {}
+
+impl IconStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let icons = Icon::iter();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, ui::IconElement>(cx))
+            .child(Story::label(cx, "All Icons"))
+            .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+    }
+}

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

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::Input;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct InputStory {}
+
+impl InputStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Input>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(div().flex().child(Input::new("Search")))
+    }
+}

crates/storybook/src/stories/elements/label.rs 🔗

@@ -0,0 +1,18 @@
+use ui::prelude::*;
+use ui::Label;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct LabelStory {}
+
+impl LabelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Label>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Label::new("Hello, world!"))
+            .child(Story::label(cx, "Highlighted"))
+            .child(Label::new("Hello, world!").with_highlights(vec![0, 1, 2, 7, 8, 12]))
+    }
+}

crates/storybook/src/stories/kitchen_sink.rs 🔗

@@ -0,0 +1,46 @@
+use ui::prelude::*;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct KitchenSinkStory {}
+
+impl KitchenSinkStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title(cx, "Kitchen Sink"))
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(ScrollState::default())
+                    .child(crate::stories::elements::avatar::AvatarStory::default())
+                    .child(crate::stories::elements::button::ButtonStory::default())
+                    .child(crate::stories::elements::icon::IconStory::default())
+                    .child(crate::stories::elements::input::InputStory::default())
+                    .child(crate::stories::elements::label::LabelStory::default())
+                    .child(
+                        crate::stories::components::assistant_panel::AssistantPanelStory::default(),
+                    )
+                    .child(crate::stories::components::breadcrumb::BreadcrumbStory::default())
+                    .child(crate::stories::components::buffer::BufferStory::default())
+                    .child(crate::stories::components::chat_panel::ChatPanelStory::default())
+                    .child(crate::stories::components::collab_panel::CollabPanelStory::default())
+                    .child(crate::stories::components::facepile::FacepileStory::default())
+                    .child(crate::stories::components::keybinding::KeybindingStory::default())
+                    .child(crate::stories::components::palette::PaletteStory::default())
+                    .child(crate::stories::components::panel::PanelStory::default())
+                    .child(crate::stories::components::project_panel::ProjectPanelStory::default())
+                    .child(crate::stories::components::status_bar::StatusBarStory::default())
+                    .child(crate::stories::components::tab::TabStory::default())
+                    .child(crate::stories::components::tab_bar::TabBarStory::default())
+                    .child(crate::stories::components::terminal::TerminalStory::default())
+                    .child(crate::stories::components::title_bar::TitleBarStory::default())
+                    .child(crate::stories::components::toolbar::ToolbarStory::default())
+                    .child(
+                        crate::stories::components::traffic_lights::TrafficLightsStory::default(),
+                    )
+                    .child(crate::stories::components::context_menu::ContextMenuStory::default()),
+            )
+    }
+}

crates/storybook/src/story.rs 🔗

@@ -1,11 +1,13 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{rgb, Element, Hsla, ParentElement};
+use gpui2::elements::div::Div;
+use ui::prelude::*;
+use ui::theme;
 
 pub struct Story {}
 
 impl Story {
-    pub fn container<V: 'static>() -> div::Div<V> {
+    pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
+        let theme = theme(cx);
+
         div()
             .size_full()
             .flex()
@@ -13,26 +15,30 @@ impl Story {
             .pt_2()
             .px_4()
             .font("Zed Mono Extended")
-            .fill(rgb::<Hsla>(0x282c34))
+            .fill(theme.lowest.base.default.background)
     }
 
-    pub fn title<V: 'static>(title: &str) -> impl Element<V> {
+    pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Element<V> {
+        let theme = theme(cx);
+
         div()
             .text_xl()
-            .text_color(rgb::<Hsla>(0xffffff))
+            .text_color(theme.lowest.base.default.foreground)
             .child(title.to_owned())
     }
 
-    pub fn title_for<V: 'static, T>() -> impl Element<V> {
-        Self::title(std::any::type_name::<T>())
+    pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Element<V> {
+        Self::title(cx, std::any::type_name::<T>())
     }
 
-    pub fn label<V: 'static>(label: &str) -> impl Element<V> {
+    pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Element<V> {
+        let theme = theme(cx);
+
         div()
             .mt_4()
             .mb_2()
             .text_xs()
-            .text_color(rgb::<Hsla>(0xffffff))
+            .text_color(theme.lowest.base.default.foreground)
             .child(label.to_owned())
     }
 }

crates/storybook/src/story_selector.rs 🔗

@@ -1,29 +1,97 @@
-use std::{str::FromStr, sync::OnceLock};
+use std::str::FromStr;
+use std::sync::OnceLock;
 
 use anyhow::{anyhow, Context};
 use clap::builder::PossibleValue;
 use clap::ValueEnum;
+use gpui2::{AnyElement, Element};
 use strum::{EnumIter, EnumString, IntoEnumIterator};
 
-#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
 #[strum(serialize_all = "snake_case")]
 pub enum ElementStory {
     Avatar,
+    Button,
+    Icon,
+    Input,
+    Label,
 }
 
-#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
+impl ElementStory {
+    pub fn story<V: 'static>(&self) -> AnyElement<V> {
+        use crate::stories::elements;
+
+        match self {
+            Self::Avatar => elements::avatar::AvatarStory::default().into_any(),
+            Self::Button => elements::button::ButtonStory::default().into_any(),
+            Self::Icon => elements::icon::IconStory::default().into_any(),
+            Self::Input => elements::input::InputStory::default().into_any(),
+            Self::Label => elements::label::LabelStory::default().into_any(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
 #[strum(serialize_all = "snake_case")]
 pub enum ComponentStory {
+    AssistantPanel,
     Breadcrumb,
+    Buffer,
+    ContextMenu,
+    ChatPanel,
+    CollabPanel,
     Facepile,
+    Keybinding,
+    Palette,
+    Panel,
+    ProjectPanel,
+    StatusBar,
+    Tab,
+    TabBar,
+    Terminal,
+    TitleBar,
     Toolbar,
     TrafficLights,
 }
 
-#[derive(Debug, Clone, Copy)]
+impl ComponentStory {
+    pub fn story<V: 'static>(&self) -> AnyElement<V> {
+        use crate::stories::components;
+
+        match self {
+            Self::AssistantPanel => {
+                components::assistant_panel::AssistantPanelStory::default().into_any()
+            }
+            Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(),
+            Self::Buffer => components::buffer::BufferStory::default().into_any(),
+            Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(),
+            Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(),
+            Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
+            Self::Facepile => components::facepile::FacepileStory::default().into_any(),
+            Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
+            Self::Palette => components::palette::PaletteStory::default().into_any(),
+            Self::Panel => components::panel::PanelStory::default().into_any(),
+            Self::ProjectPanel => {
+                components::project_panel::ProjectPanelStory::default().into_any()
+            }
+            Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
+            Self::Tab => components::tab::TabStory::default().into_any(),
+            Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
+            Self::Terminal => components::terminal::TerminalStory::default().into_any(),
+            Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
+            Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
+            Self::TrafficLights => {
+                components::traffic_lights::TrafficLightsStory::default().into_any()
+            }
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum StorySelector {
     Element(ElementStory),
     Component(ComponentStory),
+    KitchenSink,
 }
 
 impl FromStr for StorySelector {
@@ -32,6 +100,10 @@ impl FromStr for StorySelector {
     fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
         let story = raw_story_name.to_ascii_lowercase();
 
+        if story == "kitchen_sink" {
+            return Ok(Self::KitchenSink);
+        }
+
         if let Some((_, story)) = story.split_once("elements/") {
             let element_story = ElementStory::from_str(story)
                 .with_context(|| format!("story not found for element '{story}'"))?;
@@ -50,25 +122,49 @@ impl FromStr for StorySelector {
     }
 }
 
+impl StorySelector {
+    pub fn story<V: 'static>(&self) -> Vec<AnyElement<V>> {
+        match self {
+            Self::Element(element_story) => vec![element_story.story()],
+            Self::Component(component_story) => vec![component_story.story()],
+            Self::KitchenSink => all_story_selectors()
+                .into_iter()
+                // Exclude the kitchen sink to prevent `story` from recursively
+                // calling itself for all eternity.
+                .filter(|selector| **selector != Self::KitchenSink)
+                .flat_map(|selector| selector.story())
+                .collect(),
+        }
+    }
+}
+
 /// The list of all stories available in the storybook.
-static ALL_STORIES: OnceLock<Vec<StorySelector>> = OnceLock::new();
+static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
 
-impl ValueEnum for StorySelector {
-    fn value_variants<'a>() -> &'a [Self] {
-        let stories = ALL_STORIES.get_or_init(|| {
-            let element_stories = ElementStory::iter().map(Self::Element);
-            let component_stories = ComponentStory::iter().map(Self::Component);
+fn all_story_selectors<'a>() -> &'a [StorySelector] {
+    let stories = ALL_STORY_SELECTORS.get_or_init(|| {
+        let element_stories = ElementStory::iter().map(StorySelector::Element);
+        let component_stories = ComponentStory::iter().map(StorySelector::Component);
+
+        element_stories
+            .chain(component_stories)
+            .chain(std::iter::once(StorySelector::KitchenSink))
+            .collect::<Vec<_>>()
+    });
 
-            element_stories.chain(component_stories).collect::<Vec<_>>()
-        });
+    stories
+}
 
-        stories
+impl ValueEnum for StorySelector {
+    fn value_variants<'a>() -> &'a [Self] {
+        all_story_selectors()
     }
 
     fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
         let value = match self {
             Self::Element(story) => format!("elements/{story}"),
             Self::Component(story) => format!("components/{story}"),
+            Self::KitchenSink => "kitchen_sink".to_string(),
         };
 
         Some(PossibleValue::new(value))

crates/storybook/src/storybook.rs 🔗

@@ -1,26 +1,24 @@
 #![allow(dead_code, unused_variables)]
 
-mod collab_panel;
 mod stories;
 mod story;
 mod story_selector;
-mod workspace;
+
+use std::sync::Arc;
 
 use ::theme as legacy_theme;
 use clap::Parser;
-use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds};
-use legacy_theme::ThemeSettings;
+use gpui2::{
+    serde_json, vec2f, view, Element, IntoElement, ParentElement, RectF, ViewContext, WindowBounds,
+};
+use legacy_theme::{ThemeRegistry, ThemeSettings};
 use log::LevelFilter;
 use settings::{default_settings, SettingsStore};
 use simplelog::SimpleLogger;
-use stories::components::breadcrumb::BreadcrumbStory;
-use stories::components::facepile::FacepileStory;
-use stories::components::toolbar::ToolbarStory;
-use stories::components::traffic_lights::TrafficLightsStory;
-use stories::elements::avatar::AvatarStory;
-use ui::{ElementExt, Theme};
+use ui::prelude::*;
+use ui::{ElementExt, Theme, WorkspaceElement};
 
-use crate::story_selector::{ComponentStory, ElementStory, StorySelector};
+use crate::story_selector::StorySelector;
 
 gpui2::actions! {
     storybook,
@@ -32,6 +30,12 @@ gpui2::actions! {
 struct Args {
     #[arg(value_enum)]
     story: Option<StorySelector>,
+
+    /// The name of the theme to use in the storybook.
+    ///
+    /// If not provided, the default theme will be used.
+    #[arg(long)]
+    theme: Option<String>,
 }
 
 fn main() {
@@ -48,31 +52,60 @@ fn main() {
         legacy_theme::init(Assets, cx);
         // load_embedded_fonts(cx.platform().as_ref());
 
+        let theme_registry = cx.global::<Arc<ThemeRegistry>>();
+
+        let theme_override = args
+            .theme
+            .and_then(|theme| {
+                theme_registry
+                    .list_names(true)
+                    .find(|known_theme| theme == *known_theme)
+            })
+            .and_then(|theme_name| theme_registry.get(&theme_name).ok());
+
         cx.add_window(
             gpui2::WindowOptions {
-                bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1600., 900.))),
+                bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))),
                 center: true,
                 ..Default::default()
             },
             |cx| match args.story {
-                Some(StorySelector::Element(ElementStory::Avatar)) => {
-                    view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default()))
-                }
-                Some(StorySelector::Component(ComponentStory::Breadcrumb)) => {
-                    view(|cx| render_story(&mut ViewContext::new(cx), BreadcrumbStory::default()))
-                }
-                Some(StorySelector::Component(ComponentStory::Facepile)) => {
-                    view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default()))
-                }
-                Some(StorySelector::Component(ComponentStory::Toolbar)) => {
-                    view(|cx| render_story(&mut ViewContext::new(cx), ToolbarStory::default()))
-                }
-                Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| {
-                    render_story(&mut ViewContext::new(cx), TrafficLightsStory::default())
+                // HACK: Special-case the kitchen sink to fix scrolling.
+                // There is something about going through `children_any` that messes
+                // with the scroll interactions.
+                Some(StorySelector::KitchenSink) => view(move |cx| {
+                    render_story(
+                        &mut ViewContext::new(cx),
+                        theme_override.clone(),
+                        crate::stories::kitchen_sink::KitchenSinkStory::default(),
+                    )
                 }),
-                None => {
-                    view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default()))
+                // HACK: Special-case the panel story to fix scrolling.
+                // There is something about going through `children_any` that messes
+                // with the scroll interactions.
+                Some(StorySelector::Component(story_selector::ComponentStory::Panel)) => {
+                    view(move |cx| {
+                        render_story(
+                            &mut ViewContext::new(cx),
+                            theme_override.clone(),
+                            crate::stories::components::panel::PanelStory::default(),
+                        )
+                    })
                 }
+                Some(selector) => view(move |cx| {
+                    render_story(
+                        &mut ViewContext::new(cx),
+                        theme_override.clone(),
+                        div().children_any(selector.story()),
+                    )
+                }),
+                None => view(move |cx| {
+                    render_story(
+                        &mut ViewContext::new(cx),
+                        theme_override.clone(),
+                        WorkspaceElement::default(),
+                    )
+                }),
             },
         );
         cx.platform().activate(true);
@@ -81,23 +114,32 @@ fn main() {
 
 fn render_story<V: 'static, S: IntoElement<V>>(
     cx: &mut ViewContext<V>,
+    theme_override: Option<Arc<legacy_theme::Theme>>,
     story: S,
 ) -> impl Element<V> {
-    story.into_element().themed(current_theme(cx))
+    let theme = current_theme(cx, theme_override);
+
+    story.into_element().themed(theme)
+}
+
+fn current_theme<V: 'static>(
+    cx: &mut ViewContext<V>,
+    theme_override: Option<Arc<legacy_theme::Theme>>,
+) -> Theme {
+    let legacy_theme =
+        theme_override.unwrap_or_else(|| settings::get::<ThemeSettings>(cx).theme.clone());
+
+    let new_theme: Theme = serde_json::from_value(legacy_theme.base_theme.clone()).unwrap();
+
+    add_base_theme_to_legacy_theme(&legacy_theme, new_theme)
 }
 
 // Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct.
-fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
-    settings::get::<ThemeSettings>(cx)
-        .theme
+fn add_base_theme_to_legacy_theme(legacy_theme: &legacy_theme::Theme, new_theme: Theme) -> Theme {
+    legacy_theme
         .deserialized_base_theme
         .lock()
-        .get_or_insert_with(|| {
-            let theme: Theme =
-                serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
-                    .unwrap();
-            Box::new(theme)
-        })
+        .get_or_insert_with(|| Box::new(new_theme))
         .downcast_ref::<Theme>()
         .unwrap()
         .clone()
@@ -106,7 +148,6 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
 use anyhow::{anyhow, Result};
 use gpui2::AssetSource;
 use rust_embed::RustEmbed;
-use workspace::WorkspaceElement;
 
 #[derive(RustEmbed)]
 #[folder = "../../assets"]

crates/storybook/src/workspace.rs 🔗

@@ -1,56 +0,0 @@
-use gpui2::{
-    elements::{div, div::ScrollState},
-    style::StyleHelpers,
-    Element, IntoElement, ParentElement, ViewContext,
-};
-use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar, toolbar};
-
-#[derive(Element, Default)]
-pub struct WorkspaceElement {
-    left_scroll_state: ScrollState,
-    right_scroll_state: ScrollState,
-    tab_bar_scroll_state: ScrollState,
-}
-
-impl WorkspaceElement {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        div()
-            .size_full()
-            .flex()
-            .flex_col()
-            .font("Zed Sans Extended")
-            .gap_0()
-            .justify_start()
-            .items_start()
-            .text_color(theme.lowest.base.default.foreground)
-            .fill(theme.lowest.base.default.background)
-            .child(title_bar())
-            .child(
-                div()
-                    .flex_1()
-                    .w_full()
-                    .flex()
-                    .flex_row()
-                    .overflow_hidden()
-                    .child(project_panel(self.left_scroll_state.clone()))
-                    .child(
-                        div()
-                            .h_full()
-                            .flex_1()
-                            .fill(theme.highest.base.default.background)
-                            .child(
-                                div()
-                                    .flex()
-                                    .flex_col()
-                                    .flex_1()
-                                    .child(tab_bar(self.tab_bar_scroll_state.clone()))
-                                    .child(toolbar()),
-                            ),
-                    )
-                    .child(chat_panel(self.right_scroll_state.clone())),
-            )
-            .child(status_bar())
-    }
-}

crates/ui/Cargo.toml 🔗

@@ -6,7 +6,10 @@ publish = false
 
 [dependencies]
 anyhow.workspace = true
+chrono = "0.4"
 gpui2 = { path = "../gpui2" }
 serde.workspace = true
 settings = { path = "../settings" }
+smallvec.workspace = true
+strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme" }

crates/ui/docs/_project.md 🔗

@@ -0,0 +1,13 @@
+## Project Plan
+
+- Port existing UI to GPUI2
+- Update UI in places that GPUI1 was limiting us*
+- Understand the needs &/|| struggles the engineers have been having with building UI in the past and address as many of those as possible as we go
+- Ship a simple, straightforward system with documentation that is easy to use to build UI
+
+## Component Classification
+
+To simplify the understanding of components and minimize unnecessary cognitive load, let's categorize components into two types:
+
+- An element refers to a standalone component that doesn't import any other 'ui' components.
+- A component indicates a component that utilizes or imports other 'ui' components.

crates/ui/src/children.rs 🔗

@@ -0,0 +1,7 @@
+use std::any::Any;
+
+use gpui2::{AnyElement, ViewContext};
+
+pub type HackyChildren<V> = fn(&mut ViewContext<V>, &dyn Any) -> Vec<AnyElement<V>>;
+
+pub type HackyChildrenPayload = Box<dyn Any>;

crates/ui/src/components.rs 🔗

@@ -1,142 +1,153 @@
+mod assistant_panel;
 mod breadcrumb;
+mod buffer;
 mod chat_panel;
 mod collab_panel;
 mod command_palette;
+mod context_menu;
+mod editor;
 mod facepile;
-mod follow_group;
 mod icon_button;
+mod keybinding;
 mod list;
-mod list_item;
-mod list_section_header;
 mod palette;
-mod palette_item;
+mod panel;
+mod panes;
+mod player_stack;
 mod project_panel;
 mod status_bar;
 mod tab;
 mod tab_bar;
+mod terminal;
 mod title_bar;
 mod toolbar;
 mod traffic_lights;
 mod workspace;
 
+pub use assistant_panel::*;
 pub use breadcrumb::*;
+pub use buffer::*;
 pub use chat_panel::*;
 pub use collab_panel::*;
 pub use command_palette::*;
+pub use context_menu::*;
+pub use editor::*;
 pub use facepile::*;
-pub use follow_group::*;
 pub use icon_button::*;
+pub use keybinding::*;
 pub use list::*;
-pub use list_item::*;
-pub use list_section_header::*;
 pub use palette::*;
-pub use palette_item::*;
+pub use panel::*;
+pub use panes::*;
+pub use player_stack::*;
 pub use project_panel::*;
 pub use status_bar::*;
 pub use tab::*;
 pub use tab_bar::*;
+pub use terminal::*;
 pub use title_bar::*;
 pub use toolbar::*;
 pub use traffic_lights::*;
 pub use workspace::*;
 
-use std::marker::PhantomData;
-use std::rc::Rc;
-
-use gpui2::elements::div;
-use gpui2::interactive::Interactive;
-use gpui2::platform::MouseButton;
-use gpui2::style::StyleHelpers;
-use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext};
-
-struct ButtonHandlers<V, D> {
-    click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
-}
-
-impl<V, D> Default for ButtonHandlers<V, D> {
-    fn default() -> Self {
-        Self { click: None }
-    }
-}
-
-#[derive(Element)]
-pub struct Button<V: 'static, D: 'static> {
-    handlers: ButtonHandlers<V, D>,
-    label: Option<ArcCow<'static, str>>,
-    icon: Option<ArcCow<'static, str>>,
-    data: Rc<D>,
-    view_type: PhantomData<V>,
-}
-
-// Impl block for buttons without data.
-// See below for an impl block for any button.
-impl<V: 'static> Button<V, ()> {
-    fn new() -> Self {
-        Self {
-            handlers: ButtonHandlers::default(),
-            label: None,
-            icon: None,
-            data: Rc::new(()),
-            view_type: PhantomData,
-        }
-    }
-
-    pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
-        Button {
-            handlers: ButtonHandlers::default(),
-            label: self.label,
-            icon: self.icon,
-            data: Rc::new(data),
-            view_type: PhantomData,
-        }
-    }
-}
-
-// Impl block for button regardless of its data type.
-impl<V: 'static, D: 'static> Button<V, D> {
-    pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
-        self.label = Some(label.into());
-        self
-    }
-
-    pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
-        self.icon = Some(icon.into());
-        self
-    }
-
-    pub fn on_click(
-        mut self,
-        handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
-    ) -> Self {
-        self.handlers.click = Some(Rc::new(handler));
-        self
-    }
-}
-
-pub fn button<V>() -> Button<V, ()> {
-    Button::new()
-}
-
-impl<V: 'static, D: 'static> Button<V, D> {
-    fn render(
-        &mut self,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> impl IntoElement<V> + Interactive<V> {
-        // let colors = &cx.theme::<Theme>().colors;
-
-        let button = div()
-            // .fill(colors.error(0.5))
-            .h_4()
-            .children(self.label.clone());
-
-        if let Some(handler) = self.handlers.click.clone() {
-            let data = self.data.clone();
-            button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
-                handler(view, data.as_ref(), cx)
-            })
-        } else {
-            button
-        }
-    }
-}
+// Nate: Commenting this out for now, unsure if we need it.
+
+// use std::marker::PhantomData;
+// use std::rc::Rc;
+
+// use gpui2::elements::div;
+// use gpui2::interactive::Interactive;
+// use gpui2::platform::MouseButton;
+// use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext};
+
+// struct ButtonHandlers<V, D> {
+//     click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
+// }
+
+// impl<V, D> Default for ButtonHandlers<V, D> {
+//     fn default() -> Self {
+//         Self { click: None }
+//     }
+// }
+
+// #[derive(Element)]
+// pub struct Button<V: 'static, D: 'static> {
+//     handlers: ButtonHandlers<V, D>,
+//     label: Option<ArcCow<'static, str>>,
+//     icon: Option<ArcCow<'static, str>>,
+//     data: Rc<D>,
+//     view_type: PhantomData<V>,
+// }
+
+// // Impl block for buttons without data.
+// // See below for an impl block for any button.
+// impl<V: 'static> Button<V, ()> {
+//     fn new() -> Self {
+//         Self {
+//             handlers: ButtonHandlers::default(),
+//             label: None,
+//             icon: None,
+//             data: Rc::new(()),
+//             view_type: PhantomData,
+//         }
+//     }
+
+//     pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+//         Button {
+//             handlers: ButtonHandlers::default(),
+//             label: self.label,
+//             icon: self.icon,
+//             data: Rc::new(data),
+//             view_type: PhantomData,
+//         }
+//     }
+// }
+
+// // Impl block for button regardless of its data type.
+// impl<V: 'static, D: 'static> Button<V, D> {
+//     pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+//         self.label = Some(label.into());
+//         self
+//     }
+
+//     pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+//         self.icon = Some(icon.into());
+//         self
+//     }
+
+//     pub fn on_click(
+//         mut self,
+//         handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
+//     ) -> Self {
+//         self.handlers.click = Some(Rc::new(handler));
+//         self
+//     }
+// }
+
+// pub fn button<V>() -> Button<V, ()> {
+//     Button::new()
+// }
+
+// impl<V: 'static, D: 'static> Button<V, D> {
+//     fn render(
+//         &mut self,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> impl IntoElement<V> + Interactive<V> {
+//         // let colors = &cx.theme::<Theme>().colors;
+
+//         let button = div()
+//             // .fill(colors.error(0.5))
+//             .h_4()
+//             .children(self.label.clone());
+
+//         if let Some(handler) = self.handlers.click.clone() {
+//             let data = self.data.clone();
+//             button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+//                 handler(view, data.as_ref(), cx)
+//             })
+//         } else {
+//             button
+//         }
+//     }
+// }

crates/ui/src/components/assistant_panel.rs 🔗

@@ -0,0 +1,91 @@
+use std::marker::PhantomData;
+
+use gpui2::geometry::rems;
+
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{Icon, IconButton, Label, Panel, PanelSide};
+
+#[derive(Element)]
+pub struct AssistantPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    current_side: PanelSide,
+}
+
+impl<V: 'static> AssistantPanel<V> {
+    pub fn new() -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state: ScrollState::default(),
+            current_side: PanelSide::default(),
+        }
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        self.current_side = side;
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        struct PanelPayload {
+            pub scroll_state: ScrollState,
+        }
+
+        Panel::new(
+            self.scroll_state.clone(),
+            |_, payload| {
+                let payload = payload.downcast_ref::<PanelPayload>().unwrap();
+
+                vec![div()
+                    .flex()
+                    .flex_col()
+                    .h_full()
+                    .px_2()
+                    .gap_2()
+                    // Header
+                    .child(
+                        div()
+                            .flex()
+                            .justify_between()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .flex()
+                                    .child(IconButton::new(Icon::Menu))
+                                    .child(Label::new("New Conversation")),
+                            )
+                            .child(
+                                div()
+                                    .flex()
+                                    .items_center()
+                                    .gap_px()
+                                    .child(IconButton::new(Icon::SplitMessage))
+                                    .child(IconButton::new(Icon::Quote))
+                                    .child(IconButton::new(Icon::MagicWand))
+                                    .child(IconButton::new(Icon::Plus))
+                                    .child(IconButton::new(Icon::Maximize)),
+                            ),
+                    )
+                    // Chat Body
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_col()
+                            .gap_3()
+                            .overflow_y_scroll(payload.scroll_state.clone())
+                            .child(Label::new("Is this thing on?")),
+                    )
+                    .into_any()]
+            },
+            Box::new(PanelPayload {
+                scroll_state: self.scroll_state.clone(),
+            }),
+        )
+        .side(self.current_side)
+        .width(rems(32.))
+    }
+}

crates/ui/src/components/breadcrumb.rs 🔗

@@ -1,24 +1,19 @@
-use gpui2::elements::div;
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::theme;
+use crate::prelude::*;
+use crate::{h_stack, theme};
 
 #[derive(Element)]
 pub struct Breadcrumb {}
 
-pub fn breadcrumb() -> Breadcrumb {
-    Breadcrumb {}
-}
-
 impl Breadcrumb {
+    pub fn new() -> Self {
+        Self {}
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        div()
+        h_stack()
             .px_1()
-            .flex()
-            .flex_row()
             // TODO: Read font from theme (or settings?).
             .font("Zed Mono Extended")
             .text_sm()

crates/ui/src/components/buffer.rs 🔗

@@ -0,0 +1,229 @@
+use std::marker::PhantomData;
+
+use gpui2::{Hsla, WindowContext};
+
+use crate::prelude::*;
+use crate::{h_stack, theme, v_stack, Icon, IconElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub struct PlayerCursor {
+    color: Hsla,
+    index: usize,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct HighlightedText {
+    pub text: String,
+    pub color: Hsla,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct HighlightedLine {
+    pub highlighted_texts: Vec<HighlightedText>,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct BufferRow {
+    pub line_number: usize,
+    pub code_action: bool,
+    pub current: bool,
+    pub line: Option<HighlightedLine>,
+    pub cursors: Option<Vec<PlayerCursor>>,
+    pub status: GitStatus,
+    pub show_line_number: bool,
+}
+
+pub struct BufferRows {
+    pub show_line_numbers: bool,
+    pub rows: Vec<BufferRow>,
+}
+
+impl Default for BufferRows {
+    fn default() -> Self {
+        Self {
+            show_line_numbers: true,
+            rows: vec![BufferRow {
+                line_number: 1,
+                code_action: false,
+                current: true,
+                line: None,
+                cursors: None,
+                status: GitStatus::None,
+                show_line_number: true,
+            }],
+        }
+    }
+}
+
+impl BufferRow {
+    pub fn new(line_number: usize) -> Self {
+        Self {
+            line_number,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number: true,
+        }
+    }
+
+    pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
+        self.line = line;
+        self
+    }
+
+    pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
+        self.cursors = cursors;
+        self
+    }
+
+    pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
+        if let Some(cursors) = &mut self.cursors {
+            cursors.push(cursor);
+        } else {
+            self.cursors = Some(vec![cursor]);
+        }
+        self
+    }
+
+    pub fn set_status(mut self, status: GitStatus) -> Self {
+        self.status = status;
+        self
+    }
+
+    pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
+        self.show_line_number = show_line_number;
+        self
+    }
+
+    pub fn set_code_action(mut self, code_action: bool) -> Self {
+        self.code_action = code_action;
+        self
+    }
+
+    pub fn set_current(mut self, current: bool) -> Self {
+        self.current = current;
+        self
+    }
+}
+
+#[derive(Element)]
+pub struct Buffer<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    rows: Option<BufferRows>,
+    readonly: bool,
+    language: Option<String>,
+    title: Option<String>,
+    path: Option<String>,
+}
+
+impl<V: 'static> Buffer<V> {
+    pub fn new() -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state: ScrollState::default(),
+            rows: Some(BufferRows::default()),
+            readonly: false,
+            language: None,
+            title: Some("untitled".to_string()),
+            path: None,
+        }
+    }
+
+    pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
+        self.scroll_state = scroll_state;
+    }
+
+    pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
+        self.title = title.into();
+        self
+    }
+
+    pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
+        self.path = path.into();
+        self
+    }
+
+    pub fn set_readonly(mut self, readonly: bool) -> Self {
+        self.readonly = readonly;
+        self
+    }
+
+    pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
+        self.rows = rows.into();
+        self
+    }
+
+    pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
+        self.language = language.into();
+        self
+    }
+
+    fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        let line_background = if row.current {
+            theme.middle.base.default.background
+        } else {
+            system_color.transparent
+        };
+
+        let line_number_color = if row.current {
+            HighlightColor::Default.hsla(cx)
+        } else {
+            HighlightColor::Comment.hsla(cx)
+        };
+
+        h_stack()
+            .fill(line_background)
+            .gap_2()
+            .px_2()
+            .child(h_stack().w_4().h_full().px_1().when(row.code_action, |c| {
+                div().child(IconElement::new(Icon::Bolt))
+            }))
+            .when(row.show_line_number, |this| {
+                this.child(
+                    h_stack().justify_end().px_1().w_4().child(
+                        div()
+                            .text_color(line_number_color)
+                            .child(row.line_number.to_string()),
+                    ),
+                )
+            })
+            .child(div().mx_1().w_1().h_full().fill(row.status.hsla(cx)))
+            .children(row.line.map(|line| {
+                div()
+                    .flex()
+                    .children(line.highlighted_texts.iter().map(|highlighted_text| {
+                        div()
+                            .text_color(highlighted_text.color)
+                            .child(highlighted_text.text.clone())
+                    }))
+            }))
+    }
+
+    fn render_rows(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
+        match &self.rows {
+            Some(rows) => rows
+                .rows
+                .iter()
+                .map(|row| Self::render_row(row.clone(), cx))
+                .collect(),
+            None => vec![],
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let rows = self.render_rows(cx);
+        v_stack()
+            .flex_1()
+            .w_full()
+            .h_full()
+            .fill(theme.highest.base.default.background)
+            .children(rows)
+    }
+}

crates/ui/src/components/chat_panel.rs 🔗

@@ -1,66 +1,127 @@
 use std::marker::PhantomData;
 
-use gpui2::elements::div::ScrollState;
-use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use chrono::NaiveDateTime;
 
+use crate::prelude::*;
 use crate::theme::theme;
-use crate::{icon_button, IconAsset};
+use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide};
 
 #[derive(Element)]
 pub struct ChatPanel<V: 'static> {
     view_type: PhantomData<V>,
     scroll_state: ScrollState,
+    current_side: PanelSide,
+    messages: Vec<ChatMessage>,
 }
 
-pub fn chat_panel<V: 'static>(scroll_state: ScrollState) -> ChatPanel<V> {
-    ChatPanel {
-        view_type: PhantomData,
-        scroll_state,
+impl<V: 'static> ChatPanel<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            current_side: PanelSide::default(),
+            messages: Vec::new(),
+        }
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        self.current_side = side;
+        self
+    }
+
+    pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
+        self.messages = messages;
+        self
     }
-}
 
-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()
+        struct PanelPayload {
+            pub scroll_state: ScrollState,
+            pub messages: Vec<ChatMessage>,
+        }
+
+        Panel::new(
+            self.scroll_state.clone(),
+            |_, payload| {
+                let payload = payload.downcast_ref::<PanelPayload>().unwrap();
+
+                vec![div()
                     .flex()
                     .flex_col()
-                    .overflow_y_scroll(self.scroll_state.clone())
-                    .child("body"),
-            )
-            // Composer
-            .child(
-                div()
+                    .h_full()
                     .px_2()
-                    .flex()
                     .gap_2()
-                    // Nav Buttons
+                    // Header
+                    .child(
+                        div()
+                            .flex()
+                            .justify_between()
+                            .gap_2()
+                            .child(div().flex().child(Label::new("#design")))
+                            .child(
+                                div()
+                                    .flex()
+                                    .items_center()
+                                    .gap_px()
+                                    .child(IconButton::new(Icon::File))
+                                    .child(IconButton::new(Icon::AudioOn)),
+                            ),
+                    )
+                    // Chat Body
                     .child(
                         div()
+                            .w_full()
                             .flex()
-                            .items_center()
-                            .gap_px()
-                            .child(icon_button().icon(IconAsset::Plus))
-                            .child(icon_button().icon(IconAsset::Split)),
+                            .flex_col()
+                            .gap_3()
+                            .overflow_y_scroll(payload.scroll_state.clone())
+                            .children(payload.messages.clone()),
+                    )
+                    // Composer
+                    .child(div().flex().gap_2().child(Input::new("Message #design")))
+                    .into_any()]
+            },
+            Box::new(PanelPayload {
+                scroll_state: self.scroll_state.clone(),
+                messages: self.messages.clone(),
+            }),
+        )
+        .side(self.current_side)
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct ChatMessage {
+    author: String,
+    text: String,
+    sent_at: NaiveDateTime,
+}
+
+impl ChatMessage {
+    pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
+        Self {
+            author,
+            text,
+            sent_at,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div()
+            .flex()
+            .flex_col()
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .child(Label::new(self.author.clone()))
+                    .child(
+                        Label::new(self.sent_at.format("%m/%d/%Y").to_string())
+                            .color(LabelColor::Muted),
                     ),
             )
+            .child(div().child(Label::new(self.text.clone())))
     }
 }

crates/ui/src/components/collab_panel.rs 🔗

@@ -1,101 +1,85 @@
+use std::marker::PhantomData;
+
+use gpui2::elements::{img, svg};
+use gpui2::ArcCow;
+
+use crate::prelude::*;
 use crate::theme::{theme, Theme};
-use gpui2::{
-    elements::{div, div::ScrollState, img, svg},
-    style::{StyleHelpers, Styleable},
-    ArcCow, Element, IntoElement, ParentElement, ViewContext,
+use crate::{
+    static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List,
+    ListHeader, ToggleState,
 };
-use std::marker::PhantomData;
 
 #[derive(Element)]
-pub struct CollabPanelElement<V: 'static> {
+pub struct CollabPanel<V: 'static> {
     view_type: PhantomData<V>,
     scroll_state: ScrollState,
 }
 
-// When I improve child view rendering, I'd like to have V implement a trait  that
-// provides the scroll state, among other things.
-pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
-    CollabPanelElement {
-        view_type: PhantomData,
-        scroll_state,
+impl<V: 'static> CollabPanel<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
     }
-}
 
-impl<V: 'static> CollabPanelElement<V> {
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        // Panel
-        div()
+        v_stack()
             .w_64()
             .h_full()
-            .flex()
-            .flex_col()
-            .font("Zed Sans Extended")
-            .text_color(theme.middle.base.default.foreground)
-            .border_color(theme.middle.base.default.border)
-            .border()
             .fill(theme.middle.base.default.background)
             .child(
-                div()
+                v_stack()
                     .w_full()
-                    .flex()
-                    .flex_col()
                     .overflow_y_scroll(self.scroll_state.clone())
-                    // List Container
                     .child(
                         div()
                             .fill(theme.lowest.base.default.background)
                             .pb_1()
                             .border_color(theme.lowest.base.default.border)
                             .border_b()
-                            //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
-                            // .group()
-                            // List Section Header
-                            .child(self.list_section_header("#CRDB", true, &theme))
-                            // List Item Large
-                            .child(self.list_item(
-                                "http://github.com/maxbrunsfeld.png?s=50",
-                                "maxbrunsfeld",
-                                &theme,
-                            )),
+                            .child(
+                                List::new(static_collab_panel_current_call())
+                                    .header(
+                                        ListHeader::new("CRDB")
+                                            .left_icon(Icon::Hash.into())
+                                            .set_toggle(ToggleState::Toggled),
+                                    )
+                                    .set_toggle(ToggleState::Toggled),
+                            ),
                     )
                     .child(
-                        div()
-                            .py_2()
-                            .flex()
-                            .flex_col()
-                            .child(self.list_section_header("CHANNELS", true, &theme)),
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_channels())
+                                .header(
+                                    ListHeader::new("CHANNELS").set_toggle(ToggleState::Toggled),
+                                )
+                                .empty_message("No channels yet. Add a channel to get started.")
+                                .set_toggle(ToggleState::Toggled),
+                        ),
                     )
                     .child(
-                        div()
-                            .py_2()
-                            .flex()
-                            .flex_col()
-                            .child(self.list_section_header("CONTACTS", true, &theme))
-                            .children(
-                                std::iter::repeat_with(|| {
-                                    vec![
-                                        self.list_item(
-                                            "http://github.com/as-cii.png?s=50",
-                                            "as-cii",
-                                            &theme,
-                                        ),
-                                        self.list_item(
-                                            "http://github.com/nathansobo.png?s=50",
-                                            "nathansobo",
-                                            &theme,
-                                        ),
-                                        self.list_item(
-                                            "http://github.com/maxbrunsfeld.png?s=50",
-                                            "maxbrunsfeld",
-                                            &theme,
-                                        ),
-                                    ]
-                                })
-                                .take(3)
-                                .flatten(),
-                            ),
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – ONLINE")
+                                        .set_toggle(ToggleState::Toggled),
+                                )
+                                .set_toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – OFFLINE")
+                                        .set_toggle(ToggleState::NotToggled),
+                                )
+                                .set_toggle(ToggleState::NotToggled),
+                        ),
                     ),
             )
             .child(

crates/ui/src/components/command_palette.rs 🔗

@@ -1,9 +1,7 @@
-use gpui2::elements::div;
-use gpui2::{elements::div::ScrollState, ViewContext};
-use gpui2::{Element, IntoElement, ParentElement};
 use std::marker::PhantomData;
 
-use crate::{example_editor_actions, palette, OrderMethod};
+use crate::prelude::*;
+use crate::{example_editor_actions, OrderMethod, Palette};
 
 #[derive(Element)]
 pub struct CommandPalette<V: 'static> {
@@ -11,17 +9,17 @@ pub struct CommandPalette<V: 'static> {
     scroll_state: ScrollState,
 }
 
-pub fn command_palette<V: 'static>(scroll_state: ScrollState) -> CommandPalette<V> {
-    CommandPalette {
-        view_type: PhantomData,
-        scroll_state,
+impl<V: 'static> CommandPalette<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
     }
-}
 
-impl<V: 'static> CommandPalette<V> {
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         div().child(
-            palette(self.scroll_state.clone())
+            Palette::new(self.scroll_state.clone())
                 .items(example_editor_actions())
                 .placeholder("Execute a command...")
                 .empty_string("No items found.")

crates/ui/src/components/context_menu.rs 🔗

@@ -0,0 +1,65 @@
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{
+    v_stack, Label, List, ListEntry, ListItem, ListItemVariant, ListSeparator, ListSubHeader,
+};
+
+#[derive(Clone)]
+pub enum ContextMenuItem {
+    Header(&'static str),
+    Entry(Label),
+    Separator,
+}
+
+impl ContextMenuItem {
+    fn to_list_item(self) -> ListItem {
+        match self {
+            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
+            ContextMenuItem::Entry(label) => {
+                ListEntry::new(label).variant(ListItemVariant::Inset).into()
+            }
+            ContextMenuItem::Separator => ListSeparator::new().into(),
+        }
+    }
+    pub fn header(label: &'static str) -> Self {
+        Self::Header(label)
+    }
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+    pub fn entry(label: Label) -> Self {
+        Self::Entry(label)
+    }
+}
+
+#[derive(Element)]
+pub struct ContextMenu {
+    items: Vec<ContextMenuItem>,
+}
+
+impl ContextMenu {
+    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
+        Self {
+            items: items.into_iter().collect(),
+        }
+    }
+    fn render<V: 'static>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        v_stack()
+            .flex()
+            .fill(theme.lowest.base.default.background)
+            .border()
+            .border_color(theme.lowest.base.default.border)
+            .child(
+                List::new(
+                    self.items
+                        .clone()
+                        .into_iter()
+                        .map(ContextMenuItem::to_list_item)
+                        .collect(),
+                )
+                .set_toggle(ToggleState::Toggled),
+            )
+        //div().p_1().children(self.items.clone())
+    }
+}

crates/ui/src/components/editor.rs 🔗

@@ -0,0 +1,25 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{Buffer, Toolbar};
+
+#[derive(Element)]
+struct Editor<V: 'static> {
+    view_type: PhantomData<V>,
+    toolbar: Toolbar,
+    buffer: Buffer<V>,
+}
+
+impl<V: 'static> Editor<V> {
+    pub fn new(toolbar: Toolbar, buffer: Buffer<V>) -> Self {
+        Self {
+            view_type: PhantomData,
+            toolbar,
+            buffer,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(self.toolbar.clone())
+    }
+}

crates/ui/src/components/facepile.rs 🔗

@@ -1,29 +1,27 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::{theme, Avatar};
+use crate::prelude::*;
+use crate::{theme, Avatar, Player};
 
 #[derive(Element)]
 pub struct Facepile {
-    players: Vec<Avatar>,
+    players: Vec<Player>,
 }
 
-pub fn facepile<P: Iterator<Item = Avatar>>(players: P) -> Facepile {
-    Facepile {
-        players: players.collect(),
+impl Facepile {
+    pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
+        Self {
+            players: players.collect(),
+        }
     }
-}
 
-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())
+                .child(Avatar::new(player.avatar_src().to_string()))
         });
         div().p_1().flex().items_center().children(player_list)
     }

crates/ui/src/components/follow_group.rs 🔗

@@ -1,52 +0,0 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::{facepile, indicator, theme, Avatar};
-
-#[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().into_iter())),
-            )
-    }
-}

crates/ui/src/components/icon_button.rs 🔗

@@ -1,29 +1,16 @@
-use gpui2::elements::div;
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::{icon, theme, IconColor};
-use crate::{prelude::*, IconAsset};
+use crate::prelude::*;
+use crate::{theme, Icon, IconColor, IconElement};
 
 #[derive(Element)]
 pub struct IconButton {
-    icon: IconAsset,
+    icon: Icon,
     color: IconColor,
     variant: ButtonVariant,
     state: InteractionState,
 }
 
-pub fn icon_button() -> IconButton {
-    IconButton {
-        icon: IconAsset::default(),
-        color: IconColor::default(),
-        variant: ButtonVariant::default(),
-        state: InteractionState::default(),
-    }
-}
-
 impl IconButton {
-    pub fn new(icon: IconAsset) -> Self {
+    pub fn new(icon: Icon) -> Self {
         Self {
             icon,
             color: IconColor::default(),
@@ -32,7 +19,7 @@ impl IconButton {
         }
     }
 
-    pub fn icon(mut self, icon: IconAsset) -> Self {
+    pub fn icon(mut self, icon: Icon) -> Self {
         self.icon = icon;
         self
     }
@@ -75,6 +62,6 @@ impl IconButton {
             .fill(theme.highest.base.hovered.background)
             .active()
             .fill(theme.highest.base.pressed.background)
-            .child(icon(self.icon).color(icon_color))
+            .child(IconElement::new(self.icon).color(icon_color))
     }
 }

crates/ui/src/components/keybinding.rs 🔗

@@ -0,0 +1,158 @@
+use std::collections::HashSet;
+
+use strum::{EnumIter, IntoEnumIterator};
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element, Clone)]
+pub struct Keybinding {
+    /// A keybinding consists of a key and a set of modifier keys.
+    /// More then one keybinding produces a chord.
+    ///
+    /// This should always contain at least one element.
+    keybinding: Vec<(String, ModifierKeys)>,
+}
+
+impl Keybinding {
+    pub fn new(key: String, modifiers: ModifierKeys) -> Self {
+        Self {
+            keybinding: vec![(key, modifiers)],
+        }
+    }
+
+    pub fn new_chord(
+        first_note: (String, ModifierKeys),
+        second_note: (String, ModifierKeys),
+    ) -> Self {
+        Self {
+            keybinding: vec![first_note, second_note],
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div()
+            .flex()
+            .gap_2()
+            .children(self.keybinding.iter().map(|(key, modifiers)| {
+                div()
+                    .flex()
+                    .gap_1()
+                    .children(ModifierKey::iter().filter_map(|modifier| {
+                        if modifiers.0.contains(&modifier) {
+                            Some(Key::new(modifier.glyph()))
+                        } else {
+                            None
+                        }
+                    }))
+                    .child(Key::new(key.clone()))
+            }))
+    }
+}
+
+#[derive(Element)]
+pub struct Key {
+    key: String,
+}
+
+impl Key {
+    pub fn new<K>(key: K) -> Self
+    where
+        K: Into<String>,
+    {
+        Self { key: key.into() }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .px_2()
+            .py_0()
+            .rounded_md()
+            .text_sm()
+            .text_color(theme.lowest.on.default.foreground)
+            .fill(theme.lowest.on.default.background)
+            .child(self.key.clone())
+    }
+}
+
+// NOTE: The order the modifier keys appear in this enum impacts the order in
+// which they are rendered in the UI.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum ModifierKey {
+    Control,
+    Alt,
+    Command,
+    Shift,
+}
+
+impl ModifierKey {
+    /// Returns the glyph for the [`ModifierKey`].
+    pub fn glyph(&self) -> char {
+        match self {
+            Self::Control => '^',
+            Self::Alt => '⎇',
+            Self::Command => '⌘',
+            Self::Shift => '⇧',
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct ModifierKeys(HashSet<ModifierKey>);
+
+impl ModifierKeys {
+    pub fn new() -> Self {
+        Self(HashSet::new())
+    }
+
+    pub fn all() -> Self {
+        Self(HashSet::from_iter(ModifierKey::iter()))
+    }
+
+    pub fn add(mut self, modifier: ModifierKey) -> Self {
+        self.0.insert(modifier);
+        self
+    }
+
+    pub fn control(mut self, control: bool) -> Self {
+        if control {
+            self.0.insert(ModifierKey::Control);
+        } else {
+            self.0.remove(&ModifierKey::Control);
+        }
+
+        self
+    }
+
+    pub fn alt(mut self, alt: bool) -> Self {
+        if alt {
+            self.0.insert(ModifierKey::Alt);
+        } else {
+            self.0.remove(&ModifierKey::Alt);
+        }
+
+        self
+    }
+
+    pub fn command(mut self, command: bool) -> Self {
+        if command {
+            self.0.insert(ModifierKey::Command);
+        } else {
+            self.0.remove(&ModifierKey::Command);
+        }
+
+        self
+    }
+
+    pub fn shift(mut self, shift: bool) -> Self {
+        if shift {
+            self.0.insert(ModifierKey::Shift);
+        } else {
+            self.0.remove(&ModifierKey::Shift);
+        }
+
+        self
+    }
+}

crates/ui/src/components/list.rs 🔗

@@ -1,36 +1,294 @@
-use crate::theme::theme;
-use crate::tokens::token;
-use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader};
-use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::elements::div::Div;
+use gpui2::{Hsla, WindowContext};
 
-#[derive(Element)]
-pub struct List {
-    header: Option<ListSectionHeader>,
-    items: Vec<ListItem>,
-    empty_message: &'static str,
-    toggle: Option<ToggleState>,
-    // footer: Option<ListSectionFooter>,
+use crate::prelude::*;
+use crate::{
+    h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor,
+    IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor,
+    ToggleState,
+};
+
+#[derive(Clone, Copy, Default, Debug, PartialEq)]
+pub enum ListItemVariant {
+    /// The list item extends to the far left and right of the list.
+    #[default]
+    FullWidth,
+    Inset,
 }
 
-pub fn list(items: Vec<ListItem>) -> List {
-    List {
-        header: None,
-        items,
-        empty_message: "No items",
-        toggle: None,
+#[derive(Element, Clone, Copy)]
+pub struct ListHeader {
+    label: &'static str,
+    left_icon: Option<Icon>,
+    variant: ListItemVariant,
+    state: InteractionState,
+    toggleable: Toggleable,
+}
+
+impl ListHeader {
+    pub fn new(label: &'static str) -> Self {
+        Self {
+            label,
+            left_icon: None,
+            variant: ListItemVariant::default(),
+            state: InteractionState::default(),
+            toggleable: Toggleable::default(),
+        }
+    }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggleable = toggle.into();
+        self
+    }
+
+    pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
+        self.toggleable = toggleable;
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    fn disclosure_control<V: 'static>(&self) -> Div<V> {
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        match (is_toggleable, is_toggled) {
+            (false, _) => div(),
+            (_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
+            (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
+        }
+    }
+
+    fn background_color(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self.state {
+            InteractionState::Hovered => theme.lowest.base.hovered.background,
+            InteractionState::Active => theme.lowest.base.pressed.background,
+            InteractionState::Enabled => theme.lowest.on.default.background,
+            _ => system_color.transparent,
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+        let system_color = SystemColor::new();
+        let background_color = self.background_color(cx);
+
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        let disclosure_control = self.disclosure_control();
+
+        h_stack()
+            .flex_1()
+            .w_full()
+            .fill(background_color)
+            .when(self.state == InteractionState::Focused, |this| {
+                this.border()
+                    .border_color(theme.lowest.accent.default.border)
+            })
+            .relative()
+            .py_1()
+            .child(
+                div()
+                    .h_6()
+                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    .flex()
+                    .flex_1()
+                    .w_full()
+                    .gap_1()
+                    .items_center()
+                    .justify_between()
+                    .child(
+                        div()
+                            .flex()
+                            .gap_1()
+                            .items_center()
+                            .children(self.left_icon.map(|i| {
+                                IconElement::new(i)
+                                    .color(IconColor::Muted)
+                                    .size(IconSize::Small)
+                            }))
+                            .child(
+                                Label::new(self.label.clone())
+                                    .color(LabelColor::Muted)
+                                    .size(LabelSize::Small),
+                            ),
+                    )
+                    .child(disclosure_control),
+            )
     }
 }
 
-impl List {
-    pub fn header(mut self, header: ListSectionHeader) -> Self {
-        self.header = Some(header);
+#[derive(Element, Clone, Copy)]
+pub struct ListSubHeader {
+    label: &'static str,
+    left_icon: Option<Icon>,
+    variant: ListItemVariant,
+}
+
+impl ListSubHeader {
+    pub fn new(label: &'static str) -> Self {
+        Self {
+            label,
+            left_icon: None,
+            variant: ListItemVariant::default(),
+        }
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
         self
     }
 
-    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
-        self.empty_message = empty_message;
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+
+        h_stack().flex_1().w_full().relative().py_1().child(
+            div()
+                .h_6()
+                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                .flex()
+                .flex_1()
+                .w_full()
+                .gap_1()
+                .items_center()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .gap_1()
+                        .items_center()
+                        .children(self.left_icon.map(|i| {
+                            IconElement::new(i)
+                                .color(IconColor::Muted)
+                                .size(IconSize::Small)
+                        }))
+                        .child(
+                            Label::new(self.label.clone())
+                                .color(LabelColor::Muted)
+                                .size(LabelSize::Small),
+                        ),
+                ),
+        )
+    }
+}
+
+#[derive(Clone)]
+pub enum LeftContent {
+    Icon(Icon),
+    Avatar(&'static str),
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum ListEntrySize {
+    #[default]
+    Small,
+    Medium,
+}
+
+#[derive(Clone, Element)]
+pub enum ListItem {
+    Entry(ListEntry),
+    Separator(ListSeparator),
+    Header(ListSubHeader),
+}
+
+impl From<ListEntry> for ListItem {
+    fn from(entry: ListEntry) -> Self {
+        Self::Entry(entry)
+    }
+}
+
+impl From<ListSeparator> for ListItem {
+    fn from(entry: ListSeparator) -> Self {
+        Self::Separator(entry)
+    }
+}
+
+impl From<ListSubHeader> for ListItem {
+    fn from(entry: ListSubHeader) -> Self {
+        Self::Header(entry)
+    }
+}
+
+impl ListItem {
+    fn render<V: 'static>(&mut self, v: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        match self {
+            ListItem::Entry(entry) => div().child(entry.render(v, cx)),
+            ListItem::Separator(separator) => div().child(separator.render(v, cx)),
+            ListItem::Header(header) => div().child(header.render(v, cx)),
+        }
+    }
+    pub fn new(label: Label) -> Self {
+        Self::Entry(ListEntry::new(label))
+    }
+    pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
+        if let Self::Entry(entry) = self {
+            Some(entry)
+        } else {
+            None
+        }
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct ListEntry {
+    disclosure_control_style: DisclosureControlVisibility,
+    indent_level: u32,
+    label: Label,
+    left_content: Option<LeftContent>,
+    variant: ListItemVariant,
+    size: ListEntrySize,
+    state: InteractionState,
+    toggle: Option<ToggleState>,
+}
+
+impl ListEntry {
+    pub fn new(label: Label) -> Self {
+        Self {
+            disclosure_control_style: DisclosureControlVisibility::default(),
+            indent_level: 0,
+            label,
+            variant: ListItemVariant::default(),
+            left_content: None,
+            size: ListEntrySize::default(),
+            state: InteractionState::default(),
+            toggle: None,
+        }
+    }
+    pub fn variant(mut self, variant: ListItemVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+    pub fn indent_level(mut self, indent_level: u32) -> Self {
+        self.indent_level = indent_level;
         self
     }
 
@@ -39,26 +297,216 @@ impl List {
         self
     }
 
+    pub fn left_content(mut self, left_content: LeftContent) -> Self {
+        self.left_content = Some(left_content);
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Icon) -> Self {
+        self.left_content = Some(LeftContent::Icon(left_icon));
+        self
+    }
+
+    pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
+        self.left_content = Some(LeftContent::Avatar(left_avatar));
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn size(mut self, size: ListEntrySize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn disclosure_control_style(
+        mut self,
+        disclosure_control_style: DisclosureControlVisibility,
+    ) -> Self {
+        self.disclosure_control_style = disclosure_control_style;
+        self
+    }
+
+    fn background_color(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self.state {
+            InteractionState::Hovered => theme.lowest.base.hovered.background,
+            InteractionState::Active => theme.lowest.base.pressed.background,
+            InteractionState::Enabled => theme.lowest.on.default.background,
+            _ => system_color.transparent,
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn disclosure_control<V: 'static>(
+        &mut self,
+        cx: &mut ViewContext<V>,
+    ) -> Option<impl IntoElement<V>> {
+        let theme = theme(cx);
+        let token = token();
+
+        let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
+            IconElement::new(Icon::ChevronDown)
+        } else {
+            IconElement::new(Icon::ChevronRight)
+        }
+        .color(IconColor::Muted)
+        .size(IconSize::Small);
+
+        match (self.toggle, self.disclosure_control_style) {
+            (Some(_), DisclosureControlVisibility::OnHover) => {
+                Some(div().absolute().neg_left_5().child(disclosure_control_icon))
+            }
+            (Some(_), DisclosureControlVisibility::Always) => {
+                Some(div().child(disclosure_control_icon))
+            }
+            (None, _) => None,
+        }
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
         let token = token();
+        let system_color = SystemColor::new();
+        let background_color = self.background_color(cx);
 
-        let disclosure_control = match self.toggle {
-            Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
-            Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
+        let left_content = match self.left_content {
+            Some(LeftContent::Icon(i)) => {
+                Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
+            }
+            Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
             None => None,
         };
 
+        let sized_item = match self.size {
+            ListEntrySize::Small => div().h_6(),
+            ListEntrySize::Medium => div().h_7(),
+        };
+
         div()
+            .fill(background_color)
+            .when(self.state == InteractionState::Focused, |this| {
+                this.border()
+                    .border_color(theme.lowest.accent.default.border)
+            })
+            .relative()
+            .py_1()
+            .child(
+                sized_item
+                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    // .ml(rems(0.75 * self.indent_level as f32))
+                    .children((0..self.indent_level).map(|_| {
+                        div()
+                            .w(token.list_indent_depth)
+                            .h_full()
+                            .flex()
+                            .justify_center()
+                            .child(h_stack().child(div().w_px().h_full()).child(
+                                div().w_px().h_full().fill(theme.middle.base.default.border),
+                            ))
+                    }))
+                    .flex()
+                    .gap_1()
+                    .items_center()
+                    .relative()
+                    .children(self.disclosure_control(cx))
+                    .children(left_content)
+                    .child(self.label.clone()),
+            )
+    }
+}
+
+#[derive(Clone, Default, Element)]
+pub struct ListSeparator;
+
+impl ListSeparator {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div().h_px().w_full().fill(theme.lowest.base.default.border)
+    }
+}
+
+#[derive(Element)]
+pub struct List {
+    items: Vec<ListItem>,
+    empty_message: &'static str,
+    header: Option<ListHeader>,
+    toggleable: Toggleable,
+}
+
+impl List {
+    pub fn new(items: Vec<ListItem>) -> Self {
+        Self {
+            items,
+            empty_message: "No items",
+            header: None,
+            toggleable: Toggleable::default(),
+        }
+    }
+
+    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
+        self.empty_message = empty_message;
+        self
+    }
+
+    pub fn header(mut self, header: ListHeader) -> Self {
+        self.header = Some(header);
+        self
+    }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggleable = toggle.into();
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        let disclosure_control = if is_toggleable {
+            IconElement::new(Icon::ChevronRight)
+        } else {
+            IconElement::new(Icon::ChevronDown)
+        };
+
+        let list_content = match (self.items.is_empty(), is_toggled) {
+            (_, false) => div(),
+            (false, _) => div().children(self.items.iter().cloned()),
+            (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
+        };
+
+        v_stack()
             .py_1()
-            .flex()
-            .flex_col()
-            .children(self.header.map(|h| h))
             .children(
-                self.items
-                    .is_empty()
-                    .then(|| label(self.empty_message).color(LabelColor::Muted)),
+                self.header
+                    .clone()
+                    .map(|header| header.set_toggleable(self.toggleable)),
             )
-            .children(self.items.iter().cloned())
+            .child(list_content)
     }
 }

crates/ui/src/components/list_item.rs 🔗

@@ -1,112 +0,0 @@
-use crate::prelude::{DisclosureControlVisibility, InteractionState, ToggleState};
-use crate::theme::theme;
-use crate::tokens::token;
-use crate::{icon, IconAsset, Label};
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
-
-#[derive(Element, Clone)]
-pub struct ListItem {
-    label: Label,
-    left_icon: Option<IconAsset>,
-    indent_level: u32,
-    state: InteractionState,
-    disclosure_control_style: DisclosureControlVisibility,
-    toggle: Option<ToggleState>,
-}
-
-pub fn list_item(label: Label) -> ListItem {
-    ListItem {
-        label,
-        indent_level: 0,
-        left_icon: None,
-        disclosure_control_style: DisclosureControlVisibility::default(),
-        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
-    }
-
-    pub fn disclosure_control_style(
-        mut self,
-        disclosure_control_style: DisclosureControlVisibility,
-    ) -> Self {
-        self.disclosure_control_style = disclosure_control_style;
-        self
-    }
-
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        let token = token();
-        let mut disclosure_control = match self.toggle {
-            Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))),
-            Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))),
-            None => Some(div()),
-        };
-
-        match self.disclosure_control_style {
-            DisclosureControlVisibility::OnHover => {
-                disclosure_control =
-                    disclosure_control.map(|c| div().absolute().neg_left_5().child(c));
-            }
-            DisclosureControlVisibility::Always => {}
-        }
-
-        div()
-            .fill(theme.middle.base.default.background)
-            .hover()
-            .fill(theme.middle.base.hovered.background)
-            .active()
-            .fill(theme.middle.base.pressed.background)
-            .relative()
-            .py_1()
-            .child(
-                div()
-                    .h_6()
-                    .px_2()
-                    // .ml(rems(0.75 * self.indent_level as f32))
-                    .children((0..self.indent_level).map(|_| {
-                        div()
-                            .w(token.list_indent_depth)
-                            .h_full()
-                            .flex()
-                            .justify_center()
-                            .child(
-                                div()
-                                    .ml_px()
-                                    .w_px()
-                                    .h_full()
-                                    .fill(theme.middle.base.default.border),
-                            )
-                    }))
-                    .flex()
-                    .gap_1()
-                    .items_center()
-                    .relative()
-                    .children(disclosure_control)
-                    .children(self.left_icon.map(|i| icon(i)))
-                    .child(self.label.clone()),
-            )
-    }
-}

crates/ui/src/components/list_section_header.rs 🔗

@@ -1,88 +0,0 @@
-use crate::prelude::{InteractionState, ToggleState};
-use crate::theme::theme;
-use crate::tokens::token;
-use crate::{icon, label, IconAsset, LabelColor, LabelSize};
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
-
-#[derive(Element, Clone, Copy)]
-pub struct ListSectionHeader {
-    label: &'static str,
-    left_icon: Option<IconAsset>,
-    state: InteractionState,
-    toggle: Option<ToggleState>,
-}
-
-pub fn list_section_header(label: &'static str) -> ListSectionHeader {
-    ListSectionHeader {
-        label,
-        left_icon: None,
-        state: InteractionState::default(),
-        toggle: None,
-    }
-}
-
-impl ListSectionHeader {
-    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);
-        let token = token();
-
-        let disclosure_control = match self.toggle {
-            Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))),
-            Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))),
-            None => Some(div()),
-        };
-
-        div()
-            .flex()
-            .flex_1()
-            .w_full()
-            .fill(theme.middle.base.default.background)
-            .hover()
-            .fill(theme.middle.base.hovered.background)
-            .active()
-            .fill(theme.middle.base.pressed.background)
-            .relative()
-            .py_1()
-            .child(
-                div()
-                    .h_6()
-                    .px_2()
-                    .flex()
-                    .flex_1()
-                    .w_full()
-                    .gap_1()
-                    .items_center()
-                    .justify_between()
-                    .child(
-                        div()
-                            .flex()
-                            .gap_1()
-                            .items_center()
-                            .children(self.left_icon.map(|i| icon(i)))
-                            .child(
-                                label(self.label.clone())
-                                    .color(LabelColor::Muted)
-                                    .size(LabelSize::Small),
-                            ),
-                    )
-                    .children(disclosure_control),
-            )
-    }
-}

crates/ui/src/components/palette.rs 🔗

@@ -1,12 +1,8 @@
 use std::marker::PhantomData;
 
-use crate::prelude::OrderMethod;
+use crate::prelude::*;
 use crate::theme::theme;
-use crate::{label, palette_item, LabelColor, PaletteItem};
-use gpui2::elements::div::ScrollState;
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use crate::{h_stack, v_stack, Keybinding, Label, LabelColor};
 
 #[derive(Element)]
 pub struct Palette<V: 'static> {
@@ -18,20 +14,19 @@ pub struct Palette<V: 'static> {
     default_order: OrderMethod,
 }
 
-pub fn palette<V: 'static>(scroll_state: ScrollState) -> Palette<V> {
-    Palette {
-        view_type: PhantomData,
-        scroll_state,
-        input_placeholder: "Find something...",
-        empty_string: "No items found.",
-        items: vec![],
-        default_order: OrderMethod::default(),
+impl<V: 'static> Palette<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            input_placeholder: "Find something...",
+            empty_string: "No items found.",
+            items: vec![],
+            default_order: OrderMethod::default(),
+        }
     }
-}
 
-impl<V: 'static> Palette<V> {
-    pub fn items(mut self, mut items: Vec<PaletteItem>) -> Self {
-        items.sort_by_key(|item| item.label);
+    pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
         self.items = items;
         self
     }
@@ -55,49 +50,33 @@ impl<V: 'static> Palette<V> {
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        div()
+        v_stack()
             .w_96()
             .rounded_lg()
             .fill(theme.lowest.base.default.background)
             .border()
             .border_color(theme.lowest.base.default.border)
-            .flex()
-            .flex_col()
             .child(
-                div()
-                    .flex()
-                    .flex_col()
+                v_stack()
                     .gap_px()
-                    .child(
-                        div().py_0p5().px_1().flex().flex_col().child(
-                            div().px_2().py_0p5().child(
-                                label(self.input_placeholder).color(LabelColor::Placeholder),
-                            ),
+                    .child(v_stack().py_0p5().px_1().child(
+                        div().px_2().py_0p5().child(
+                            Label::new(self.input_placeholder).color(LabelColor::Placeholder),
                         ),
-                    )
+                    ))
                     .child(div().h_px().w_full().fill(theme.lowest.base.default.border))
                     .child(
-                        div()
+                        v_stack()
                             .py_0p5()
                             .px_1()
-                            .flex()
-                            .flex_col()
                             .grow()
                             .max_h_96()
                             .overflow_y_scroll(self.scroll_state.clone())
                             .children(
                                 vec![if self.items.is_empty() {
-                                    Some(
-                                        div()
-                                            .flex()
-                                            .flex_row()
-                                            .justify_between()
-                                            .px_2()
-                                            .py_1()
-                                            .child(
-                                                label(self.empty_string).color(LabelColor::Muted),
-                                            ),
-                                    )
+                                    Some(h_stack().justify_between().px_2().py_1().child(
+                                        Label::new(self.empty_string).color(LabelColor::Muted),
+                                    ))
                                 } else {
                                     None
                                 }]
@@ -105,9 +84,7 @@ impl<V: 'static> Palette<V> {
                                 .flatten(),
                             )
                             .children(self.items.iter().map(|item| {
-                                div()
-                                    .flex()
-                                    .flex_row()
+                                h_stack()
                                     .justify_between()
                                     .px_2()
                                     .py_0p5()
@@ -116,9 +93,52 @@ impl<V: 'static> Palette<V> {
                                     .fill(theme.lowest.base.hovered.background)
                                     .active()
                                     .fill(theme.lowest.base.pressed.background)
-                                    .child(palette_item(item.label, item.keybinding))
+                                    .child(
+                                        PaletteItem::new(item.label)
+                                            .keybinding(item.keybinding.clone()),
+                                    )
                             })),
                     ),
             )
     }
 }
+
+#[derive(Element)]
+pub struct PaletteItem {
+    pub label: &'static str,
+    pub keybinding: Option<Keybinding>,
+}
+
+impl PaletteItem {
+    pub fn new(label: &'static str) -> Self {
+        Self {
+            label,
+            keybinding: None,
+        }
+    }
+
+    pub fn label(mut self, label: &'static str) -> Self {
+        self.label = label;
+        self
+    }
+
+    pub fn keybinding<K>(mut self, keybinding: K) -> Self
+    where
+        K: Into<Option<Keybinding>>,
+    {
+        self.keybinding = keybinding.into();
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_row()
+            .grow()
+            .justify_between()
+            .child(Label::new(self.label))
+            .children(self.keybinding.clone())
+    }
+}

crates/ui/src/components/palette_item.rs 🔗

@@ -1,63 +0,0 @@
-use crate::theme::theme;
-use crate::{label, LabelColor, LabelSize};
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement};
-use gpui2::{ParentElement, ViewContext};
-
-#[derive(Element)]
-pub struct PaletteItem {
-    pub label: &'static str,
-    pub keybinding: Option<&'static str>,
-}
-
-pub fn palette_item(label: &'static str, keybinding: Option<&'static str>) -> PaletteItem {
-    PaletteItem { label, keybinding }
-}
-
-impl PaletteItem {
-    pub fn label(mut self, label: &'static str) -> Self {
-        self.label = label;
-        self
-    }
-
-    pub fn keybinding(mut self, keybinding: Option<&'static str>) -> Self {
-        self.keybinding = keybinding;
-        self
-    }
-
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        let keybinding_label = match self.keybinding {
-            Some(keybind) => label(keybind)
-                .color(LabelColor::Muted)
-                .size(LabelSize::Small),
-            None => label(""),
-        };
-
-        div()
-            .flex()
-            .flex_row()
-            .grow()
-            .justify_between()
-            .child(label(self.label))
-            .child(
-                self.keybinding
-                    .map(|_| {
-                        div()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .px_1()
-                            .py_0()
-                            .my_0p5()
-                            .rounded_md()
-                            .text_sm()
-                            .fill(theme.lowest.on.default.background)
-                            .child(keybinding_label)
-                    })
-                    .unwrap_or_else(|| div()),
-            )
-    }
-}

crates/ui/src/components/panel.rs 🔗

@@ -0,0 +1,146 @@
+use std::marker::PhantomData;
+
+use gpui2::geometry::AbsoluteLength;
+
+use crate::prelude::*;
+use crate::{theme, token, v_stack};
+
+#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum PanelAllowedSides {
+    LeftOnly,
+    RightOnly,
+    BottomOnly,
+    #[default]
+    LeftAndRight,
+    All,
+}
+
+impl PanelAllowedSides {
+    /// Return a `HashSet` that contains the allowable `PanelSide`s.
+    pub fn allowed_sides(&self) -> HashSet<PanelSide> {
+        match self {
+            Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
+            Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
+            Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
+            Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
+            Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
+        }
+    }
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum PanelSide {
+    #[default]
+    Left,
+    Right,
+    Bottom,
+}
+
+use std::collections::HashSet;
+
+#[derive(Element)]
+pub struct Panel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    current_side: PanelSide,
+    /// Defaults to PanelAllowedSides::LeftAndRight
+    allowed_sides: PanelAllowedSides,
+    initial_width: AbsoluteLength,
+    width: Option<AbsoluteLength>,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Panel<V> {
+    pub fn new(
+        scroll_state: ScrollState,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        let token = token();
+
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            current_side: PanelSide::default(),
+            allowed_sides: PanelAllowedSides::default(),
+            initial_width: token.default_panel_size,
+            width: None,
+            children,
+            payload,
+        }
+    }
+
+    pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
+        self.initial_width = initial_width;
+        self
+    }
+
+    pub fn width(mut self, width: AbsoluteLength) -> Self {
+        self.width = Some(width);
+        self
+    }
+
+    pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
+        self.allowed_sides = allowed_sides;
+        self
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        let allowed_sides = self.allowed_sides.allowed_sides();
+
+        if allowed_sides.contains(&side) {
+            self.current_side = side;
+        } else {
+            panic!(
+                "The panel side {:?} was not added as allowed before it was set.",
+                side
+            );
+        }
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let token = token();
+        let theme = theme(cx);
+
+        let panel_base;
+        let current_width = if let Some(width) = self.width {
+            width
+        } else {
+            self.initial_width
+        };
+
+        match self.current_side {
+            PanelSide::Left => {
+                panel_base = v_stack()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .h_full()
+                    .w(current_width)
+                    .fill(theme.middle.base.default.background)
+                    .border_r()
+                    .border_color(theme.middle.base.default.border);
+            }
+            PanelSide::Right => {
+                panel_base = v_stack()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .h_full()
+                    .w(current_width)
+                    .fill(theme.middle.base.default.background)
+                    .border_r()
+                    .border_color(theme.middle.base.default.border);
+            }
+            PanelSide::Bottom => {
+                panel_base = v_stack()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .w_full()
+                    .h(current_width)
+                    .fill(theme.middle.base.default.background)
+                    .border_r()
+                    .border_color(theme.middle.base.default.border);
+            }
+        }
+
+        panel_base.children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}

crates/ui/src/components/panes.rs 🔗

@@ -0,0 +1,132 @@
+use std::marker::PhantomData;
+
+use gpui2::geometry::{Length, Size};
+use gpui2::{hsla, Hsla};
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Default, PartialEq)]
+pub enum SplitDirection {
+    #[default]
+    Horizontal,
+    Vertical,
+}
+
+#[derive(Element)]
+pub struct Pane<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    size: Size<Length>,
+    fill: Hsla,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Pane<V> {
+    pub fn new(
+        scroll_state: ScrollState,
+        size: Size<Length>,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        // Fill is only here for debugging purposes, remove before release
+        let system_color = SystemColor::new();
+
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            size,
+            fill: hsla(0.3, 0.3, 0.3, 1.),
+            // fill: system_color.transparent,
+            children,
+            payload,
+        }
+    }
+
+    pub fn fill(mut self, fill: Hsla) -> Self {
+        self.fill = fill;
+        self
+    }
+
+    fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_initial()
+            .fill(self.fill)
+            .w(self.size.width)
+            .h(self.size.height)
+            .overflow_y_scroll(self.scroll_state.clone())
+            .children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}
+
+#[derive(Element)]
+pub struct PaneGroup<V: 'static> {
+    view_type: PhantomData<V>,
+    groups: Vec<PaneGroup<V>>,
+    panes: Vec<Pane<V>>,
+    split_direction: SplitDirection,
+}
+
+impl<V: 'static> PaneGroup<V> {
+    pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
+        Self {
+            view_type: PhantomData,
+            groups,
+            panes: Vec::new(),
+            split_direction,
+        }
+    }
+
+    pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
+        Self {
+            view_type: PhantomData,
+            groups: Vec::new(),
+            panes,
+            split_direction,
+        }
+    }
+
+    fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        if !self.panes.is_empty() {
+            let el = div()
+                .flex()
+                .flex_1()
+                .gap_px()
+                .w_full()
+                .h_full()
+                .fill(theme.lowest.base.default.background)
+                .children(self.panes.iter_mut().map(|pane| pane.render(view, cx)));
+
+            if self.split_direction == SplitDirection::Horizontal {
+                return el;
+            } else {
+                return el.flex_col();
+            }
+        }
+
+        if !self.groups.is_empty() {
+            let el = div()
+                .flex()
+                .flex_1()
+                .gap_px()
+                .w_full()
+                .h_full()
+                .fill(theme.lowest.base.default.background)
+                .children(self.groups.iter_mut().map(|group| group.render(view, cx)));
+
+            if self.split_direction == SplitDirection::Horizontal {
+                return el;
+            } else {
+                return el.flex_col();
+            }
+        }
+
+        unreachable!()
+    }
+}

crates/ui/src/components/player_stack.rs 🔗

@@ -0,0 +1,66 @@
+use crate::prelude::*;
+use crate::{Avatar, Facepile, PlayerWithCallStatus};
+
+#[derive(Element)]
+pub struct PlayerStack {
+    player_with_call_status: PlayerWithCallStatus,
+}
+
+impl PlayerStack {
+    pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
+        Self {
+            player_with_call_status,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let system_color = SystemColor::new();
+        let player = self.player_with_call_status.get_player();
+        self.player_with_call_status.get_call_status();
+
+        let followers = self
+            .player_with_call_status
+            .get_call_status()
+            .followers
+            .as_ref()
+            .map(|followers| followers.clone());
+
+        // if we have no followers return a slightly different element
+        // if mic_status == muted add a red ring to avatar
+
+        div()
+            .h_full()
+            .flex()
+            .flex_col()
+            .gap_px()
+            .justify_center()
+            .child(
+                div().flex().justify_center().w_full().child(
+                    div()
+                        .w_4()
+                        .h_1()
+                        .rounded_bl_sm()
+                        .rounded_br_sm()
+                        .fill(player.cursor_color(cx)),
+                ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .h_6()
+                    .px_1()
+                    .rounded_lg()
+                    .fill(if followers.is_none() {
+                        system_color.transparent
+                    } else {
+                        player.selection_color(cx)
+                    })
+                    .child(Avatar::new(player.avatar_src().to_string()))
+                    .children(followers.map(|followers| {
+                        div().neg_mr_1().child(Facepile::new(followers.into_iter()))
+                    })),
+            )
+    }
+}

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

@@ -1,62 +1,87 @@
-use crate::{
-    input, list, list_section_header, prelude::*, static_project_panel_project_items,
-    static_project_panel_single_items, theme,
-};
+use std::marker::PhantomData;
+use std::sync::Arc;
 
-use gpui2::{
-    elements::{div, div::ScrollState},
-    style::StyleHelpers,
-    ParentElement, ViewContext,
+use crate::prelude::*;
+use crate::{
+    static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
+    ListHeader, Panel, PanelSide, Theme,
 };
-use gpui2::{Element, IntoElement};
-use std::marker::PhantomData;
 
 #[derive(Element)]
 pub struct ProjectPanel<V: 'static> {
     view_type: PhantomData<V>,
     scroll_state: ScrollState,
+    current_side: PanelSide,
 }
 
-pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
-    ProjectPanel {
-        view_type: PhantomData,
-        scroll_state,
+impl<V: 'static> ProjectPanel<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            current_side: PanelSide::default(),
+        }
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        self.current_side = side;
+        self
     }
-}
 
-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()
+        struct PanelPayload {
+            pub theme: Arc<Theme>,
+            pub scroll_state: ScrollState,
+        }
+
+        Panel::new(
+            self.scroll_state.clone(),
+            |_, payload| {
+                let payload = payload.downcast_ref::<PanelPayload>().unwrap();
+
+                let theme = payload.theme.clone();
+
+                vec![div()
                     .flex()
                     .flex_col()
-                    .overflow_y_scroll(self.scroll_state.clone())
+                    .w_56()
+                    .h_full()
+                    .px_2()
+                    .fill(theme.middle.base.default.background)
                     .child(
-                        list(static_project_panel_single_items())
-                            .header(list_section_header("FILES").set_toggle(ToggleState::Toggled))
-                            .empty_message("No files in directory")
-                            .set_toggle(ToggleState::Toggled),
+                        div()
+                            .w_56()
+                            .flex()
+                            .flex_col()
+                            .overflow_y_scroll(payload.scroll_state.clone())
+                            .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(
-                        list(static_project_panel_project_items())
-                            .header(list_section_header("PROJECT").set_toggle(ToggleState::Toggled))
-                            .empty_message("No folders in directory")
-                            .set_toggle(ToggleState::Toggled),
-                    ),
-            )
-            .child(
-                input("Find something...")
-                    .value("buffe".to_string())
-                    .state(InteractionState::Focused),
-            )
+                        Input::new("Find something...")
+                            .value("buffe".to_string())
+                            .state(InteractionState::Focused),
+                    )
+                    .into_any()]
+            },
+            Box::new(PanelPayload {
+                theme: theme(cx),
+                scroll_state: self.scroll_state.clone(),
+            }),
+        )
     }
 }

crates/ui/src/components/status_bar.rs 🔗

@@ -1,11 +1,8 @@
 use std::marker::PhantomData;
 
-use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
-
+use crate::prelude::*;
 use crate::theme::{theme, Theme};
-use crate::{icon_button, text_button, tool_divider, IconAsset};
+use crate::{Button, Icon, IconButton, IconColor, ToolDivider};
 
 #[derive(Default, PartialEq)]
 pub enum Tool {
@@ -40,16 +37,16 @@ pub struct StatusBar<V: 'static> {
     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 new() -> Self {
+        Self {
+            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];
@@ -106,10 +103,10 @@ impl<V: 'static> StatusBar<V> {
             .flex()
             .items_center()
             .gap_1()
-            .child(icon_button().icon(IconAsset::FileTree))
-            .child(icon_button().icon(IconAsset::Hash))
-            .child(tool_divider())
-            .child(icon_button().icon(IconAsset::XCircle))
+            .child(IconButton::new(Icon::FileTree).color(IconColor::Accent))
+            .child(IconButton::new(Icon::Hash))
+            .child(ToolDivider::new())
+            .child(IconButton::new(Icon::XCircle))
     }
     fn right_tools(&self, theme: &Theme) -> impl Element<V> {
         div()
@@ -121,27 +118,27 @@ impl<V: 'static> StatusBar<V> {
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(text_button("116:25"))
-                    .child(text_button("Rust")),
+                    .child(Button::new("116:25"))
+                    .child(Button::new("Rust")),
             )
-            .child(tool_divider())
+            .child(ToolDivider::new())
             .child(
                 div()
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(icon_button().icon(IconAsset::Copilot))
-                    .child(icon_button().icon(IconAsset::Envelope)),
+                    .child(IconButton::new(Icon::Copilot))
+                    .child(IconButton::new(Icon::Envelope)),
             )
-            .child(tool_divider())
+            .child(ToolDivider::new())
             .child(
                 div()
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(icon_button().icon(IconAsset::Terminal))
-                    .child(icon_button().icon(IconAsset::MessageBubbles))
-                    .child(icon_button().icon(IconAsset::Ai)),
+                    .child(IconButton::new(Icon::Terminal))
+                    .child(IconButton::new(Icon::MessageBubbles))
+                    .child(IconButton::new(Icon::Ai)),
             )
     }
 }

crates/ui/src/components/tab.rs 🔗

@@ -1,22 +1,96 @@
-use gpui2::elements::div;
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::theme;
+use crate::prelude::*;
+use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
 
 #[derive(Element)]
 pub struct Tab {
-    title: &'static str,
-    enabled: bool,
-}
-
-pub fn tab<V: 'static>(title: &'static str, enabled: bool) -> impl Element<V> {
-    Tab { title, enabled }
+    title: String,
+    icon: Option<Icon>,
+    current: bool,
+    dirty: bool,
+    fs_status: FileSystemStatus,
+    git_status: GitStatus,
+    diagnostic_status: DiagnosticStatus,
+    close_side: IconSide,
 }
 
 impl Tab {
+    pub fn new() -> Self {
+        Self {
+            title: "untitled".to_string(),
+            icon: None,
+            current: false,
+            dirty: false,
+            fs_status: FileSystemStatus::None,
+            git_status: GitStatus::None,
+            diagnostic_status: DiagnosticStatus::None,
+            close_side: IconSide::Right,
+        }
+    }
+
+    pub fn current(mut self, current: bool) -> Self {
+        self.current = current;
+        self
+    }
+
+    pub fn title(mut self, title: String) -> Self {
+        self.title = title;
+        self
+    }
+
+    pub fn icon<I>(mut self, icon: I) -> Self
+    where
+        I: Into<Option<Icon>>,
+    {
+        self.icon = icon.into();
+        self
+    }
+
+    pub fn dirty(mut self, dirty: bool) -> Self {
+        self.dirty = dirty;
+        self
+    }
+
+    pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
+        self.fs_status = fs_status;
+        self
+    }
+
+    pub fn git_status(mut self, git_status: GitStatus) -> Self {
+        self.git_status = git_status;
+        self
+    }
+
+    pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
+        self.diagnostic_status = diagnostic_status;
+        self
+    }
+
+    pub fn close_side(mut self, close_side: IconSide) -> Self {
+        self.close_side = close_side;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
+        let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
+        let is_deleted = self.fs_status == FileSystemStatus::Deleted;
+
+        let label = match (self.git_status, is_deleted) {
+            (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
+                .color(LabelColor::Hidden)
+                .set_strikethrough(true),
+            (GitStatus::None, false) => Label::new(self.title.clone()),
+            (GitStatus::Created, false) => {
+                Label::new(self.title.clone()).color(LabelColor::Created)
+            }
+            (GitStatus::Modified, false) => {
+                Label::new(self.title.clone()).color(LabelColor::Modified)
+            }
+            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
+            (GitStatus::Conflict, false) => Label::new(self.title.clone()),
+        };
+
+        let close_icon = IconElement::new(Icon::Close).color(IconColor::Muted);
 
         div()
             .px_2()
@@ -24,33 +98,34 @@ impl Tab {
             .flex()
             .items_center()
             .justify_center()
-            .rounded_lg()
-            .fill(if self.enabled {
-                theme.highest.on.default.background
-            } else {
+            .fill(if self.current {
                 theme.highest.base.default.background
-            })
-            .hover()
-            .fill(if self.enabled {
-                theme.highest.on.hovered.background
             } else {
-                theme.highest.base.hovered.background
-            })
-            .active()
-            .fill(if self.enabled {
-                theme.highest.on.pressed.background
-            } else {
-                theme.highest.base.pressed.background
+                theme.middle.base.default.background
             })
             .child(
                 div()
-                    .text_sm()
-                    .text_color(if self.enabled {
-                        theme.highest.base.default.foreground
+                    .px_1()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .children(has_fs_conflict.then(|| {
+                        IconElement::new(Icon::ExclamationTriangle)
+                            .size(crate::IconSize::Small)
+                            .color(IconColor::Warning)
+                    }))
+                    .children(self.icon.map(IconElement::new))
+                    .children(if self.close_side == IconSide::Left {
+                        Some(close_icon.clone())
                     } else {
-                        theme.highest.variant.default.foreground
+                        None
                     })
-                    .child(self.title),
+                    .child(label)
+                    .children(if self.close_side == IconSide::Right {
+                        Some(close_icon)
+                    } else {
+                        None
+                    }),
             )
     }
 }

crates/ui/src/components/tab_bar.rs 🔗

@@ -1,13 +1,7 @@
 use std::marker::PhantomData;
 
-use gpui2::elements::div::ScrollState;
-use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
-
-use crate::prelude::InteractionState;
-use crate::theme::theme;
-use crate::{icon_button, tab, IconAsset};
+use crate::prelude::*;
+use crate::{theme, Icon, IconButton, Tab};
 
 #[derive(Element)]
 pub struct TabBar<V: 'static> {
@@ -15,14 +9,14 @@ pub struct TabBar<V: 'static> {
     scroll_state: ScrollState,
 }
 
-pub fn tab_bar<V: 'static>(scroll_state: ScrollState) -> TabBar<V> {
-    TabBar {
-        view_type: PhantomData,
-        scroll_state,
+impl<V: 'static> TabBar<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
     }
-}
 
-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;
@@ -30,6 +24,7 @@ impl<V: 'static> TabBar<V> {
         div()
             .w_full()
             .flex()
+            .fill(theme.middle.base.default.background)
             // Left Side
             .child(
                 div()
@@ -44,12 +39,11 @@ impl<V: 'static> TabBar<V> {
                             .items_center()
                             .gap_px()
                             .child(
-                                icon_button()
-                                    .icon(IconAsset::ArrowLeft)
+                                IconButton::new(Icon::ArrowLeft)
                                     .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
                             )
                             .child(
-                                icon_button().icon(IconAsset::ArrowRight).state(
+                                IconButton::new(Icon::ArrowRight).state(
                                     InteractionState::Enabled.if_enabled(can_navigate_forward),
                                 ),
                             ),
@@ -59,17 +53,52 @@ impl<V: 'static> TabBar<V> {
                 div().w_0().flex_1().h_full().child(
                     div()
                         .flex()
-                        .gap_1()
                         .overflow_x_scroll(self.scroll_state.clone())
-                        .child(tab("Cargo.toml", false))
-                        .child(tab("Channels Panel", true))
-                        .child(tab("channels_panel.rs", false))
-                        .child(tab("workspace.rs", false))
-                        .child(tab("icon_button.rs", false))
-                        .child(tab("storybook.rs", false))
-                        .child(tab("theme.rs", false))
-                        .child(tab("theme_registry.rs", false))
-                        .child(tab("styleable_helpers.rs", false)),
+                        .child(
+                            Tab::new()
+                                .title("Cargo.toml".to_string())
+                                .current(false)
+                                .git_status(GitStatus::Modified),
+                        )
+                        .child(
+                            Tab::new()
+                                .title("Channels Panel".to_string())
+                                .current(false),
+                        )
+                        .child(
+                            Tab::new()
+                                .title("channels_panel.rs".to_string())
+                                .current(true)
+                                .git_status(GitStatus::Modified),
+                        )
+                        .child(
+                            Tab::new()
+                                .title("workspace.rs".to_string())
+                                .current(false)
+                                .git_status(GitStatus::Modified),
+                        )
+                        .child(
+                            Tab::new()
+                                .title("icon_button.rs".to_string())
+                                .current(false),
+                        )
+                        .child(
+                            Tab::new()
+                                .title("storybook.rs".to_string())
+                                .current(false)
+                                .git_status(GitStatus::Created),
+                        )
+                        .child(Tab::new().title("theme.rs".to_string()).current(false))
+                        .child(
+                            Tab::new()
+                                .title("theme_registry.rs".to_string())
+                                .current(false),
+                        )
+                        .child(
+                            Tab::new()
+                                .title("styleable_helpers.rs".to_string())
+                                .current(false),
+                        ),
                 ),
             )
             // Right Side
@@ -85,8 +114,8 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button().icon(IconAsset::Plus))
-                            .child(icon_button().icon(IconAsset::Split)),
+                            .child(IconButton::new(Icon::Plus))
+                            .child(IconButton::new(Icon::Split)),
                     ),
             )
     }

crates/ui/src/components/terminal.rs 🔗

@@ -0,0 +1,77 @@
+use gpui2::geometry::{relative, rems, Size};
+
+use crate::prelude::*;
+use crate::{theme, Icon, IconButton, Pane, Tab};
+
+#[derive(Element)]
+pub struct Terminal {}
+
+impl Terminal {
+    pub fn new() -> Self {
+        Self {}
+    }
+
+    fn render<V: 'static>(&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()
+            .flex()
+            .flex_col()
+            .child(
+                // Terminal Tabs.
+                div()
+                    .w_full()
+                    .flex()
+                    .fill(theme.middle.base.default.background)
+                    .child(
+                        div().px_1().flex().flex_none().gap_2().child(
+                            div()
+                                .flex()
+                                .items_center()
+                                .gap_px()
+                                .child(
+                                    IconButton::new(Icon::ArrowLeft).state(
+                                        InteractionState::Enabled.if_enabled(can_navigate_back),
+                                    ),
+                                )
+                                .child(IconButton::new(Icon::ArrowRight).state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                )),
+                        ),
+                    )
+                    .child(
+                        div().w_0().flex_1().h_full().child(
+                            div()
+                                .flex()
+                                .child(
+                                    Tab::new()
+                                        .title("zed — fish".to_string())
+                                        .icon(Icon::Terminal)
+                                        .close_side(IconSide::Right)
+                                        .current(true),
+                                )
+                                .child(
+                                    Tab::new()
+                                        .title("zed — fish".to_string())
+                                        .icon(Icon::Terminal)
+                                        .close_side(IconSide::Right)
+                                        .current(false),
+                                ),
+                        ),
+                    ),
+            )
+            // Terminal Pane.
+            .child(Pane::new(
+                ScrollState::default(),
+                Size {
+                    width: relative(1.).into(),
+                    height: rems(36.).into(),
+                },
+                |_, _| vec![],
+                Box::new(()),
+            ))
+    }
+}

crates/ui/src/components/title_bar.rs 🔗

@@ -1,33 +1,41 @@
 use std::marker::PhantomData;
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
 
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::prelude::Shape;
+use crate::prelude::*;
 use crate::{
-    avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights, IconAsset,
-    IconColor,
+    static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor,
+    PlayerStack, ToolDivider, TrafficLights,
 };
 
 #[derive(Element)]
 pub struct TitleBar<V: 'static> {
     view_type: PhantomData<V>,
+    is_active: Arc<AtomicBool>,
 }
 
-pub fn title_bar<V: 'static>() -> TitleBar<V> {
-    TitleBar {
-        view_type: PhantomData,
+impl<V: 'static> TitleBar<V> {
+    pub fn new(cx: &mut ViewContext<V>) -> Self {
+        let is_active = Arc::new(AtomicBool::new(true));
+        let active = is_active.clone();
+
+        cx.observe_window_activation(move |_, is_active, cx| {
+            active.store(is_active, std::sync::atomic::Ordering::SeqCst);
+            cx.notify();
+        })
+        .detach();
+
+        Self {
+            view_type: PhantomData,
+            is_active,
+        }
     }
-}
 
-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"),
-        ];
+        let has_focus = cx.window_is_active();
+
+        let player_list = static_players_with_call_status().into_iter();
 
         div()
             .flex()
@@ -43,20 +51,17 @@ impl<V: 'static> TitleBar<V> {
                     .h_full()
                     .gap_4()
                     .px_2()
-                    .child(traffic_lights())
+                    .child(TrafficLights::new().window_has_focus(has_focus))
                     // === 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(Button::new("zed"))
+                            .child(Button::new("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)),
+                    .children(player_list.map(|p| PlayerStack::new(p))),
             )
             .child(
                 div()
@@ -68,27 +73,23 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button().icon(IconAsset::FolderX))
-                            .child(icon_button().icon(IconAsset::Close)),
+                            .child(IconButton::new(Icon::FolderX))
+                            .child(IconButton::new(Icon::Close)),
                     )
-                    .child(tool_divider())
+                    .child(ToolDivider::new())
                     .child(
                         div()
                             .px_2()
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button().icon(IconAsset::Mic))
-                            .child(icon_button().icon(IconAsset::AudioOn))
-                            .child(
-                                icon_button()
-                                    .icon(IconAsset::Screen)
-                                    .color(IconColor::Accent),
-                            ),
+                            .child(IconButton::new(Icon::Mic))
+                            .child(IconButton::new(Icon::AudioOn))
+                            .child(IconButton::new(Icon::Screen).color(IconColor::Accent)),
                     )
                     .child(
                         div().px_2().flex().items_center().child(
-                            avatar("https://avatars.githubusercontent.com/u/1714999?v=4")
+                            Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
                                 .shape(Shape::RoundedRectangle),
                         ),
                     ),

crates/ui/src/components/toolbar.rs 🔗

@@ -1,21 +1,19 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::{breadcrumb, theme, IconAsset, IconButton};
+use crate::prelude::*;
+use crate::{theme, Breadcrumb, Icon, IconButton};
 
+#[derive(Clone)]
 pub struct ToolbarItem {}
 
-#[derive(Element)]
+#[derive(Element, Clone)]
 pub struct Toolbar {
     items: Vec<ToolbarItem>,
 }
 
-pub fn toolbar() -> Toolbar {
-    Toolbar { items: Vec::new() }
-}
-
 impl Toolbar {
+    pub fn new() -> Self {
+        Self { items: Vec::new() }
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
@@ -23,13 +21,13 @@ impl Toolbar {
             .p_2()
             .flex()
             .justify_between()
-            .child(breadcrumb())
+            .child(Breadcrumb::new())
             .child(
                 div()
                     .flex()
-                    .child(IconButton::new(IconAsset::InlayHint))
-                    .child(IconButton::new(IconAsset::MagnifyingGlass))
-                    .child(IconButton::new(IconAsset::MagicWand)),
+                    .child(IconButton::new(Icon::InlayHint))
+                    .child(IconButton::new(Icon::MagnifyingGlass))
+                    .child(IconButton::new(Icon::MagicWand)),
             )
     }
 }

crates/ui/src/components/traffic_lights.rs 🔗

@@ -1,30 +1,78 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, Hsla, IntoElement, ParentElement, ViewContext};
+use crate::prelude::*;
+use crate::{theme, token, SystemColor};
 
-use crate::theme;
+#[derive(Clone, Copy)]
+enum TrafficLightColor {
+    Red,
+    Yellow,
+    Green,
+}
 
 #[derive(Element)]
-pub struct TrafficLights {}
+struct TrafficLight {
+    color: TrafficLightColor,
+    window_has_focus: bool,
+}
+
+impl TrafficLight {
+    fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
+        Self {
+            color,
+            window_has_focus,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        let fill = match (self.window_has_focus, self.color) {
+            (true, TrafficLightColor::Red) => system_color.mac_os_traffic_light_red,
+            (true, TrafficLightColor::Yellow) => system_color.mac_os_traffic_light_yellow,
+            (true, TrafficLightColor::Green) => system_color.mac_os_traffic_light_green,
+            (false, _) => theme.lowest.base.active.background,
+        };
+
+        div().w_3().h_3().rounded_full().fill(fill)
+    }
+}
 
-pub fn traffic_lights() -> TrafficLights {
-    TrafficLights {}
+#[derive(Element)]
+pub struct TrafficLights {
+    window_has_focus: bool,
 }
 
 impl TrafficLights {
+    pub fn new() -> Self {
+        Self {
+            window_has_focus: true,
+        }
+    }
+
+    pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
+        self.window_has_focus = window_has_focus;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
+        let token = token();
 
         div()
             .flex()
             .items_center()
             .gap_2()
-            .child(traffic_light(theme.lowest.negative.default.foreground))
-            .child(traffic_light(theme.lowest.warning.default.foreground))
-            .child(traffic_light(theme.lowest.positive.default.foreground))
+            .child(TrafficLight::new(
+                TrafficLightColor::Red,
+                self.window_has_focus,
+            ))
+            .child(TrafficLight::new(
+                TrafficLightColor::Yellow,
+                self.window_has_focus,
+            ))
+            .child(TrafficLight::new(
+                TrafficLightColor::Green,
+                self.window_has_focus,
+            ))
     }
 }
-
-fn traffic_light<V: 'static, C: Into<Hsla>>(fill: C) -> div::Div<V> {
-    div().w_3().h_3().rounded_full().fill(fill.into())
-}

crates/ui/src/components/workspace.rs 🔗

@@ -1,30 +1,68 @@
-use crate::{chat_panel, collab_panel, project_panel, status_bar, tab_bar, theme, title_bar};
+use chrono::DateTime;
+use gpui2::geometry::{relative, rems, Size};
 
-use gpui2::{
-    elements::{div, div::ScrollState},
-    style::StyleHelpers,
-    Element, IntoElement, ParentElement, ViewContext,
+use crate::prelude::*;
+use crate::{
+    theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide,
+    ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
 };
 
 #[derive(Element, Default)]
-struct WorkspaceElement {
-    project_panel_scroll_state: ScrollState,
-    collab_panel_scroll_state: ScrollState,
-    right_scroll_state: ScrollState,
+pub struct WorkspaceElement {
+    left_panel_scroll_state: ScrollState,
+    right_panel_scroll_state: ScrollState,
     tab_bar_scroll_state: ScrollState,
-    palette_scroll_state: ScrollState,
-}
-
-pub fn workspace<V: 'static>() -> impl Element<V> {
-    WorkspaceElement::default()
+    bottom_panel_scroll_state: ScrollState,
 }
 
 impl WorkspaceElement {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
+        let temp_size = rems(36.).into();
+
+        let root_group = PaneGroup::new_groups(
+            vec![
+                PaneGroup::new_panes(
+                    vec![
+                        Pane::new(
+                            ScrollState::default(),
+                            Size {
+                                width: relative(1.).into(),
+                                height: temp_size,
+                            },
+                            |_, _| vec![Terminal::new().into_any()],
+                            Box::new(()),
+                        ),
+                        Pane::new(
+                            ScrollState::default(),
+                            Size {
+                                width: relative(1.).into(),
+                                height: temp_size,
+                            },
+                            |_, _| vec![Terminal::new().into_any()],
+                            Box::new(()),
+                        ),
+                    ],
+                    SplitDirection::Vertical,
+                ),
+                PaneGroup::new_panes(
+                    vec![Pane::new(
+                        ScrollState::default(),
+                        Size {
+                            width: relative(1.).into(),
+                            height: relative(1.).into(),
+                        },
+                        |_, _| vec![Terminal::new().into_any()],
+                        Box::new(()),
+                    )],
+                    SplitDirection::Vertical,
+                ),
+            ],
+            SplitDirection::Horizontal,
+        );
+
+        let theme = theme(cx).clone();
 
         div()
-            // Elevation Level 0
             .size_full()
             .flex()
             .flex_col()
@@ -34,9 +72,7 @@ impl WorkspaceElement {
             .items_start()
             .text_color(theme.lowest.base.default.foreground)
             .fill(theme.lowest.base.default.background)
-            .relative()
-            // Elevation Level 1
-            .child(title_bar())
+            .child(TitleBar::new(cx))
             .child(
                 div()
                     .flex_1()
@@ -44,37 +80,57 @@ impl WorkspaceElement {
                     .flex()
                     .flex_row()
                     .overflow_hidden()
-                    .child(project_panel(self.project_panel_scroll_state.clone()))
-                    .child(collab_panel(self.collab_panel_scroll_state.clone()))
+                    .border_t()
+                    .border_b()
+                    .border_color(theme.lowest.base.default.border)
                     .child(
-                        div()
-                            .h_full()
+                        ProjectPanel::new(self.left_panel_scroll_state.clone())
+                            .side(PanelSide::Left),
+                    )
+                    .child(
+                        v_stack()
                             .flex_1()
-                            .fill(theme.highest.base.default.background)
+                            .h_full()
                             .child(
                                 div()
                                     .flex()
-                                    .flex_col()
                                     .flex_1()
-                                    .child(tab_bar(self.tab_bar_scroll_state.clone())),
+                                    // CSS Hack: Flex 1 has to have a set height to properly fill the space
+                                    // Or it will give you a height of 0
+                                    .h_px()
+                                    .child(root_group),
+                            )
+                            .child(
+                                Panel::new(
+                                    self.bottom_panel_scroll_state.clone(),
+                                    |_, _| vec![Terminal::new().into_any()],
+                                    Box::new(()),
+                                )
+                                .allowed_sides(PanelAllowedSides::BottomOnly)
+                                .side(PanelSide::Bottom),
                             ),
                     )
-                    .child(chat_panel(self.right_scroll_state.clone())),
+                    .child(ChatPanel::new(ScrollState::default()).with_messages(vec![
+                                ChatMessage::new(
+                                    "osiewicz".to_string(),
+                                    "is this thing on?".to_string(),
+                                    DateTime::parse_from_rfc3339(
+                                        "2023-09-27T15:40:52.707Z",
+                                    )
+                                    .unwrap()
+                                    .naive_local(),
+                                ),
+                                ChatMessage::new(
+                                    "maxdeviant".to_string(),
+                                    "Reading you loud and clear!".to_string(),
+                                    DateTime::parse_from_rfc3339(
+                                        "2023-09-28T15:40:52.707Z",
+                                    )
+                                    .unwrap()
+                                    .naive_local(),
+                                ),
+                            ])),
             )
-            .child(status_bar())
-        // Elevation Level 3
-        // .child(
-        //     div()
-        //         .absolute()
-        //         .top_0()
-        //         .left_0()
-        //         .size_full()
-        //         .flex()
-        //         .justify_center()
-        //         .items_center()
-        //         // .fill(theme.lowest.base.default.background)
-        //         // Elevation Level 4
-        //         .child(command_palette(self.palette_scroll_state.clone())),
-        // )
+            .child(StatusBar::new())
     }
 }

crates/ui/src/elements.rs 🔗

@@ -1,17 +1,19 @@
 mod avatar;
+mod button;
 mod details;
 mod icon;
-mod indicator;
 mod input;
 mod label;
-mod text_button;
+mod player;
+mod stack;
 mod tool_divider;
 
 pub use avatar::*;
+pub use button::*;
 pub use details::*;
 pub use icon::*;
-pub use indicator::*;
 pub use input::*;
 pub use label::*;
-pub use text_button::*;
+pub use player::*;
+pub use stack::*;
 pub use tool_divider::*;

crates/ui/src/elements/avatar.rs 🔗

@@ -1,6 +1,5 @@
 use gpui2::elements::img;
-use gpui2::style::StyleHelpers;
-use gpui2::{ArcCow, Element, IntoElement, ViewContext};
+use gpui2::ArcCow;
 
 use crate::prelude::*;
 use crate::theme;
@@ -11,14 +10,14 @@ pub struct Avatar {
     shape: Shape,
 }
 
-pub fn avatar(src: impl Into<ArcCow<'static, str>>) -> Avatar {
-    Avatar {
-        src: src.into(),
-        shape: Shape::Circle,
+impl Avatar {
+    pub fn new(src: impl Into<ArcCow<'static, str>>) -> Self {
+        Self {
+            src: src.into(),
+            shape: Shape::Circle,
+        }
     }
-}
 
-impl Avatar {
     pub fn shape(mut self, shape: Shape) -> Self {
         self.shape = shape;
         self

crates/ui/src/elements/button.rs 🔗

@@ -0,0 +1,203 @@
+use std::rc::Rc;
+
+use gpui2::geometry::DefiniteLength;
+use gpui2::platform::MouseButton;
+use gpui2::{EventContext, Hsla, Interactive, WindowContext};
+
+use crate::prelude::*;
+use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum IconPosition {
+    #[default]
+    Left,
+    Right,
+}
+
+#[derive(Default, Copy, Clone, PartialEq)]
+pub enum ButtonVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+struct ButtonHandlers<V> {
+    click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
+}
+
+impl<V> Default for ButtonHandlers<V> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+#[derive(Element)]
+pub struct Button<V: 'static> {
+    label: String,
+    variant: ButtonVariant,
+    state: InteractionState,
+    icon: Option<Icon>,
+    icon_position: Option<IconPosition>,
+    width: Option<DefiniteLength>,
+    handlers: ButtonHandlers<V>,
+}
+
+impl<V: 'static> Button<V> {
+    pub fn new<L>(label: L) -> Self
+    where
+        L: Into<String>,
+    {
+        Self {
+            label: label.into(),
+            variant: Default::default(),
+            state: Default::default(),
+            icon: None,
+            icon_position: None,
+            width: Default::default(),
+            handlers: ButtonHandlers::default(),
+        }
+    }
+
+    pub fn ghost<L>(label: L) -> Self
+    where
+        L: Into<String>,
+    {
+        Self::new(label).variant(ButtonVariant::Ghost)
+    }
+
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn icon(mut self, icon: Icon) -> Self {
+        self.icon = Some(icon);
+        self
+    }
+
+    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
+        if self.icon.is_none() {
+            panic!("An icon must be present if an icon_position is provided.");
+        }
+        self.icon_position = Some(icon_position);
+        self
+    }
+
+    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
+        self.width = width;
+        self
+    }
+
+    pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext<V>) + 'static) -> Self {
+        self.handlers.click = Some(Rc::new(handler));
+        self
+    }
+
+    fn background_color(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match (self.variant, self.state) {
+            (ButtonVariant::Ghost, InteractionState::Hovered) => {
+                theme.lowest.base.hovered.background
+            }
+            (ButtonVariant::Ghost, InteractionState::Active) => {
+                theme.lowest.base.pressed.background
+            }
+            (ButtonVariant::Filled, InteractionState::Enabled) => {
+                theme.lowest.on.default.background
+            }
+            (ButtonVariant::Filled, InteractionState::Hovered) => {
+                theme.lowest.on.hovered.background
+            }
+            (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
+            (ButtonVariant::Filled, InteractionState::Disabled) => {
+                theme.lowest.on.disabled.background
+            }
+            _ => system_color.transparent,
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn border_color(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self.state {
+            InteractionState::Focused => theme.lowest.accent.default.border,
+            _ => system_color.transparent,
+        }
+    }
+
+    fn render_label(&self) -> Label {
+        Label::new(self.label.clone())
+            .size(LabelSize::Small)
+            .color(self.label_color())
+    }
+
+    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
+        self.icon.map(|i| IconElement::new(i).color(icon_color))
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let icon_color = self.icon_color();
+        let system_color = SystemColor::new();
+        let border_color = self.border_color(cx);
+
+        let mut el = h_stack()
+            .h_6()
+            .px_1()
+            .items_center()
+            .rounded_md()
+            .border()
+            .border_color(border_color)
+            .fill(self.background_color(cx));
+
+        match (self.icon, self.icon_position) {
+            (Some(_), Some(IconPosition::Left)) => {
+                el = el
+                    .gap_1()
+                    .child(self.render_label())
+                    .children(self.render_icon(icon_color))
+            }
+            (Some(_), Some(IconPosition::Right)) => {
+                el = el
+                    .gap_1()
+                    .children(self.render_icon(icon_color))
+                    .child(self.render_label())
+            }
+            (_, _) => el = el.child(self.render_label()),
+        }
+
+        if let Some(width) = self.width {
+            el = el.w(width).justify_center();
+        }
+
+        if let Some(click_handler) = self.handlers.click.clone() {
+            el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+                click_handler(view, cx);
+            });
+        }
+
+        el
+    }
+}

crates/ui/src/elements/details.rs 🔗

@@ -1,7 +1,4 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
+use crate::prelude::*;
 use crate::theme;
 
 #[derive(Element, Clone)]
@@ -10,11 +7,11 @@ pub struct Details {
     meta: Option<&'static str>,
 }
 
-pub fn details(text: &'static str) -> Details {
-    Details { text, meta: None }
-}
-
 impl Details {
+    pub fn new(text: &'static str) -> Self {
+        Self { text, meta: None }
+    }
+
     pub fn meta_text(mut self, meta: &'static str) -> Self {
         self.meta = Some(meta);
         self

crates/ui/src/elements/icon.rs 🔗

@@ -1,11 +1,19 @@
 use std::sync::Arc;
 
+use gpui2::elements::svg;
+use gpui2::Hsla;
+use strum::EnumIter;
+
+use crate::prelude::*;
 use crate::theme::theme;
 use crate::Theme;
-use gpui2::elements::svg;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, ViewContext};
-use gpui2::{Hsla, IntoElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconSize {
+    Small,
+    #[default]
+    Large,
+}
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconColor {
@@ -37,8 +45,8 @@ impl IconColor {
     }
 }
 
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum IconAsset {
+#[derive(Default, PartialEq, Copy, Clone, EnumIter)]
+pub enum Icon {
     Ai,
     ArrowLeft,
     ArrowRight,
@@ -53,6 +61,7 @@ pub enum IconAsset {
     Close,
     ExclamationTriangle,
     File,
+    FileGeneric,
     FileDoc,
     FileGit,
     FileLock,
@@ -67,89 +76,106 @@ pub enum IconAsset {
     InlayHint,
     MagicWand,
     MagnifyingGlass,
+    Maximize,
+    Menu,
     MessageBubbles,
     Mic,
     MicMute,
     Plus,
+    Quote,
     Screen,
     Split,
+    SplitMessage,
     Terminal,
     XCircle,
     Copilot,
     Envelope,
 }
 
-impl IconAsset {
+impl Icon {
     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::AudioOff => "icons/speaker-off.svg",
-            IconAsset::AudioOn => "icons/speaker-loud.svg",
-            IconAsset::Bolt => "icons/bolt.svg",
-            IconAsset::ChevronDown => "icons/chevron_down.svg",
-            IconAsset::ChevronLeft => "icons/chevron_left.svg",
-            IconAsset::ChevronRight => "icons/chevron_right.svg",
-            IconAsset::ChevronUp => "icons/chevron_up.svg",
-            IconAsset::Close => "icons/x.svg",
-            IconAsset::ExclamationTriangle => "icons/warning.svg",
-            IconAsset::File => "icons/file_icons/file.svg",
-            IconAsset::FileDoc => "icons/file_icons/book.svg",
-            IconAsset::FileGit => "icons/file_icons/git.svg",
-            IconAsset::FileLock => "icons/file_icons/lock.svg",
-            IconAsset::FileRust => "icons/file_icons/rust.svg",
-            IconAsset::FileToml => "icons/file_icons/toml.svg",
-            IconAsset::FileTree => "icons/project.svg",
-            IconAsset::Folder => "icons/file_icons/folder.svg",
-            IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
-            IconAsset::FolderX => "icons/stop_sharing.svg",
-            IconAsset::Hash => "icons/hash.svg",
-            IconAsset::InlayHint => "icons/inlay_hint.svg",
-            IconAsset::MagicWand => "icons/magic-wand.svg",
-            IconAsset::MagnifyingGlass => "icons/magnifying_glass.svg",
-            IconAsset::MessageBubbles => "icons/conversations.svg",
-            IconAsset::Mic => "icons/mic.svg",
-            IconAsset::MicMute => "icons/mic-mute.svg",
-            IconAsset::Plus => "icons/plus.svg",
-            IconAsset::Screen => "icons/desktop.svg",
-            IconAsset::Split => "icons/split.svg",
-            IconAsset::Terminal => "icons/terminal.svg",
-            IconAsset::XCircle => "icons/error.svg",
-            IconAsset::Copilot => "icons/copilot.svg",
-            IconAsset::Envelope => "icons/feedback.svg",
+            Icon::Ai => "icons/ai.svg",
+            Icon::ArrowLeft => "icons/arrow_left.svg",
+            Icon::ArrowRight => "icons/arrow_right.svg",
+            Icon::ArrowUpRight => "icons/arrow_up_right.svg",
+            Icon::AudioOff => "icons/speaker-off.svg",
+            Icon::AudioOn => "icons/speaker-loud.svg",
+            Icon::Bolt => "icons/bolt.svg",
+            Icon::ChevronDown => "icons/chevron_down.svg",
+            Icon::ChevronLeft => "icons/chevron_left.svg",
+            Icon::ChevronRight => "icons/chevron_right.svg",
+            Icon::ChevronUp => "icons/chevron_up.svg",
+            Icon::Close => "icons/x.svg",
+            Icon::ExclamationTriangle => "icons/warning.svg",
+            Icon::File => "icons/file.svg",
+            Icon::FileGeneric => "icons/file_icons/file.svg",
+            Icon::FileDoc => "icons/file_icons/book.svg",
+            Icon::FileGit => "icons/file_icons/git.svg",
+            Icon::FileLock => "icons/file_icons/lock.svg",
+            Icon::FileRust => "icons/file_icons/rust.svg",
+            Icon::FileToml => "icons/file_icons/toml.svg",
+            Icon::FileTree => "icons/project.svg",
+            Icon::Folder => "icons/file_icons/folder.svg",
+            Icon::FolderOpen => "icons/file_icons/folder_open.svg",
+            Icon::FolderX => "icons/stop_sharing.svg",
+            Icon::Hash => "icons/hash.svg",
+            Icon::InlayHint => "icons/inlay_hint.svg",
+            Icon::MagicWand => "icons/magic-wand.svg",
+            Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
+            Icon::Maximize => "icons/maximize.svg",
+            Icon::Menu => "icons/menu.svg",
+            Icon::MessageBubbles => "icons/conversations.svg",
+            Icon::Mic => "icons/mic.svg",
+            Icon::MicMute => "icons/mic-mute.svg",
+            Icon::Plus => "icons/plus.svg",
+            Icon::Quote => "icons/quote.svg",
+            Icon::Screen => "icons/desktop.svg",
+            Icon::Split => "icons/split.svg",
+            Icon::SplitMessage => "icons/split_message.svg",
+            Icon::Terminal => "icons/terminal.svg",
+            Icon::XCircle => "icons/error.svg",
+            Icon::Copilot => "icons/copilot.svg",
+            Icon::Envelope => "icons/feedback.svg",
         }
     }
 }
 
 #[derive(Element, Clone)]
-pub struct Icon {
-    asset: IconAsset,
+pub struct IconElement {
+    icon: Icon,
     color: IconColor,
+    size: IconSize,
 }
 
-pub fn icon(asset: IconAsset) -> Icon {
-    Icon {
-        asset,
-        color: IconColor::default(),
+impl IconElement {
+    pub fn new(icon: Icon) -> Self {
+        Self {
+            icon,
+            color: IconColor::default(),
+            size: IconSize::default(),
+        }
     }
-}
 
-impl Icon {
     pub fn color(mut self, color: IconColor) -> Self {
         self.color = color;
         self
     }
 
+    pub fn size(mut self, size: IconSize) -> Self {
+        self.size = size;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
         let fill = self.color.color(theme);
 
-        svg()
-            .flex_none()
-            .path(self.asset.path())
-            .size_4()
-            .fill(fill)
+        let sized_svg = match self.size {
+            IconSize::Small => svg().size_3p5(),
+            IconSize::Large => svg().size_4(),
+        };
+
+        sized_svg.flex_none().path(self.icon.path()).fill(fill)
     }
 }

crates/ui/src/elements/indicator.rs 🔗

@@ -1,33 +0,0 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ViewContext};
-
-use crate::theme;
-
-#[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/ui/src/elements/input.rs 🔗

@@ -1,10 +1,13 @@
-use gpui2::elements::div;
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
 use crate::prelude::*;
 use crate::theme;
 
+#[derive(Default, PartialEq)]
+pub enum InputVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
 #[derive(Element)]
 pub struct Input {
     placeholder: &'static str,
@@ -13,24 +16,26 @@ pub struct Input {
     variant: InputVariant,
 }
 
-pub fn input(placeholder: &'static str) -> Input {
-    Input {
-        placeholder,
-        value: "".to_string(),
-        state: InteractionState::default(),
-        variant: InputVariant::default(),
+impl Input {
+    pub fn new(placeholder: &'static str) -> Self {
+        Self {
+            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

crates/ui/src/elements/label.rs 🔗

@@ -1,8 +1,8 @@
+use gpui2::{Hsla, WindowContext};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
 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 {
@@ -12,8 +12,28 @@ pub enum LabelColor {
     Created,
     Modified,
     Deleted,
+    Disabled,
     Hidden,
     Placeholder,
+    Accent,
+}
+
+impl LabelColor {
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            Self::Default => theme.middle.base.default.foreground,
+            Self::Muted => theme.middle.variant.default.foreground,
+            Self::Created => theme.middle.positive.default.foreground,
+            Self::Modified => theme.middle.warning.default.foreground,
+            Self::Deleted => theme.middle.negative.default.foreground,
+            Self::Disabled => theme.middle.base.disabled.foreground,
+            Self::Hidden => theme.middle.variant.default.foreground,
+            Self::Placeholder => theme.middle.base.disabled.foreground,
+            Self::Accent => theme.middle.accent.default.foreground,
+        }
+    }
 }
 
 #[derive(Default, PartialEq, Copy, Clone)]
@@ -25,20 +45,27 @@ pub enum LabelSize {
 
 #[derive(Element, Clone)]
 pub struct Label {
-    label: &'static str,
+    label: String,
     color: LabelColor,
     size: LabelSize,
+    highlight_indices: Vec<usize>,
+    strikethrough: bool,
 }
 
-pub fn label(label: &'static str) -> Label {
-    Label {
-        label,
-        color: LabelColor::Default,
-        size: LabelSize::Default,
+impl Label {
+    pub fn new<L>(label: L) -> Self
+    where
+        L: Into<String>,
+    {
+        Self {
+            label: label.into(),
+            color: LabelColor::Default,
+            size: LabelSize::Default,
+            highlight_indices: Vec::new(),
+            strikethrough: false,
+        }
     }
-}
 
-impl Label {
     pub fn color(mut self, color: LabelColor) -> Self {
         self.color = color;
         self
@@ -49,27 +76,86 @@ impl Label {
         self
     }
 
+    pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
+        self.highlight_indices = indices;
+        self
+    }
+
+    pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
+        self.strikethrough = strikethrough;
+        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::Muted => theme.lowest.variant.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,
-            LabelColor::Placeholder => theme.lowest.base.disabled.foreground,
-        };
-
-        let mut div = div();
-
-        if self.size == LabelSize::Small {
-            div = div.text_xs();
-        } else {
-            div = div.text_sm();
+        let highlight_color = theme.lowest.accent.default.foreground;
+
+        let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
+
+        let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
+
+        for (char_ix, char) in self.label.char_indices() {
+            let mut color = self.color.hsla(cx);
+
+            if let Some(highlight_ix) = highlight_indices.peek() {
+                if char_ix == *highlight_ix {
+                    color = highlight_color;
+
+                    highlight_indices.next();
+                }
+            }
+
+            let last_run = runs.last_mut();
+
+            let start_new_run = if let Some(last_run) = last_run {
+                if color == last_run.color {
+                    last_run.text.push(char);
+                    false
+                } else {
+                    true
+                }
+            } else {
+                true
+            };
+
+            if start_new_run {
+                runs.push(Run {
+                    text: char.to_string(),
+                    color,
+                });
+            }
         }
 
-        div.text_color(color).child(self.label.clone())
+        div()
+            .flex()
+            .when(self.strikethrough, |this| {
+                this.relative().child(
+                    div()
+                        .absolute()
+                        .top_px()
+                        .my_auto()
+                        .w_full()
+                        .h_px()
+                        .fill(LabelColor::Hidden.hsla(cx)),
+                )
+            })
+            .children(runs.into_iter().map(|run| {
+                let mut div = div();
+
+                if self.size == LabelSize::Small {
+                    div = div.text_xs();
+                } else {
+                    div = div.text_sm();
+                }
+
+                div.text_color(run.color).child(run.text)
+            }))
     }
 }
+
+/// A run of text that receives the same style.
+struct Run {
+    pub text: String,
+    pub color: Hsla,
+}

crates/ui/src/elements/player.rs 🔗

@@ -0,0 +1,132 @@
+use gpui2::{Hsla, ViewContext};
+
+use crate::theme;
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum PlayerStatus {
+    #[default]
+    Offline,
+    Online,
+    InCall,
+    Away,
+    DoNotDisturb,
+    Invisible,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum MicStatus {
+    Muted,
+    #[default]
+    Unmuted,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum VideoStatus {
+    On,
+    #[default]
+    Off,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum ScreenShareStatus {
+    Shared,
+    #[default]
+    NotShared,
+}
+
+#[derive(Clone)]
+pub struct PlayerCallStatus {
+    pub mic_status: MicStatus,
+    /// Indicates if the player is currently speaking
+    /// And the intensity of the volume coming through
+    ///
+    /// 0.0 - 1.0
+    pub voice_activity: f32,
+    pub video_status: VideoStatus,
+    pub screen_share_status: ScreenShareStatus,
+    pub in_current_project: bool,
+    pub disconnected: bool,
+    pub following: Option<Vec<Player>>,
+    pub followers: Option<Vec<Player>>,
+}
+
+impl PlayerCallStatus {
+    pub fn new() -> Self {
+        Self {
+            mic_status: MicStatus::default(),
+            voice_activity: 0.,
+            video_status: VideoStatus::default(),
+            screen_share_status: ScreenShareStatus::default(),
+            in_current_project: true,
+            disconnected: false,
+            following: None,
+            followers: None,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct Player {
+    index: usize,
+    avatar_src: String,
+    username: String,
+    status: PlayerStatus,
+}
+
+pub struct PlayerWithCallStatus {
+    player: Player,
+    call_status: PlayerCallStatus,
+}
+
+impl PlayerWithCallStatus {
+    pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
+        Self {
+            player,
+            call_status,
+        }
+    }
+
+    pub fn get_player(&self) -> &Player {
+        &self.player
+    }
+
+    pub fn get_call_status(&self) -> &PlayerCallStatus {
+        &self.call_status
+    }
+}
+
+impl Player {
+    pub fn new(index: usize, avatar_src: String, username: String) -> Self {
+        Self {
+            index,
+            avatar_src,
+            username,
+            status: Default::default(),
+        }
+    }
+
+    pub fn set_status(mut self, status: PlayerStatus) -> Self {
+        self.status = status;
+        self
+    }
+
+    pub fn cursor_color<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        let index = self.index % 8;
+        theme.players[self.index].cursor
+    }
+
+    pub fn selection_color<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        let index = self.index % 8;
+        theme.players[self.index].selection
+    }
+
+    pub fn avatar_src(&self) -> &str {
+        &self.avatar_src
+    }
+
+    pub fn index(&self) -> usize {
+        self.index
+    }
+}

crates/ui/src/elements/stack.rs 🔗

@@ -0,0 +1,31 @@
+use gpui2::elements::div::Div;
+
+use crate::prelude::*;
+
+pub trait Stack: StyleHelpers {
+    /// Horizontally stacks elements.
+    fn h_stack(self) -> Self {
+        self.flex().flex_row().items_center()
+    }
+
+    /// Vertically stacks elements.
+    fn v_stack(self) -> Self {
+        self.flex().flex_col()
+    }
+}
+
+impl<V> Stack for Div<V> {}
+
+/// Horizontally stacks elements.
+///
+/// Sets `flex()`, `flex_row()`, `items_center()`
+pub fn h_stack<V: 'static>() -> Div<V> {
+    div().h_stack()
+}
+
+/// Vertically stacks elements.
+///
+/// Sets `flex()`, `flex_col()`
+pub fn v_stack<V: 'static>() -> Div<V> {
+    div().v_stack()
+}

crates/ui/src/elements/text_button.rs 🔗

@@ -1,82 +0,0 @@
-use gpui2::elements::div;
-use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-
-use crate::prelude::*;
-use crate::theme;
-
-#[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/ui/src/elements/tool_divider.rs 🔗

@@ -1,17 +1,14 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{Element, IntoElement, ViewContext};
-
+use crate::prelude::*;
 use crate::theme;
 
 #[derive(Element)]
 pub struct ToolDivider {}
 
-pub fn tool_divider<V: 'static>() -> impl Element<V> {
-    ToolDivider {}
-}
-
 impl ToolDivider {
+    pub fn new() -> Self {
+        Self {}
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 

crates/ui/src/lib.rs 🔗

@@ -1,5 +1,6 @@
 #![allow(dead_code, unused_variables)]
 
+mod children;
 mod components;
 mod element_ext;
 mod elements;
@@ -8,10 +9,12 @@ mod static_data;
 mod theme;
 mod tokens;
 
-pub use crate::theme::*;
+pub use children::*;
 pub use components::*;
 pub use element_ext::*;
 pub use elements::*;
 pub use prelude::*;
 pub use static_data::*;
 pub use tokens::*;
+
+pub use crate::theme::*;

crates/ui/src/prelude.rs 🔗

@@ -1,23 +1,157 @@
+pub use gpui2::elements::div::{div, ScrollState};
+pub use gpui2::style::{StyleHelpers, Styleable};
+pub use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant};
+
+use gpui2::{hsla, rgb, Hsla, WindowContext};
+use strum::EnumIter;
+
+#[derive(Default)]
+pub struct SystemColor {
+    pub transparent: Hsla,
+    pub mac_os_traffic_light_red: Hsla,
+    pub mac_os_traffic_light_yellow: Hsla,
+    pub mac_os_traffic_light_green: Hsla,
+}
+
+impl SystemColor {
+    pub fn new() -> SystemColor {
+        SystemColor {
+            transparent: hsla(0.0, 0.0, 0.0, 0.0),
+            mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
+            mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
+            mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
+        }
+    }
+    pub fn color(&self) -> Hsla {
+        self.transparent
+    }
+}
+
+#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+pub enum HighlightColor {
+    #[default]
+    Default,
+    Comment,
+    String,
+    Function,
+    Keyword,
+}
+
+impl HighlightColor {
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self {
+            Self::Default => theme
+                .syntax
+                .get("primary")
+                .expect("no theme.syntax.primary")
+                .clone(),
+            Self::Comment => theme
+                .syntax
+                .get("comment")
+                .expect("no theme.syntax.comment")
+                .clone(),
+            Self::String => theme
+                .syntax
+                .get("string")
+                .expect("no theme.syntax.string")
+                .clone(),
+            Self::Function => theme
+                .syntax
+                .get("function")
+                .expect("no theme.syntax.function")
+                .clone(),
+            Self::Keyword => theme
+                .syntax
+                .get("keyword")
+                .expect("no theme.syntax.keyword")
+                .clone(),
+        }
+    }
+}
+
+#[derive(Default, PartialEq, EnumIter)]
+pub enum FileSystemStatus {
+    #[default]
+    None,
+    Conflict,
+    Deleted,
+}
+
+impl FileSystemStatus {
+    pub fn to_string(&self) -> String {
+        match self {
+            Self::None => "None".to_string(),
+            Self::Conflict => "Conflict".to_string(),
+            Self::Deleted => "Deleted".to_string(),
+        }
+    }
+}
+
+#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+pub enum GitStatus {
+    #[default]
+    None,
+    Created,
+    Modified,
+    Deleted,
+    Conflict,
+    Renamed,
+}
+
+impl GitStatus {
+    pub fn to_string(&self) -> String {
+        match self {
+            Self::None => "None".to_string(),
+            Self::Created => "Created".to_string(),
+            Self::Modified => "Modified".to_string(),
+            Self::Deleted => "Deleted".to_string(),
+            Self::Conflict => "Conflict".to_string(),
+            Self::Renamed => "Renamed".to_string(),
+        }
+    }
+
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self {
+            Self::None => system_color.transparent,
+            Self::Created => theme.lowest.positive.default.foreground,
+            Self::Modified => theme.lowest.warning.default.foreground,
+            Self::Deleted => theme.lowest.negative.default.foreground,
+            Self::Conflict => theme.lowest.warning.default.foreground,
+            Self::Renamed => theme.lowest.accent.default.foreground,
+        }
+    }
+}
+
 #[derive(Default, PartialEq)]
-pub enum OrderMethod {
+pub enum DiagnosticStatus {
     #[default]
-    Ascending,
-    Descending,
-    MostRecent,
+    None,
+    Error,
+    Warning,
+    Info,
 }
 
 #[derive(Default, PartialEq)]
-pub enum ButtonVariant {
+pub enum IconSide {
     #[default]
-    Ghost,
-    Filled,
+    Left,
+    Right,
 }
 
 #[derive(Default, PartialEq)]
-pub enum InputVariant {
+pub enum OrderMethod {
     #[default]
-    Ghost,
-    Filled,
+    Ascending,
+    Descending,
+    MostRecent,
 }
 
 #[derive(Default, PartialEq, Clone, Copy)]
@@ -34,14 +168,13 @@ pub enum DisclosureControlVisibility {
     Always,
 }
 
-#[derive(Default, PartialEq, Clone, Copy)]
+#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
 pub enum InteractionState {
     #[default]
     Enabled,
     Hovered,
     Active,
     Focused,
-    Dragged,
     Disabled,
 }
 
@@ -63,8 +196,60 @@ pub enum SelectedState {
     Selected,
 }
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Toggleable {
+    Toggleable(ToggleState),
+    #[default]
+    NotToggleable,
+}
+
+impl Toggleable {
+    pub fn is_toggled(&self) -> bool {
+        match self {
+            Self::Toggleable(ToggleState::Toggled) => true,
+            _ => false,
+        }
+    }
+}
+
+impl From<ToggleState> for Toggleable {
+    fn from(state: ToggleState) -> Self {
+        Self::Toggleable(state)
+    }
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
 pub enum ToggleState {
+    /// The "on" state of a toggleable element.
+    ///
+    /// Example:
+    ///     - A collasable list that is currently expanded
+    ///     - A toggle button that is currently on.
     Toggled,
+    /// The "off" state of a toggleable element.
+    ///
+    /// Example:
+    ///     - A collasable list that is currently collapsed
+    ///     - A toggle button that is currently off.
+    #[default]
     NotToggled,
 }
+
+impl From<Toggleable> for ToggleState {
+    fn from(toggleable: Toggleable) -> Self {
+        match toggleable {
+            Toggleable::Toggleable(state) => state,
+            Toggleable::NotToggleable => ToggleState::NotToggled,
+        }
+    }
+}
+
+impl From<bool> for ToggleState {
+    fn from(toggled: bool) -> Self {
+        if toggled {
+            ToggleState::Toggled
+        } else {
+            ToggleState::NotToggled
+        }
+    }
+}

crates/ui/src/static_data.rs 🔗

@@ -1,166 +1,558 @@
+use gpui2::WindowContext;
+
 use crate::{
-    label, list_item, palette_item, IconAsset, LabelColor, ListItem, PaletteItem, ToggleState,
+    Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText,
+    Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus,
+    ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus,
+    ToggleState,
 };
 
+pub fn static_players() -> Vec<Player> {
+    vec![
+        Player::new(
+            0,
+            "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            "nathansobo".into(),
+        ),
+        Player::new(
+            1,
+            "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            "maxbrunsfeld".into(),
+        ),
+        Player::new(
+            2,
+            "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            "as-cii".into(),
+        ),
+        Player::new(
+            3,
+            "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            "iamnbutler".into(),
+        ),
+        Player::new(
+            4,
+            "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            "maxdeviant".into(),
+        ),
+    ]
+}
+
+pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
+    let players = static_players();
+    let mut player_0_status = PlayerCallStatus::new();
+    let player_1_status = PlayerCallStatus::new();
+    let player_2_status = PlayerCallStatus::new();
+    let mut player_3_status = PlayerCallStatus::new();
+    let mut player_4_status = PlayerCallStatus::new();
+
+    player_0_status.screen_share_status = ScreenShareStatus::Shared;
+    player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]);
+
+    player_3_status.voice_activity = 0.5;
+    player_4_status.mic_status = MicStatus::Muted;
+    player_4_status.in_current_project = false;
+
+    vec![
+        PlayerWithCallStatus::new(players[0].clone(), player_0_status),
+        PlayerWithCallStatus::new(players[1].clone(), player_1_status),
+        PlayerWithCallStatus::new(players[2].clone(), player_2_status),
+        PlayerWithCallStatus::new(players[3].clone(), player_3_status),
+        PlayerWithCallStatus::new(players[4].clone(), player_4_status),
+    ]
+}
+
 pub fn static_project_panel_project_items() -> Vec<ListItem> {
     vec![
-        list_item(label("zed"))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(0)
             .set_toggle(ToggleState::Toggled),
-        list_item(label(".cargo"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label(".config"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new(".config"))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label(".git").color(LabelColor::Hidden))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label(".cargo"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label(".idea").color(LabelColor::Hidden))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label("assets"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("assets"))
+            .left_icon(Icon::Folder.into())
             .indent_level(1)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("cargo-target").color(LabelColor::Hidden))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label("crates"))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("crates"))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(1)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("activity_indicator"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("activity_indicator"))
+            .left_icon(Icon::Folder.into())
             .indent_level(2),
-        list_item(label("ai"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("ai"))
+            .left_icon(Icon::Folder.into())
             .indent_level(2),
-        list_item(label("audio"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("audio"))
+            .left_icon(Icon::Folder.into())
             .indent_level(2),
-        list_item(label("auto_update"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("auto_update"))
+            .left_icon(Icon::Folder.into())
             .indent_level(2),
-        list_item(label("breadcrumbs"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("breadcrumbs"))
+            .left_icon(Icon::Folder.into())
             .indent_level(2),
-        list_item(label("call"))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("call"))
+            .left_icon(Icon::Folder.into())
             .indent_level(2),
-        list_item(label("sqlez").color(LabelColor::Modified))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
+            .left_icon(Icon::Folder.into())
             .indent_level(2)
             .set_toggle(ToggleState::NotToggled),
-        list_item(label("gpui2"))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(2)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("src"))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("src"))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(3)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("derrive_element.rs"))
-            .left_icon(IconAsset::FileRust.into())
+        ListEntry::new(Label::new("derrive_element.rs"))
+            .left_icon(Icon::FileRust.into())
             .indent_level(4),
-        list_item(label("storybook").color(LabelColor::Modified))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(1)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("docs").color(LabelColor::Default))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("docs").color(LabelColor::Default))
+            .left_icon(Icon::Folder.into())
             .indent_level(2)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("src").color(LabelColor::Modified))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("src").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(3)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("ui").color(LabelColor::Modified))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("ui").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(4)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("component").color(LabelColor::Created))
-            .left_icon(IconAsset::FolderOpen.into())
+        ListEntry::new(Label::new("component").color(LabelColor::Created))
+            .left_icon(Icon::FolderOpen.into())
             .indent_level(5)
             .set_toggle(ToggleState::Toggled),
-        list_item(label("facepile.rs").color(LabelColor::Default))
-            .left_icon(IconAsset::FileRust.into())
+        ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        list_item(label("follow_group.rs").color(LabelColor::Default))
-            .left_icon(IconAsset::FileRust.into())
+        ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        list_item(label("list_item.rs").color(LabelColor::Created))
-            .left_icon(IconAsset::FileRust.into())
+        ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
+            .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        list_item(label("tab.rs").color(LabelColor::Default))
-            .left_icon(IconAsset::FileRust.into())
+        ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        list_item(label("target").color(LabelColor::Hidden))
-            .left_icon(IconAsset::Folder.into())
+        ListEntry::new(Label::new("target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
             .indent_level(1),
-        list_item(label(".dockerignore"))
-            .left_icon(IconAsset::File.into())
+        ListEntry::new(Label::new(".dockerignore"))
+            .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
-        list_item(label(".DS_Store").color(LabelColor::Hidden))
-            .left_icon(IconAsset::File.into())
+        ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
+            .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
-        list_item(label("Cargo.lock"))
-            .left_icon(IconAsset::FileLock.into())
+        ListEntry::new(Label::new("Cargo.lock"))
+            .left_icon(Icon::FileLock.into())
             .indent_level(1),
-        list_item(label("Cargo.toml"))
-            .left_icon(IconAsset::FileToml.into())
+        ListEntry::new(Label::new("Cargo.toml"))
+            .left_icon(Icon::FileToml.into())
             .indent_level(1),
-        list_item(label("Dockerfile"))
-            .left_icon(IconAsset::File.into())
+        ListEntry::new(Label::new("Dockerfile"))
+            .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
-        list_item(label("Procfile"))
-            .left_icon(IconAsset::File.into())
+        ListEntry::new(Label::new("Procfile"))
+            .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
-        list_item(label("README.md"))
-            .left_icon(IconAsset::FileDoc.into())
+        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() -> Vec<ListItem> {
     vec![
-        list_item(label("todo.md"))
-            .left_icon(IconAsset::FileDoc.into())
+        ListEntry::new(Label::new("todo.md"))
+            .left_icon(Icon::FileDoc.into())
             .indent_level(0),
-        list_item(label("README.md"))
-            .left_icon(IconAsset::FileDoc.into())
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
             .indent_level(0),
-        list_item(label("config.json"))
-            .left_icon(IconAsset::File.into())
+        ListEntry::new(Label::new("config.json"))
+            .left_icon(Icon::FileGeneric.into())
             .indent_level(0),
     ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_current_call() -> Vec<ListItem> {
+    vec![
+        ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
+        ListEntry::new(Label::new("nathansobo"))
+            .left_avatar("http://github.com/nathansobo.png?s=50"),
+        ListEntry::new(Label::new("maxbrunsfeld"))
+            .left_avatar("http://github.com/maxbrunsfeld.png?s=50"),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_channels() -> Vec<ListItem> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(0),
+        ListEntry::new(Label::new("community"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("dashboards"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("feedback"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("teams-in-channels-alpha"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("current-projects"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("codegen"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("livestreaming"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("open-source"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("replace"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("semantic-index"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("vim"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("web-tech"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
 }
 
 pub fn example_editor_actions() -> Vec<PaletteItem> {
     vec![
-        palette_item("New File", Some("Ctrl+N")),
-        palette_item("Open File", Some("Ctrl+O")),
-        palette_item("Save File", Some("Ctrl+S")),
-        palette_item("Cut", Some("Ctrl+X")),
-        palette_item("Copy", Some("Ctrl+C")),
-        palette_item("Paste", Some("Ctrl+V")),
-        palette_item("Undo", Some("Ctrl+Z")),
-        palette_item("Redo", Some("Ctrl+Shift+Z")),
-        palette_item("Find", Some("Ctrl+F")),
-        palette_item("Replace", Some("Ctrl+R")),
-        palette_item("Jump to Line", None),
-        palette_item("Select All", None),
-        palette_item("Deselect All", None),
-        palette_item("Switch Document", None),
-        palette_item("Insert Line Below", None),
-        palette_item("Insert Line Above", None),
-        palette_item("Move Line Up", None),
-        palette_item("Move Line Down", None),
-        palette_item("Toggle Comment", None),
-        palette_item("Delete Line", None),
+        PaletteItem::new("New File").keybinding(Keybinding::new(
+            "N".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Open File").keybinding(Keybinding::new(
+            "O".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Save File").keybinding(Keybinding::new(
+            "S".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Cut").keybinding(Keybinding::new(
+            "X".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Copy").keybinding(Keybinding::new(
+            "C".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Paste").keybinding(Keybinding::new(
+            "V".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Undo").keybinding(Keybinding::new(
+            "Z".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Redo").keybinding(Keybinding::new(
+            "Z".to_string(),
+            ModifierKeys::new().control(true).shift(true),
+        )),
+        PaletteItem::new("Find").keybinding(Keybinding::new(
+            "F".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Replace").keybinding(Keybinding::new(
+            "R".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Jump to Line"),
+        PaletteItem::new("Select All"),
+        PaletteItem::new("Deselect All"),
+        PaletteItem::new("Switch Document"),
+        PaletteItem::new("Insert Line Below"),
+        PaletteItem::new("Insert Line Above"),
+        PaletteItem::new("Move Line Up"),
+        PaletteItem::new("Move Line Down"),
+        PaletteItem::new("Toggle Comment"),
+        PaletteItem::new("Delete Line"),
+    ]
+}
+
+pub fn empty_buffer_example<V: 'static>() -> Buffer<V> {
+    Buffer::new().set_rows(Some(BufferRows::default()))
+}
+
+pub fn hello_world_rust_buffer_example<V: 'static>(cx: &WindowContext) -> Buffer<V> {
+    Buffer::new()
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_buffer_rows(cx),
+        }))
+}
+
+pub fn hello_world_rust_buffer_with_status_example<V: 'static>(cx: &WindowContext) -> Buffer<V> {
+    Buffer::new()
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_with_status_buffer_rows(cx),
+        }))
+}
+
+pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
+    let show_line_number = true;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: true,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "fn ".to_string(),
+                        color: HighlightColor::Keyword.hsla(cx),
+                    },
+                    HighlightedText {
+                        text: "main".to_string(),
+                        color: HighlightColor::Function.hsla(cx),
+                    },
+                    HighlightedText {
+                        text: "() {".to_string(),
+                        color: HighlightColor::Default.hsla(cx),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Statements here are executed when the compiled binary is called."
+                        .to_string(),
+                    color: HighlightColor::Comment.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 3,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 4,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Print text to the console.".to_string(),
+                    color: HighlightColor::Comment.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 5,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "}".to_string(),
+                    color: HighlightColor::Default.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+    ]
+}
+
+pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
+    let show_line_number = true;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: true,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "fn ".to_string(),
+                        color: HighlightColor::Keyword.hsla(cx),
+                    },
+                    HighlightedText {
+                        text: "main".to_string(),
+                        color: HighlightColor::Function.hsla(cx),
+                    },
+                    HighlightedText {
+                        text: "() {".to_string(),
+                        color: HighlightColor::Default.hsla(cx),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "// Statements here are executed when the compiled binary is called."
+                        .to_string(),
+                    color: HighlightColor::Comment.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Modified,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 3,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 4,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Print text to the console.".to_string(),
+                    color: HighlightColor::Comment.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 5,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "}".to_string(),
+                    color: HighlightColor::Default.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "".to_string(),
+                    color: HighlightColor::Default.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Created,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 7,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "Marshall and Nate were here".to_string(),
+                    color: HighlightColor::Default.hsla(cx),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Created,
+            show_line_number,
+        },
     ]
 }

crates/ui/src/tokens.rs 🔗

@@ -1,14 +1,21 @@
 use gpui2::geometry::AbsoluteLength;
+use gpui2::{hsla, Hsla};
 
 #[derive(Clone, Copy)]
 pub struct Token {
     pub list_indent_depth: AbsoluteLength,
+    pub default_panel_size: AbsoluteLength,
+    pub state_hover_background: Hsla,
+    pub state_active_background: Hsla,
 }
 
 impl Default for Token {
     fn default() -> Self {
         Self {
             list_indent_depth: AbsoluteLength::Rems(0.5),
+            default_panel_size: AbsoluteLength::Rems(16.),
+            state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
+            state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
         }
     }
 }