Mainline GPUI2 UI work (#3062)

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

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Nate <nate@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Marshall Bowers 2023-09-28 19:36:21 -04:00 committed by GitHub
parent e7ee8a95f6
commit f26ca0866c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 4658 additions and 1623 deletions

View file

@ -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

View file

@ -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),
)
}
}

View file

@ -1,2 +1,3 @@
pub mod components;
pub mod elements;
pub mod kitchen_sink;

View file

@ -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;

View file

@ -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())
}
}

View file

@ -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())
}
}

View file

@ -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)),
)
}
}

View file

@ -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(),
),
]))
}
}

View file

@ -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()))
}
}

View file

@ -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")),
]))
}
}

View file

@ -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)))
})
}
}

View file

@ -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)),
))
}
}

View file

@ -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())),
]),
)
}
}

View file

@ -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(()),
))
}
}

View file

@ -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()))
}
}

View file

@ -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())
}
}

View file

@ -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)
}))),
)
}
}

View file

@ -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()))
}
}

View file

@ -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())
}
}

View file

@ -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))
}
}

View file

@ -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())
}
}

View file

@ -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))
}
}

View file

@ -1 +1,5 @@
pub mod avatar;
pub mod button;
pub mod icon;
pub mod input;
pub mod label;

View file

@ -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),
)
}

View file

@ -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.")),
)
}
}

View file

@ -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)))
}
}

View file

@ -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")))
}
}

View file

@ -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]))
}
}

View file

@ -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()),
)
}
}

View file

@ -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())
}
}

View file

@ -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();
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<_>>()
});
stories
}
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);
element_stories.chain(component_stories).collect::<Vec<_>>()
});
stories
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))

View file

@ -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"]

View file

@ -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())
}
}