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

5
Cargo.lock generated
View file

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

View file

@ -198,6 +198,31 @@ pub trait ParentElement<V: 'static> {
); );
self 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> { pub trait IntoElement<V: 'static> {

View file

@ -11,7 +11,9 @@ path = "src/storybook.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
clap = { version = "4.4", features = ["derive", "string"] } clap = { version = "4.4", features = ["derive", "string"] }
chrono = "0.4"
gpui2 = { path = "../gpui2" } gpui2 = { path = "../gpui2" }
itertools = "0.11.0"
log.workspace = true log.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
serde.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 components;
pub mod elements; pub mod elements;
pub mod kitchen_sink;

View file

@ -1,4 +1,18 @@
pub mod assistant_panel;
pub mod breadcrumb; pub mod breadcrumb;
pub mod buffer;
pub mod chat_panel;
pub mod collab_panel;
pub mod context_menu;
pub mod facepile; 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 toolbar;
pub mod traffic_lights; 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::prelude::*;
use ui::breadcrumb; use ui::Breadcrumb;
use crate::story::Story; use crate::story::Story;
@ -8,9 +8,9 @@ pub struct BreadcrumbStory {}
impl BreadcrumbStory { impl BreadcrumbStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container() Story::container(cx)
.child(Story::title_for::<_, ui::Breadcrumb>()) .child(Story::title_for::<_, Breadcrumb>(cx))
.child(Story::label("Default")) .child(Story::label(cx, "Default"))
.child(breadcrumb()) .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::prelude::*;
use ui::{avatar, facepile, theme}; use ui::{static_players, Facepile};
use crate::story::Story; use crate::story::Story;
@ -11,40 +8,18 @@ pub struct FacepileStory {}
impl FacepileStory { impl FacepileStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { 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![ Story::container(cx)
avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), .child(Story::title_for::<_, ui::Facepile>(cx))
avatar("https://avatars.githubusercontent.com/u/482957?v=4"), .child(Story::label(cx, "Default"))
avatar("https://avatars.githubusercontent.com/u/1789?v=4"),
];
Story::container()
.child(Story::title_for::<_, ui::Facepile>())
.child(Story::label("Default"))
.child( .child(
div() div()
.flex() .flex()
.gap_3() .gap_3()
.child(facepile(avatars.clone().into_iter().take(1))) .child(Facepile::new(players.clone().into_iter().take(1)))
.child(facepile(avatars.clone().into_iter().take(2))) .child(Facepile::new(players.clone().into_iter().take(2)))
.child(facepile(avatars.clone().into_iter().take(3))), .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::prelude::*;
use ui::toolbar; use ui::Toolbar;
use crate::story::Story; use crate::story::Story;
@ -8,9 +8,9 @@ pub struct ToolbarStory {}
impl ToolbarStory { impl ToolbarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container() Story::container(cx)
.child(Story::title_for::<_, ui::Toolbar>()) .child(Story::title_for::<_, Toolbar>(cx))
.child(Story::label("Default")) .child(Story::label(cx, "Default"))
.child(toolbar()) .child(Toolbar::new())
} }
} }

View file

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

View file

@ -1 +1,5 @@
pub mod avatar; 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::prelude::*;
use ui::{avatar, theme}; use ui::Avatar;
use crate::story::Story; use crate::story::Story;
@ -9,17 +8,15 @@ pub struct AvatarStory {}
impl AvatarStory { impl AvatarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); Story::container(cx)
.child(Story::title_for::<_, ui::Avatar>(cx))
Story::container() .child(Story::label(cx, "Default"))
.child(Story::title_for::<_, ui::Avatar>()) .child(Avatar::new(
.child(Story::label("Default"))
.child(avatar(
"https://avatars.githubusercontent.com/u/1714999?v=4", "https://avatars.githubusercontent.com/u/1714999?v=4",
)) ))
.child(Story::label("Rounded rectangle")) .child(Story::label(cx, "Rounded rectangle"))
.child( .child(
avatar("https://avatars.githubusercontent.com/u/1714999?v=4") Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
.shape(Shape::RoundedRectangle), .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::elements::div::Div;
use gpui2::style::StyleHelpers; use ui::prelude::*;
use gpui2::{rgb, Element, Hsla, ParentElement}; use ui::theme;
pub struct Story {} pub struct Story {}
impl 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() div()
.size_full() .size_full()
.flex() .flex()
@ -13,26 +15,30 @@ impl Story {
.pt_2() .pt_2()
.px_4() .px_4()
.font("Zed Mono Extended") .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() div()
.text_xl() .text_xl()
.text_color(rgb::<Hsla>(0xffffff)) .text_color(theme.lowest.base.default.foreground)
.child(title.to_owned()) .child(title.to_owned())
} }
pub fn title_for<V: 'static, T>() -> impl Element<V> { pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Element<V> {
Self::title(std::any::type_name::<T>()) 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() div()
.mt_4() .mt_4()
.mb_2() .mb_2()
.text_xs() .text_xs()
.text_color(rgb::<Hsla>(0xffffff)) .text_color(theme.lowest.base.default.foreground)
.child(label.to_owned()) .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 anyhow::{anyhow, Context};
use clap::builder::PossibleValue; use clap::builder::PossibleValue;
use clap::ValueEnum; use clap::ValueEnum;
use gpui2::{AnyElement, Element};
use strum::{EnumIter, EnumString, IntoEnumIterator}; 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")] #[strum(serialize_all = "snake_case")]
pub enum ElementStory { pub enum ElementStory {
Avatar, 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")] #[strum(serialize_all = "snake_case")]
pub enum ComponentStory { pub enum ComponentStory {
AssistantPanel,
Breadcrumb, Breadcrumb,
Buffer,
ContextMenu,
ChatPanel,
CollabPanel,
Facepile, Facepile,
Keybinding,
Palette,
Panel,
ProjectPanel,
StatusBar,
Tab,
TabBar,
Terminal,
TitleBar,
Toolbar, Toolbar,
TrafficLights, 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 { pub enum StorySelector {
Element(ElementStory), Element(ElementStory),
Component(ComponentStory), Component(ComponentStory),
KitchenSink,
} }
impl FromStr for StorySelector { 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> { fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
let story = raw_story_name.to_ascii_lowercase(); let story = raw_story_name.to_ascii_lowercase();
if story == "kitchen_sink" {
return Ok(Self::KitchenSink);
}
if let Some((_, story)) = story.split_once("elements/") { if let Some((_, story)) = story.split_once("elements/") {
let element_story = ElementStory::from_str(story) let element_story = ElementStory::from_str(story)
.with_context(|| format!("story not found for element '{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. /// 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 { impl ValueEnum for StorySelector {
fn value_variants<'a>() -> &'a [Self] { fn value_variants<'a>() -> &'a [Self] {
let stories = ALL_STORIES.get_or_init(|| { all_story_selectors()
let element_stories = ElementStory::iter().map(Self::Element);
let component_stories = ComponentStory::iter().map(Self::Component);
element_stories.chain(component_stories).collect::<Vec<_>>()
});
stories
} }
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> { fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
let value = match self { let value = match self {
Self::Element(story) => format!("elements/{story}"), Self::Element(story) => format!("elements/{story}"),
Self::Component(story) => format!("components/{story}"), Self::Component(story) => format!("components/{story}"),
Self::KitchenSink => "kitchen_sink".to_string(),
}; };
Some(PossibleValue::new(value)) Some(PossibleValue::new(value))

View file

@ -1,26 +1,24 @@
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
mod collab_panel;
mod stories; mod stories;
mod story; mod story;
mod story_selector; mod story_selector;
mod workspace;
use std::sync::Arc;
use ::theme as legacy_theme; use ::theme as legacy_theme;
use clap::Parser; use clap::Parser;
use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds}; use gpui2::{
use legacy_theme::ThemeSettings; serde_json, vec2f, view, Element, IntoElement, ParentElement, RectF, ViewContext, WindowBounds,
};
use legacy_theme::{ThemeRegistry, ThemeSettings};
use log::LevelFilter; use log::LevelFilter;
use settings::{default_settings, SettingsStore}; use settings::{default_settings, SettingsStore};
use simplelog::SimpleLogger; use simplelog::SimpleLogger;
use stories::components::breadcrumb::BreadcrumbStory; use ui::prelude::*;
use stories::components::facepile::FacepileStory; use ui::{ElementExt, Theme, WorkspaceElement};
use stories::components::toolbar::ToolbarStory;
use stories::components::traffic_lights::TrafficLightsStory;
use stories::elements::avatar::AvatarStory;
use ui::{ElementExt, Theme};
use crate::story_selector::{ComponentStory, ElementStory, StorySelector}; use crate::story_selector::StorySelector;
gpui2::actions! { gpui2::actions! {
storybook, storybook,
@ -32,6 +30,12 @@ gpui2::actions! {
struct Args { struct Args {
#[arg(value_enum)] #[arg(value_enum)]
story: Option<StorySelector>, 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() { fn main() {
@ -48,31 +52,60 @@ fn main() {
legacy_theme::init(Assets, cx); legacy_theme::init(Assets, cx);
// load_embedded_fonts(cx.platform().as_ref()); // 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( cx.add_window(
gpui2::WindowOptions { 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, center: true,
..Default::default() ..Default::default()
}, },
|cx| match args.story { |cx| match args.story {
Some(StorySelector::Element(ElementStory::Avatar)) => { // HACK: Special-case the kitchen sink to fix scrolling.
view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default())) // There is something about going through `children_any` that messes
} // with the scroll interactions.
Some(StorySelector::Component(ComponentStory::Breadcrumb)) => { Some(StorySelector::KitchenSink) => view(move |cx| {
view(|cx| render_story(&mut ViewContext::new(cx), BreadcrumbStory::default())) render_story(
} &mut ViewContext::new(cx),
Some(StorySelector::Component(ComponentStory::Facepile)) => { theme_override.clone(),
view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default())) crate::stories::kitchen_sink::KitchenSinkStory::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())
}), }),
None => { // HACK: Special-case the panel story to fix scrolling.
view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default())) // 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); cx.platform().activate(true);
@ -81,23 +114,32 @@ fn main() {
fn render_story<V: 'static, S: IntoElement<V>>( fn render_story<V: 'static, S: IntoElement<V>>(
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
theme_override: Option<Arc<legacy_theme::Theme>>,
story: S, story: S,
) -> impl Element<V> { ) -> 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. // 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 { fn add_base_theme_to_legacy_theme(legacy_theme: &legacy_theme::Theme, new_theme: Theme) -> Theme {
settings::get::<ThemeSettings>(cx) legacy_theme
.theme
.deserialized_base_theme .deserialized_base_theme
.lock() .lock()
.get_or_insert_with(|| { .get_or_insert_with(|| Box::new(new_theme))
let theme: Theme =
serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
.unwrap();
Box::new(theme)
})
.downcast_ref::<Theme>() .downcast_ref::<Theme>()
.unwrap() .unwrap()
.clone() .clone()
@ -106,7 +148,6 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use gpui2::AssetSource; use gpui2::AssetSource;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use workspace::WorkspaceElement;
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "../../assets"] #[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())
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,29 +1,27 @@
use gpui2::elements::div; use crate::prelude::*;
use gpui2::style::StyleHelpers; use crate::{theme, Avatar, Player};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::{theme, Avatar};
#[derive(Element)] #[derive(Element)]
pub struct Facepile { 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 { impl Facepile {
pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
Self {
players: players.collect(),
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); let theme = theme(cx);
let player_count = self.players.len(); let player_count = self.players.len();
let player_list = self.players.iter().enumerate().map(|(ix, player)| { let player_list = self.players.iter().enumerate().map(|(ix, player)| {
let isnt_last = ix < player_count - 1; let isnt_last = ix < player_count - 1;
div() div()
.when(isnt_last, |div| div.neg_mr_1()) .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) div().p_1().flex().items_center().children(player_list)
} }

View file

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

View file

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

View file

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

View file

@ -1,36 +1,294 @@
use crate::theme::theme; use gpui2::elements::div::Div;
use crate::tokens::token; use gpui2::{Hsla, WindowContext};
use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)] use crate::prelude::*;
pub struct List { use crate::{
header: Option<ListSectionHeader>, h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor,
items: Vec<ListItem>, IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor,
empty_message: &'static str, ToggleState,
toggle: Option<ToggleState>, };
// footer: Option<ListSectionFooter>,
#[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 { #[derive(Element, Clone, Copy)]
List { pub struct ListHeader {
header: None, label: &'static str,
items, left_icon: Option<Icon>,
empty_message: "No items", variant: ListItemVariant,
toggle: None, 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(),
}
} }
}
impl List { pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
pub fn header(mut self, header: ListSectionHeader) -> Self { self.toggleable = toggle.into();
self.header = Some(header);
self self
} }
pub fn empty_message(mut self, empty_message: &'static str) -> Self { pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
self.empty_message = empty_message; 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),
)
}
}
#[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
}
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 self
} }
@ -39,26 +297,216 @@ impl List {
self self
} }
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { 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 theme = theme(cx);
let token = token(); let token = token();
let disclosure_control = match self.toggle { let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)), IconElement::new(Icon::ChevronDown)
Some(ToggleState::Toggled) => Some(icon(IconAsset::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 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, None => None,
}; };
let sized_item = match self.size {
ListEntrySize::Small => div().h_6(),
ListEntrySize::Medium => div().h_7(),
};
div() div()
.fill(background_color)
.when(self.state == InteractionState::Focused, |this| {
this.border()
.border_color(theme.lowest.accent.default.border)
})
.relative()
.py_1() .py_1()
.flex() .child(
.flex_col() sized_item
.children(self.header.map(|h| h)) .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
.children( // .ml(rems(0.75 * self.indent_level as f32))
self.items .children((0..self.indent_level).map(|_| {
.is_empty() div()
.then(|| label(self.empty_message).color(LabelColor::Muted)), .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()),
) )
.children(self.items.iter().cloned()) }
}
#[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()
.children(
self.header
.clone()
.map(|header| header.set_toggleable(self.toggleable)),
)
.child(list_content)
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,22 +1,96 @@
use gpui2::elements::div; use crate::prelude::*;
use gpui2::style::{StyleHelpers, Styleable}; use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
use crate::theme;
#[derive(Element)] #[derive(Element)]
pub struct Tab { pub struct Tab {
title: &'static str, title: String,
enabled: bool, icon: Option<Icon>,
} current: bool,
dirty: bool,
pub fn tab<V: 'static>(title: &'static str, enabled: bool) -> impl Element<V> { fs_status: FileSystemStatus,
Tab { title, enabled } git_status: GitStatus,
diagnostic_status: DiagnosticStatus,
close_side: IconSide,
} }
impl Tab { 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> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); 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() div()
.px_2() .px_2()
@ -24,33 +98,34 @@ impl Tab {
.flex() .flex()
.items_center() .items_center()
.justify_center() .justify_center()
.rounded_lg() .fill(if self.current {
.fill(if self.enabled {
theme.highest.on.default.background
} else {
theme.highest.base.default.background theme.highest.base.default.background
})
.hover()
.fill(if self.enabled {
theme.highest.on.hovered.background
} else { } else {
theme.highest.base.hovered.background theme.middle.base.default.background
})
.active()
.fill(if self.enabled {
theme.highest.on.pressed.background
} else {
theme.highest.base.pressed.background
}) })
.child( .child(
div() div()
.text_sm() .px_1()
.text_color(if self.enabled { .flex()
theme.highest.base.default.foreground .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 { } else {
theme.highest.variant.default.foreground None
}) })
.child(self.title), .child(label)
.children(if self.close_side == IconSide::Right {
Some(close_icon)
} else {
None
}),
) )
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,30 +1,78 @@
use gpui2::elements::div; use crate::prelude::*;
use gpui2::style::StyleHelpers; use crate::{theme, token, SystemColor};
use gpui2::{Element, Hsla, IntoElement, ParentElement, ViewContext};
use crate::theme; #[derive(Clone, Copy)]
enum TrafficLightColor {
Red,
Yellow,
Green,
}
#[derive(Element)] #[derive(Element)]
pub struct TrafficLights {} struct TrafficLight {
color: TrafficLightColor,
window_has_focus: bool,
}
pub fn traffic_lights() -> TrafficLights { impl TrafficLight {
TrafficLights {} 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)
}
}
#[derive(Element)]
pub struct TrafficLights {
window_has_focus: bool,
} }
impl TrafficLights { 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> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); let theme = theme(cx);
let token = token();
div() div()
.flex() .flex()
.items_center() .items_center()
.gap_2() .gap_2()
.child(traffic_light(theme.lowest.negative.default.foreground)) .child(TrafficLight::new(
.child(traffic_light(theme.lowest.warning.default.foreground)) TrafficLightColor::Red,
.child(traffic_light(theme.lowest.positive.default.foreground)) 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())
}

View file

@ -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::{ use crate::prelude::*;
elements::{div, div::ScrollState}, use crate::{
style::StyleHelpers, theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide,
Element, IntoElement, ParentElement, ViewContext, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
}; };
#[derive(Element, Default)] #[derive(Element, Default)]
struct WorkspaceElement { pub struct WorkspaceElement {
project_panel_scroll_state: ScrollState, left_panel_scroll_state: ScrollState,
collab_panel_scroll_state: ScrollState, right_panel_scroll_state: ScrollState,
right_scroll_state: ScrollState,
tab_bar_scroll_state: ScrollState, tab_bar_scroll_state: ScrollState,
palette_scroll_state: ScrollState, bottom_panel_scroll_state: ScrollState,
}
pub fn workspace<V: 'static>() -> impl Element<V> {
WorkspaceElement::default()
} }
impl WorkspaceElement { impl WorkspaceElement {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> { 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() div()
// Elevation Level 0
.size_full() .size_full()
.flex() .flex()
.flex_col() .flex_col()
@ -34,9 +72,7 @@ impl WorkspaceElement {
.items_start() .items_start()
.text_color(theme.lowest.base.default.foreground) .text_color(theme.lowest.base.default.foreground)
.fill(theme.lowest.base.default.background) .fill(theme.lowest.base.default.background)
.relative() .child(TitleBar::new(cx))
// Elevation Level 1
.child(title_bar())
.child( .child(
div() div()
.flex_1() .flex_1()
@ -44,37 +80,57 @@ impl WorkspaceElement {
.flex() .flex()
.flex_row() .flex_row()
.overflow_hidden() .overflow_hidden()
.child(project_panel(self.project_panel_scroll_state.clone())) .border_t()
.child(collab_panel(self.collab_panel_scroll_state.clone())) .border_b()
.border_color(theme.lowest.base.default.border)
.child( .child(
div() ProjectPanel::new(self.left_panel_scroll_state.clone())
.h_full() .side(PanelSide::Left),
)
.child(
v_stack()
.flex_1() .flex_1()
.fill(theme.highest.base.default.background) .h_full()
.child( .child(
div() div()
.flex() .flex()
.flex_col()
.flex_1() .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()) .child(StatusBar::new())
// 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())),
// )
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
use gpui2::{Hsla, WindowContext};
use smallvec::SmallVec;
use crate::prelude::*;
use crate::theme::theme; 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)] #[derive(Default, PartialEq, Copy, Clone)]
pub enum LabelColor { pub enum LabelColor {
@ -12,8 +12,28 @@ pub enum LabelColor {
Created, Created,
Modified, Modified,
Deleted, Deleted,
Disabled,
Hidden, Hidden,
Placeholder, 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)] #[derive(Default, PartialEq, Copy, Clone)]
@ -25,20 +45,27 @@ pub enum LabelSize {
#[derive(Element, Clone)] #[derive(Element, Clone)]
pub struct Label { pub struct Label {
label: &'static str, label: String,
color: LabelColor, color: LabelColor,
size: LabelSize, 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 { 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,
}
}
pub fn color(mut self, color: LabelColor) -> Self { pub fn color(mut self, color: LabelColor) -> Self {
self.color = color; self.color = color;
self self
@ -49,27 +76,86 @@ impl Label {
self 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> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); let theme = theme(cx);
let color = match self.color { let highlight_color = theme.lowest.accent.default.foreground;
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(); let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
if self.size == LabelSize::Small { let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
div = div.text_xs();
} else { for (char_ix, char) in self.label.char_indices() {
div = div.text_sm(); 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,
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,151 @@
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 DiagnosticStatus {
#[default]
None,
Error,
Warning,
Info,
}
#[derive(Default, PartialEq)]
pub enum IconSide {
#[default]
Left,
Right,
}
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
pub enum OrderMethod { pub enum OrderMethod {
#[default] #[default]
@ -6,20 +154,6 @@ pub enum OrderMethod {
MostRecent, MostRecent,
} }
#[derive(Default, PartialEq)]
pub enum ButtonVariant {
#[default]
Ghost,
Filled,
}
#[derive(Default, PartialEq)]
pub enum InputVariant {
#[default]
Ghost,
Filled,
}
#[derive(Default, PartialEq, Clone, Copy)] #[derive(Default, PartialEq, Clone, Copy)]
pub enum Shape { pub enum Shape {
#[default] #[default]
@ -34,14 +168,13 @@ pub enum DisclosureControlVisibility {
Always, Always,
} }
#[derive(Default, PartialEq, Clone, Copy)] #[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
pub enum InteractionState { pub enum InteractionState {
#[default] #[default]
Enabled, Enabled,
Hovered, Hovered,
Active, Active,
Focused, Focused,
Dragged,
Disabled, Disabled,
} }
@ -63,8 +196,60 @@ pub enum SelectedState {
Selected, 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 { 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, 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, 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
}
}
}

View file

@ -1,166 +1,558 @@
use gpui2::WindowContext;
use crate::{ 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> { pub fn static_project_panel_project_items() -> Vec<ListItem> {
vec![ vec![
list_item(label("zed")) ListEntry::new(Label::new("zed"))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(0) .indent_level(0)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label(".cargo")) ListEntry::new(Label::new(".cargo"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label(".config")) ListEntry::new(Label::new(".config"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label(".git").color(LabelColor::Hidden)) ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label(".cargo")) ListEntry::new(Label::new(".cargo"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label(".idea").color(LabelColor::Hidden)) ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label("assets")) ListEntry::new(Label::new("assets"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1) .indent_level(1)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("cargo-target").color(LabelColor::Hidden)) ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label("crates")) ListEntry::new(Label::new("crates"))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(1) .indent_level(1)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("activity_indicator")) ListEntry::new(Label::new("activity_indicator"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2), .indent_level(2),
list_item(label("ai")) ListEntry::new(Label::new("ai"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2), .indent_level(2),
list_item(label("audio")) ListEntry::new(Label::new("audio"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2), .indent_level(2),
list_item(label("auto_update")) ListEntry::new(Label::new("auto_update"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2), .indent_level(2),
list_item(label("breadcrumbs")) ListEntry::new(Label::new("breadcrumbs"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2), .indent_level(2),
list_item(label("call")) ListEntry::new(Label::new("call"))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2), .indent_level(2),
list_item(label("sqlez").color(LabelColor::Modified)) ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2) .indent_level(2)
.set_toggle(ToggleState::NotToggled), .set_toggle(ToggleState::NotToggled),
list_item(label("gpui2")) ListEntry::new(Label::new("gpui2"))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(2) .indent_level(2)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("src")) ListEntry::new(Label::new("src"))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(3) .indent_level(3)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("derrive_element.rs")) ListEntry::new(Label::new("derrive_element.rs"))
.left_icon(IconAsset::FileRust.into()) .left_icon(Icon::FileRust.into())
.indent_level(4), .indent_level(4),
list_item(label("storybook").color(LabelColor::Modified)) ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(1) .indent_level(1)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("docs").color(LabelColor::Default)) ListEntry::new(Label::new("docs").color(LabelColor::Default))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(2) .indent_level(2)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("src").color(LabelColor::Modified)) ListEntry::new(Label::new("src").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(3) .indent_level(3)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("ui").color(LabelColor::Modified)) ListEntry::new(Label::new("ui").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(4) .indent_level(4)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("component").color(LabelColor::Created)) ListEntry::new(Label::new("component").color(LabelColor::Created))
.left_icon(IconAsset::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
.indent_level(5) .indent_level(5)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
list_item(label("facepile.rs").color(LabelColor::Default)) ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
.left_icon(IconAsset::FileRust.into()) .left_icon(Icon::FileRust.into())
.indent_level(6), .indent_level(6),
list_item(label("follow_group.rs").color(LabelColor::Default)) ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
.left_icon(IconAsset::FileRust.into()) .left_icon(Icon::FileRust.into())
.indent_level(6), .indent_level(6),
list_item(label("list_item.rs").color(LabelColor::Created)) ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
.left_icon(IconAsset::FileRust.into()) .left_icon(Icon::FileRust.into())
.indent_level(6), .indent_level(6),
list_item(label("tab.rs").color(LabelColor::Default)) ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
.left_icon(IconAsset::FileRust.into()) .left_icon(Icon::FileRust.into())
.indent_level(6), .indent_level(6),
list_item(label("target").color(LabelColor::Hidden)) ListEntry::new(Label::new("target").color(LabelColor::Hidden))
.left_icon(IconAsset::Folder.into()) .left_icon(Icon::Folder.into())
.indent_level(1), .indent_level(1),
list_item(label(".dockerignore")) ListEntry::new(Label::new(".dockerignore"))
.left_icon(IconAsset::File.into()) .left_icon(Icon::FileGeneric.into())
.indent_level(1), .indent_level(1),
list_item(label(".DS_Store").color(LabelColor::Hidden)) ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
.left_icon(IconAsset::File.into()) .left_icon(Icon::FileGeneric.into())
.indent_level(1), .indent_level(1),
list_item(label("Cargo.lock")) ListEntry::new(Label::new("Cargo.lock"))
.left_icon(IconAsset::FileLock.into()) .left_icon(Icon::FileLock.into())
.indent_level(1), .indent_level(1),
list_item(label("Cargo.toml")) ListEntry::new(Label::new("Cargo.toml"))
.left_icon(IconAsset::FileToml.into()) .left_icon(Icon::FileToml.into())
.indent_level(1), .indent_level(1),
list_item(label("Dockerfile")) ListEntry::new(Label::new("Dockerfile"))
.left_icon(IconAsset::File.into()) .left_icon(Icon::FileGeneric.into())
.indent_level(1), .indent_level(1),
list_item(label("Procfile")) ListEntry::new(Label::new("Procfile"))
.left_icon(IconAsset::File.into()) .left_icon(Icon::FileGeneric.into())
.indent_level(1), .indent_level(1),
list_item(label("README.md")) ListEntry::new(Label::new("README.md"))
.left_icon(IconAsset::FileDoc.into()) .left_icon(Icon::FileDoc.into())
.indent_level(1), .indent_level(1),
] ]
.into_iter()
.map(From::from)
.collect()
} }
pub fn static_project_panel_single_items() -> Vec<ListItem> { pub fn static_project_panel_single_items() -> Vec<ListItem> {
vec![ vec![
list_item(label("todo.md")) ListEntry::new(Label::new("todo.md"))
.left_icon(IconAsset::FileDoc.into()) .left_icon(Icon::FileDoc.into())
.indent_level(0), .indent_level(0),
list_item(label("README.md")) ListEntry::new(Label::new("README.md"))
.left_icon(IconAsset::FileDoc.into()) .left_icon(Icon::FileDoc.into())
.indent_level(0), .indent_level(0),
list_item(label("config.json")) ListEntry::new(Label::new("config.json"))
.left_icon(IconAsset::File.into()) .left_icon(Icon::FileGeneric.into())
.indent_level(0), .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> { pub fn example_editor_actions() -> Vec<PaletteItem> {
vec![ vec![
palette_item("New File", Some("Ctrl+N")), PaletteItem::new("New File").keybinding(Keybinding::new(
palette_item("Open File", Some("Ctrl+O")), "N".to_string(),
palette_item("Save File", Some("Ctrl+S")), ModifierKeys::new().control(true),
palette_item("Cut", Some("Ctrl+X")), )),
palette_item("Copy", Some("Ctrl+C")), PaletteItem::new("Open File").keybinding(Keybinding::new(
palette_item("Paste", Some("Ctrl+V")), "O".to_string(),
palette_item("Undo", Some("Ctrl+Z")), ModifierKeys::new().control(true),
palette_item("Redo", Some("Ctrl+Shift+Z")), )),
palette_item("Find", Some("Ctrl+F")), PaletteItem::new("Save File").keybinding(Keybinding::new(
palette_item("Replace", Some("Ctrl+R")), "S".to_string(),
palette_item("Jump to Line", None), ModifierKeys::new().control(true),
palette_item("Select All", None), )),
palette_item("Deselect All", None), PaletteItem::new("Cut").keybinding(Keybinding::new(
palette_item("Switch Document", None), "X".to_string(),
palette_item("Insert Line Below", None), ModifierKeys::new().control(true),
palette_item("Insert Line Above", None), )),
palette_item("Move Line Up", None), PaletteItem::new("Copy").keybinding(Keybinding::new(
palette_item("Move Line Down", None), "C".to_string(),
palette_item("Toggle Comment", None), ModifierKeys::new().control(true),
palette_item("Delete Line", None), )),
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,
},
] ]
} }

View file

@ -1,14 +1,21 @@
use gpui2::geometry::AbsoluteLength; use gpui2::geometry::AbsoluteLength;
use gpui2::{hsla, Hsla};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Token { pub struct Token {
pub list_indent_depth: AbsoluteLength, pub list_indent_depth: AbsoluteLength,
pub default_panel_size: AbsoluteLength,
pub state_hover_background: Hsla,
pub state_active_background: Hsla,
} }
impl Default for Token { impl Default for Token {
fn default() -> Self { fn default() -> Self {
Self { Self {
list_indent_depth: AbsoluteLength::Rems(0.5), 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),
} }
} }
} }