Detailed changes
@@ -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",
]
@@ -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> {
@@ -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
@@ -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),
- )
- }
-}
@@ -1,2 +1,3 @@
pub mod components;
pub mod elements;
+pub mod kitchen_sink;
@@ -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;
@@ -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())
+ }
+}
@@ -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())
}
}
@@ -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)),
+ )
+ }
+}
@@ -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(),
+ ),
+ ]))
+ }
+}
@@ -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()))
+ }
+}
@@ -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")),
+ ]))
+ }
+}
@@ -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)))
- })
}
}
@@ -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)),
+ ))
+ }
+}
@@ -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())),
+ ]),
+ )
+ }
+}
@@ -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(()),
+ ))
+ }
+}
@@ -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()))
+ }
+}
@@ -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())
+ }
+}
@@ -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)
+ }))),
+ )
+ }
+}
@@ -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()))
+ }
+}
@@ -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())
+ }
+}
@@ -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))
+ }
+}
@@ -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())
}
}
@@ -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))
}
}
@@ -1 +1,5 @@
pub mod avatar;
+pub mod button;
+pub mod icon;
+pub mod input;
+pub mod label;
@@ -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),
)
}
@@ -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.")),
+ )
+ }
+}
@@ -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)))
+ }
+}
@@ -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")))
+ }
+}
@@ -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]))
+ }
+}
@@ -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()),
+ )
+ }
+}
@@ -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())
}
}
@@ -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))
@@ -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"]
@@ -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())
- }
-}
@@ -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" }
@@ -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.
@@ -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>;
@@ -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
+// }
+// }
+// }
@@ -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.))
+ }
+}
@@ -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()
@@ -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)
+ }
+}
@@ -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())))
}
}
@@ -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(
@@ -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.")
@@ -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())
+ }
+}
@@ -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())
+ }
+}
@@ -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)
}
@@ -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())),
- )
- }
-}
@@ -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))
}
}
@@ -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
+ }
+}
@@ -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)
}
}
@@ -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()),
- )
- }
-}
@@ -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),
- )
- }
-}
@@ -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())
+ }
+}
@@ -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()),
- )
- }
-}
@@ -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()))
+ }
+}
@@ -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!()
+ }
+}
@@ -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()))
+ })),
+ )
+ }
+}
@@ -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(),
+ }),
+ )
}
}
@@ -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)),
)
}
}
@@ -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
+ }),
)
}
}
@@ -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)),
),
)
}
@@ -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(()),
+ ))
+ }
+}
@@ -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),
),
),
@@ -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)),
)
}
}
@@ -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())
-}
@@ -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())
}
}
@@ -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::*;
@@ -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
@@ -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
+ }
+}
@@ -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
@@ -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)
}
}
@@ -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)
- }
-}
@@ -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
@@ -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,
+}
@@ -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
+ }
+}
@@ -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()
+}
@@ -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())
- }
-}
@@ -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);
@@ -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::*;
@@ -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
+ }
+ }
+}
@@ -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,
+ },
]
}
@@ -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),
}
}
}