From f26ca0866c4c097177eda3eabbeae9c8e9a2fa3a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 28 Sep 2023 19:36:21 -0400 Subject: [PATCH] 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 Co-authored-by: Max Brunsfeld 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 Co-authored-by: Mikayla --- Cargo.lock | 5 + crates/gpui2/src/element.rs | 25 + crates/storybook/Cargo.toml | 2 + crates/storybook/src/collab_panel.rs | 177 ------ crates/storybook/src/stories.rs | 1 + crates/storybook/src/stories/components.rs | 14 + .../src/stories/components/assistant_panel.rs | 16 + .../src/stories/components/breadcrumb.rs | 12 +- .../src/stories/components/buffer.rs | 34 + .../src/stories/components/chat_panel.rs | 34 + .../src/stories/components/collab_panel.rs | 16 + .../src/stories/components/context_menu.rs | 21 + .../src/stories/components/facepile.rs | 41 +- .../src/stories/components/keybinding.rs | 64 ++ .../src/stories/components/palette.rs | 53 ++ .../storybook/src/stories/components/panel.rs | 24 + .../src/stories/components/project_panel.rs | 16 + .../src/stories/components/status_bar.rs | 16 + .../storybook/src/stories/components/tab.rs | 91 +++ .../src/stories/components/tab_bar.rs | 16 + .../src/stories/components/terminal.rs | 16 + .../src/stories/components/title_bar.rs | 16 + .../src/stories/components/toolbar.rs | 12 +- .../src/stories/components/traffic_lights.rs | 16 +- crates/storybook/src/stories/elements.rs | 4 + .../storybook/src/stories/elements/avatar.rs | 17 +- .../storybook/src/stories/elements/button.rs | 192 ++++++ crates/storybook/src/stories/elements/icon.rs | 19 + .../storybook/src/stories/elements/input.rs | 16 + .../storybook/src/stories/elements/label.rs | 18 + crates/storybook/src/stories/kitchen_sink.rs | 46 ++ crates/storybook/src/story.rs | 28 +- crates/storybook/src/story_selector.rs | 122 +++- crates/storybook/src/storybook.rs | 119 ++-- crates/storybook/src/workspace.rs | 56 -- crates/ui/Cargo.toml | 3 + crates/ui/docs/_project.md | 13 + crates/ui/{doc => docs}/elevation.md | 0 crates/ui/src/children.rs | 7 + crates/ui/src/components.rs | 201 +++--- crates/ui/src/components/assistant_panel.rs | 91 +++ crates/ui/src/components/breadcrumb.rs | 19 +- crates/ui/src/components/buffer.rs | 229 +++++++ crates/ui/src/components/chat_panel.rs | 145 +++-- crates/ui/src/components/collab_panel.rs | 130 ++-- crates/ui/src/components/command_palette.rs | 22 +- crates/ui/src/components/context_menu.rs | 65 ++ crates/ui/src/components/editor.rs | 25 + crates/ui/src/components/facepile.rs | 24 +- crates/ui/src/components/follow_group.rs | 52 -- crates/ui/src/components/icon_button.rs | 25 +- crates/ui/src/components/keybinding.rs | 158 +++++ crates/ui/src/components/list.rs | 522 ++++++++++++++-- crates/ui/src/components/list_item.rs | 112 ---- .../ui/src/components/list_section_header.rs | 88 --- crates/ui/src/components/palette.rs | 118 ++-- crates/ui/src/components/palette_item.rs | 63 -- crates/ui/src/components/panel.rs | 146 +++++ crates/ui/src/components/panes.rs | 132 ++++ crates/ui/src/components/player_stack.rs | 66 ++ crates/ui/src/components/project_panel.rs | 115 ++-- crates/ui/src/components/status_bar.rs | 51 +- crates/ui/src/components/tab.rs | 137 +++- crates/ui/src/components/tab_bar.rs | 89 ++- crates/ui/src/components/terminal.rs | 77 +++ crates/ui/src/components/title_bar.rs | 71 +-- crates/ui/src/components/toolbar.rs | 26 +- crates/ui/src/components/traffic_lights.rs | 76 ++- crates/ui/src/components/workspace.rs | 140 +++-- crates/ui/src/elements.rs | 10 +- crates/ui/src/elements/avatar.rs | 17 +- crates/ui/src/elements/button.rs | 203 ++++++ crates/ui/src/elements/details.rs | 13 +- crates/ui/src/elements/icon.rs | 140 +++-- crates/ui/src/elements/indicator.rs | 33 - crates/ui/src/elements/input.rs | 31 +- crates/ui/src/elements/label.rs | 142 ++++- crates/ui/src/elements/player.rs | 132 ++++ crates/ui/src/elements/stack.rs | 31 + crates/ui/src/elements/text_button.rs | 82 --- crates/ui/src/elements/tool_divider.rs | 13 +- crates/ui/src/lib.rs | 5 +- crates/ui/src/prelude.rs | 219 ++++++- crates/ui/src/static_data.rs | 590 +++++++++++++++--- crates/ui/src/tokens.rs | 7 + 85 files changed, 4658 insertions(+), 1623 deletions(-) delete mode 100644 crates/storybook/src/collab_panel.rs create mode 100644 crates/storybook/src/stories/components/assistant_panel.rs create mode 100644 crates/storybook/src/stories/components/buffer.rs create mode 100644 crates/storybook/src/stories/components/chat_panel.rs create mode 100644 crates/storybook/src/stories/components/collab_panel.rs create mode 100644 crates/storybook/src/stories/components/context_menu.rs create mode 100644 crates/storybook/src/stories/components/keybinding.rs create mode 100644 crates/storybook/src/stories/components/palette.rs create mode 100644 crates/storybook/src/stories/components/panel.rs create mode 100644 crates/storybook/src/stories/components/project_panel.rs create mode 100644 crates/storybook/src/stories/components/status_bar.rs create mode 100644 crates/storybook/src/stories/components/tab.rs create mode 100644 crates/storybook/src/stories/components/tab_bar.rs create mode 100644 crates/storybook/src/stories/components/terminal.rs create mode 100644 crates/storybook/src/stories/components/title_bar.rs create mode 100644 crates/storybook/src/stories/elements/button.rs create mode 100644 crates/storybook/src/stories/elements/icon.rs create mode 100644 crates/storybook/src/stories/elements/input.rs create mode 100644 crates/storybook/src/stories/elements/label.rs create mode 100644 crates/storybook/src/stories/kitchen_sink.rs delete mode 100644 crates/storybook/src/workspace.rs create mode 100644 crates/ui/docs/_project.md rename crates/ui/{doc => docs}/elevation.md (100%) create mode 100644 crates/ui/src/children.rs create mode 100644 crates/ui/src/components/assistant_panel.rs create mode 100644 crates/ui/src/components/buffer.rs create mode 100644 crates/ui/src/components/context_menu.rs create mode 100644 crates/ui/src/components/editor.rs delete mode 100644 crates/ui/src/components/follow_group.rs create mode 100644 crates/ui/src/components/keybinding.rs delete mode 100644 crates/ui/src/components/list_item.rs delete mode 100644 crates/ui/src/components/list_section_header.rs delete mode 100644 crates/ui/src/components/palette_item.rs create mode 100644 crates/ui/src/components/panel.rs create mode 100644 crates/ui/src/components/panes.rs create mode 100644 crates/ui/src/components/player_stack.rs create mode 100644 crates/ui/src/components/terminal.rs create mode 100644 crates/ui/src/elements/button.rs delete mode 100644 crates/ui/src/elements/indicator.rs create mode 100644 crates/ui/src/elements/player.rs create mode 100644 crates/ui/src/elements/stack.rs delete mode 100644 crates/ui/src/elements/text_button.rs diff --git a/Cargo.lock b/Cargo.lock index 983f6946ab..2b3c3052c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7398,8 +7398,10 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "clap 4.4.4", "gpui2", + "itertools 0.11.0", "log", "rust-embed", "serde", @@ -8631,9 +8633,12 @@ name = "ui" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "gpui2", "serde", "settings", + "smallvec", + "strum", "theme", ] diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index aadceddc05..5fb7288585 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -198,6 +198,31 @@ pub trait ParentElement { ); 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(mut self, children: I) -> Self + where + I: IntoIterator>, + 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) -> Self + where + Self: Sized, + { + self.children_mut().push(children); + self + } } pub trait IntoElement { diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 5c73235d3e..73c8361613 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -11,7 +11,9 @@ path = "src/storybook.rs" [dependencies] anyhow.workspace = true clap = { version = "4.4", features = ["derive", "string"] } +chrono = "0.4" gpui2 = { path = "../gpui2" } +itertools = "0.11.0" log.workspace = true rust-embed.workspace = true serde.workspace = true diff --git a/crates/storybook/src/collab_panel.rs b/crates/storybook/src/collab_panel.rs deleted file mode 100644 index a6be248d6a..0000000000 --- a/crates/storybook/src/collab_panel.rs +++ /dev/null @@ -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 { - view_type: PhantomData, - 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(scroll_state: ScrollState) -> CollabPanelElement { - CollabPanelElement { - view_type: PhantomData, - scroll_state, - } -} - -impl CollabPanelElement { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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>, - expanded: bool, - theme: &Theme, - ) -> impl Element { - 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>, - label: impl Into>, - theme: &Theme, - ) -> impl Element { - 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), - ) - } -} diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs index 57674c3c70..95b8844157 100644 --- a/crates/storybook/src/stories.rs +++ b/crates/storybook/src/stories.rs @@ -1,2 +1,3 @@ pub mod components; pub mod elements; +pub mod kitchen_sink; diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs index 35bed24c01..345fcfa309 100644 --- a/crates/storybook/src/stories/components.rs +++ b/crates/storybook/src/stories/components.rs @@ -1,4 +1,18 @@ +pub mod assistant_panel; pub mod breadcrumb; +pub mod buffer; +pub mod chat_panel; +pub mod collab_panel; +pub mod context_menu; pub mod facepile; +pub mod keybinding; +pub mod palette; +pub mod panel; +pub mod project_panel; +pub mod status_bar; +pub mod tab; +pub mod tab_bar; +pub mod terminal; +pub mod title_bar; pub mod toolbar; pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/assistant_panel.rs b/crates/storybook/src/stories/components/assistant_panel.rs new file mode 100644 index 0000000000..09f964756e --- /dev/null +++ b/crates/storybook/src/stories/components/assistant_panel.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::AssistantPanel; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct AssistantPanelStory {} + +impl AssistantPanelStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, AssistantPanel>(cx)) + .child(Story::label(cx, "Default")) + .child(AssistantPanel::new()) + } +} diff --git a/crates/storybook/src/stories/components/breadcrumb.rs b/crates/storybook/src/stories/components/breadcrumb.rs index c702eb3def..8d144c0174 100644 --- a/crates/storybook/src/stories/components/breadcrumb.rs +++ b/crates/storybook/src/stories/components/breadcrumb.rs @@ -1,5 +1,5 @@ -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -use ui::breadcrumb; +use ui::prelude::*; +use ui::Breadcrumb; use crate::story::Story; @@ -8,9 +8,9 @@ pub struct BreadcrumbStory {} impl BreadcrumbStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container() - .child(Story::title_for::<_, ui::Breadcrumb>()) - .child(Story::label("Default")) - .child(breadcrumb()) + Story::container(cx) + .child(Story::title_for::<_, Breadcrumb>(cx)) + .child(Story::label(cx, "Default")) + .child(Breadcrumb::new()) } } diff --git a/crates/storybook/src/stories/components/buffer.rs b/crates/storybook/src/stories/components/buffer.rs new file mode 100644 index 0000000000..8d9e70a282 --- /dev/null +++ b/crates/storybook/src/stories/components/buffer.rs @@ -0,0 +1,34 @@ +use gpui2::geometry::rems; +use ui::prelude::*; +use ui::{ + empty_buffer_example, hello_world_rust_buffer_example, + hello_world_rust_buffer_with_status_example, Buffer, +}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct BufferStory {} + +impl BufferStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, Buffer>(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)), + ) + } +} diff --git a/crates/storybook/src/stories/components/chat_panel.rs b/crates/storybook/src/stories/components/chat_panel.rs new file mode 100644 index 0000000000..804290b7ca --- /dev/null +++ b/crates/storybook/src/stories/components/chat_panel.rs @@ -0,0 +1,34 @@ +use chrono::DateTime; +use ui::prelude::*; +use ui::{ChatMessage, ChatPanel}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ChatPanelStory {} + +impl ChatPanelStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, ChatPanel>(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(), + ), + ])) + } +} diff --git a/crates/storybook/src/stories/components/collab_panel.rs b/crates/storybook/src/stories/components/collab_panel.rs new file mode 100644 index 0000000000..6a66f0d47f --- /dev/null +++ b/crates/storybook/src/stories/components/collab_panel.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::CollabPanel; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct CollabPanelStory {} + +impl CollabPanelStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, CollabPanel>(cx)) + .child(Story::label(cx, "Default")) + .child(CollabPanel::new(ScrollState::default())) + } +} diff --git a/crates/storybook/src/stories/components/context_menu.rs b/crates/storybook/src/stories/components/context_menu.rs new file mode 100644 index 0000000000..71776ca66a --- /dev/null +++ b/crates/storybook/src/stories/components/context_menu.rs @@ -0,0 +1,21 @@ +use ui::prelude::*; +use ui::{ContextMenu, ContextMenuItem, Label}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ContextMenuStory {} + +impl ContextMenuStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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")), + ])) + } +} diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs index 37893a9a0a..a32ffa3693 100644 --- a/crates/storybook/src/stories/components/facepile.rs +++ b/crates/storybook/src/stories/components/facepile.rs @@ -1,8 +1,5 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use ui::prelude::*; -use ui::{avatar, facepile, theme}; +use ui::{static_players, Facepile}; use crate::story::Story; @@ -11,40 +8,18 @@ pub struct FacepileStory {} impl FacepileStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); + let players = static_players(); - let avatars = vec![ - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - avatar("https://avatars.githubusercontent.com/u/482957?v=4"), - avatar("https://avatars.githubusercontent.com/u/1789?v=4"), - ]; - - Story::container() - .child(Story::title_for::<_, ui::Facepile>()) - .child(Story::label("Default")) + Story::container(cx) + .child(Story::title_for::<_, ui::Facepile>(cx)) + .child(Story::label(cx, "Default")) .child( div() .flex() .gap_3() - .child(facepile(avatars.clone().into_iter().take(1))) - .child(facepile(avatars.clone().into_iter().take(2))) - .child(facepile(avatars.clone().into_iter().take(3))), + .child(Facepile::new(players.clone().into_iter().take(1))) + .child(Facepile::new(players.clone().into_iter().take(2))) + .child(Facepile::new(players.clone().into_iter().take(3))), ) - .child(Story::label("Rounded rectangle avatars")) - .child({ - let shape = Shape::RoundedRectangle; - - let avatars = avatars - .clone() - .into_iter() - .map(|avatar| avatar.shape(Shape::RoundedRectangle)); - - div() - .flex() - .gap_3() - .child(facepile(avatars.clone().take(1))) - .child(facepile(avatars.clone().take(2))) - .child(facepile(avatars.clone().take(3))) - }) } } diff --git a/crates/storybook/src/stories/components/keybinding.rs b/crates/storybook/src/stories/components/keybinding.rs new file mode 100644 index 0000000000..1acf59fe3b --- /dev/null +++ b/crates/storybook/src/stories/components/keybinding.rs @@ -0,0 +1,64 @@ +use itertools::Itertools; +use strum::IntoEnumIterator; +use ui::prelude::*; +use ui::{Keybinding, ModifierKey, ModifierKeys}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct KeybindingStory {} + +impl KeybindingStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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)), + )) + } +} diff --git a/crates/storybook/src/stories/components/palette.rs b/crates/storybook/src/stories/components/palette.rs new file mode 100644 index 0000000000..d14fac6697 --- /dev/null +++ b/crates/storybook/src/stories/components/palette.rs @@ -0,0 +1,53 @@ +use ui::prelude::*; +use ui::{Keybinding, ModifierKeys, Palette, PaletteItem}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct PaletteStory {} + +impl PaletteStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, Palette>(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())), + ]), + ) + } +} diff --git a/crates/storybook/src/stories/components/panel.rs b/crates/storybook/src/stories/components/panel.rs new file mode 100644 index 0000000000..38e7033d44 --- /dev/null +++ b/crates/storybook/src/stories/components/panel.rs @@ -0,0 +1,24 @@ +use ui::prelude::*; +use ui::{Label, Panel}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct PanelStory {} + +impl PanelStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, Panel>(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(()), + )) + } +} diff --git a/crates/storybook/src/stories/components/project_panel.rs b/crates/storybook/src/stories/components/project_panel.rs new file mode 100644 index 0000000000..ff4eb6099b --- /dev/null +++ b/crates/storybook/src/stories/components/project_panel.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::ProjectPanel; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ProjectPanelStory {} + +impl ProjectPanelStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, ProjectPanel>(cx)) + .child(Story::label(cx, "Default")) + .child(ProjectPanel::new(ScrollState::default())) + } +} diff --git a/crates/storybook/src/stories/components/status_bar.rs b/crates/storybook/src/stories/components/status_bar.rs new file mode 100644 index 0000000000..ed3d047c6d --- /dev/null +++ b/crates/storybook/src/stories/components/status_bar.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::StatusBar; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct StatusBarStory {} + +impl StatusBarStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, StatusBar>(cx)) + .child(Story::label(cx, "Default")) + .child(StatusBar::new()) + } +} diff --git a/crates/storybook/src/stories/components/tab.rs b/crates/storybook/src/stories/components/tab.rs new file mode 100644 index 0000000000..6a154ce644 --- /dev/null +++ b/crates/storybook/src/stories/components/tab.rs @@ -0,0 +1,91 @@ +use strum::IntoEnumIterator; +use ui::prelude::*; +use ui::{h_stack, v_stack, Tab}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct TabStory {} + +impl TabStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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) + }))), + ) + } +} diff --git a/crates/storybook/src/stories/components/tab_bar.rs b/crates/storybook/src/stories/components/tab_bar.rs new file mode 100644 index 0000000000..4c116caf7b --- /dev/null +++ b/crates/storybook/src/stories/components/tab_bar.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::TabBar; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct TabBarStory {} + +impl TabBarStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, TabBar>(cx)) + .child(Story::label(cx, "Default")) + .child(TabBar::new(ScrollState::default())) + } +} diff --git a/crates/storybook/src/stories/components/terminal.rs b/crates/storybook/src/stories/components/terminal.rs new file mode 100644 index 0000000000..2bce2e27e5 --- /dev/null +++ b/crates/storybook/src/stories/components/terminal.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::Terminal; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct TerminalStory {} + +impl TerminalStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, Terminal>(cx)) + .child(Story::label(cx, "Default")) + .child(Terminal::new()) + } +} diff --git a/crates/storybook/src/stories/components/title_bar.rs b/crates/storybook/src/stories/components/title_bar.rs new file mode 100644 index 0000000000..3c4682b3ba --- /dev/null +++ b/crates/storybook/src/stories/components/title_bar.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::TitleBar; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct TitleBarStory {} + +impl TitleBarStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, TitleBar>(cx)) + .child(Story::label(cx, "Default")) + .child(TitleBar::new(cx)) + } +} diff --git a/crates/storybook/src/stories/components/toolbar.rs b/crates/storybook/src/stories/components/toolbar.rs index 7a7688a9d8..cfe3c97840 100644 --- a/crates/storybook/src/stories/components/toolbar.rs +++ b/crates/storybook/src/stories/components/toolbar.rs @@ -1,5 +1,5 @@ -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -use ui::toolbar; +use ui::prelude::*; +use ui::Toolbar; use crate::story::Story; @@ -8,9 +8,9 @@ pub struct ToolbarStory {} impl ToolbarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container() - .child(Story::title_for::<_, ui::Toolbar>()) - .child(Story::label("Default")) - .child(toolbar()) + Story::container(cx) + .child(Story::title_for::<_, Toolbar>(cx)) + .child(Story::label(cx, "Default")) + .child(Toolbar::new()) } } diff --git a/crates/storybook/src/stories/components/traffic_lights.rs b/crates/storybook/src/stories/components/traffic_lights.rs index f3c7b93005..3b759a43a3 100644 --- a/crates/storybook/src/stories/components/traffic_lights.rs +++ b/crates/storybook/src/stories/components/traffic_lights.rs @@ -1,5 +1,5 @@ -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -use ui::{theme, traffic_lights}; +use ui::prelude::*; +use ui::TrafficLights; use crate::story::Story; @@ -8,11 +8,11 @@ pub struct TrafficLightsStory {} impl TrafficLightsStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - Story::container() - .child(Story::title_for::<_, ui::TrafficLights>()) - .child(Story::label("Default")) - .child(traffic_lights()) + Story::container(cx) + .child(Story::title_for::<_, TrafficLights>(cx)) + .child(Story::label(cx, "Default")) + .child(TrafficLights::new()) + .child(Story::label(cx, "Unfocused")) + .child(TrafficLights::new().window_has_focus(false)) } } diff --git a/crates/storybook/src/stories/elements.rs b/crates/storybook/src/stories/elements.rs index 124369cf9d..f7afec4d88 100644 --- a/crates/storybook/src/stories/elements.rs +++ b/crates/storybook/src/stories/elements.rs @@ -1 +1,5 @@ pub mod avatar; +pub mod button; +pub mod icon; +pub mod input; +pub mod label; diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs index a5caf717e7..d47c667f61 100644 --- a/crates/storybook/src/stories/elements/avatar.rs +++ b/crates/storybook/src/stories/elements/avatar.rs @@ -1,6 +1,5 @@ -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use ui::prelude::*; -use ui::{avatar, theme}; +use ui::Avatar; use crate::story::Story; @@ -9,17 +8,15 @@ pub struct AvatarStory {} impl AvatarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - Story::container() - .child(Story::title_for::<_, ui::Avatar>()) - .child(Story::label("Default")) - .child(avatar( + Story::container(cx) + .child(Story::title_for::<_, ui::Avatar>(cx)) + .child(Story::label(cx, "Default")) + .child(Avatar::new( "https://avatars.githubusercontent.com/u/1714999?v=4", )) - .child(Story::label("Rounded rectangle")) + .child(Story::label(cx, "Rounded rectangle")) .child( - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") .shape(Shape::RoundedRectangle), ) } diff --git a/crates/storybook/src/stories/elements/button.rs b/crates/storybook/src/stories/elements/button.rs new file mode 100644 index 0000000000..7ff3fdd30c --- /dev/null +++ b/crates/storybook/src/stories/elements/button.rs @@ -0,0 +1,192 @@ +use gpui2::elements::div; +use gpui2::geometry::rems; +use gpui2::{Element, IntoElement, ViewContext}; +use strum::IntoEnumIterator; +use ui::prelude::*; +use ui::{h_stack, v_stack, Button, Icon, IconPosition, Label}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ButtonStory {} + +impl ButtonStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let states = InteractionState::iter(); + + Story::container(cx) + .child(Story::title_for::<_, Button>(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.")), + ) + } +} diff --git a/crates/storybook/src/stories/elements/icon.rs b/crates/storybook/src/stories/elements/icon.rs new file mode 100644 index 0000000000..21838bd839 --- /dev/null +++ b/crates/storybook/src/stories/elements/icon.rs @@ -0,0 +1,19 @@ +use strum::IntoEnumIterator; +use ui::prelude::*; +use ui::{Icon, IconElement}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct IconStory {} + +impl IconStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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))) + } +} diff --git a/crates/storybook/src/stories/elements/input.rs b/crates/storybook/src/stories/elements/input.rs new file mode 100644 index 0000000000..8617b7daaf --- /dev/null +++ b/crates/storybook/src/stories/elements/input.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::Input; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct InputStory {} + +impl InputStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, Input>(cx)) + .child(Story::label(cx, "Default")) + .child(div().flex().child(Input::new("Search"))) + } +} diff --git a/crates/storybook/src/stories/elements/label.rs b/crates/storybook/src/stories/elements/label.rs new file mode 100644 index 0000000000..1b63cb3a3a --- /dev/null +++ b/crates/storybook/src/stories/elements/label.rs @@ -0,0 +1,18 @@ +use ui::prelude::*; +use ui::Label; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct LabelStory {} + +impl LabelStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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])) + } +} diff --git a/crates/storybook/src/stories/kitchen_sink.rs b/crates/storybook/src/stories/kitchen_sink.rs new file mode 100644 index 0000000000..9be0c5e720 --- /dev/null +++ b/crates/storybook/src/stories/kitchen_sink.rs @@ -0,0 +1,46 @@ +use ui::prelude::*; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct KitchenSinkStory {} + +impl KitchenSinkStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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()), + ) + } +} diff --git a/crates/storybook/src/story.rs b/crates/storybook/src/story.rs index c758518762..16eae50f88 100644 --- a/crates/storybook/src/story.rs +++ b/crates/storybook/src/story.rs @@ -1,11 +1,13 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{rgb, Element, Hsla, ParentElement}; +use gpui2::elements::div::Div; +use ui::prelude::*; +use ui::theme; pub struct Story {} impl Story { - pub fn container() -> div::Div { + pub fn container(cx: &mut ViewContext) -> Div { + let theme = theme(cx); + div() .size_full() .flex() @@ -13,26 +15,30 @@ impl Story { .pt_2() .px_4() .font("Zed Mono Extended") - .fill(rgb::(0x282c34)) + .fill(theme.lowest.base.default.background) } - pub fn title(title: &str) -> impl Element { + pub fn title(cx: &mut ViewContext, title: &str) -> impl Element { + let theme = theme(cx); + div() .text_xl() - .text_color(rgb::(0xffffff)) + .text_color(theme.lowest.base.default.foreground) .child(title.to_owned()) } - pub fn title_for() -> impl Element { - Self::title(std::any::type_name::()) + pub fn title_for(cx: &mut ViewContext) -> impl Element { + Self::title(cx, std::any::type_name::()) } - pub fn label(label: &str) -> impl Element { + pub fn label(cx: &mut ViewContext, label: &str) -> impl Element { + let theme = theme(cx); + div() .mt_4() .mb_2() .text_xs() - .text_color(rgb::(0xffffff)) + .text_color(theme.lowest.base.default.foreground) .child(label.to_owned()) } } diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index d12368706a..fda7290da6 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -1,29 +1,97 @@ -use std::{str::FromStr, sync::OnceLock}; +use std::str::FromStr; +use std::sync::OnceLock; use anyhow::{anyhow, Context}; use clap::builder::PossibleValue; use clap::ValueEnum; +use gpui2::{AnyElement, Element}; use strum::{EnumIter, EnumString, IntoEnumIterator}; -#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ElementStory { Avatar, + Button, + Icon, + Input, + Label, } -#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)] +impl ElementStory { + pub fn story(&self) -> AnyElement { + use crate::stories::elements; + + match self { + Self::Avatar => elements::avatar::AvatarStory::default().into_any(), + Self::Button => elements::button::ButtonStory::default().into_any(), + Self::Icon => elements::icon::IconStory::default().into_any(), + Self::Input => elements::input::InputStory::default().into_any(), + Self::Label => elements::label::LabelStory::default().into_any(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ComponentStory { + AssistantPanel, Breadcrumb, + Buffer, + ContextMenu, + ChatPanel, + CollabPanel, Facepile, + Keybinding, + Palette, + Panel, + ProjectPanel, + StatusBar, + Tab, + TabBar, + Terminal, + TitleBar, Toolbar, TrafficLights, } -#[derive(Debug, Clone, Copy)] +impl ComponentStory { + pub fn story(&self) -> AnyElement { + use crate::stories::components; + + match self { + Self::AssistantPanel => { + components::assistant_panel::AssistantPanelStory::default().into_any() + } + Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(), + Self::Buffer => components::buffer::BufferStory::default().into_any(), + Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(), + Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(), + Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(), + Self::Facepile => components::facepile::FacepileStory::default().into_any(), + Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(), + Self::Palette => components::palette::PaletteStory::default().into_any(), + Self::Panel => components::panel::PanelStory::default().into_any(), + Self::ProjectPanel => { + components::project_panel::ProjectPanelStory::default().into_any() + } + Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(), + Self::Tab => components::tab::TabStory::default().into_any(), + Self::TabBar => components::tab_bar::TabBarStory::default().into_any(), + Self::Terminal => components::terminal::TerminalStory::default().into_any(), + Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(), + Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(), + Self::TrafficLights => { + components::traffic_lights::TrafficLightsStory::default().into_any() + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum StorySelector { Element(ElementStory), Component(ComponentStory), + KitchenSink, } impl FromStr for StorySelector { @@ -32,6 +100,10 @@ impl FromStr for StorySelector { fn from_str(raw_story_name: &str) -> std::result::Result { let story = raw_story_name.to_ascii_lowercase(); + if story == "kitchen_sink" { + return Ok(Self::KitchenSink); + } + if let Some((_, story)) = story.split_once("elements/") { let element_story = ElementStory::from_str(story) .with_context(|| format!("story not found for element '{story}'"))?; @@ -50,25 +122,49 @@ impl FromStr for StorySelector { } } +impl StorySelector { + pub fn story(&self) -> Vec> { + match self { + Self::Element(element_story) => vec![element_story.story()], + Self::Component(component_story) => vec![component_story.story()], + Self::KitchenSink => all_story_selectors() + .into_iter() + // Exclude the kitchen sink to prevent `story` from recursively + // calling itself for all eternity. + .filter(|selector| **selector != Self::KitchenSink) + .flat_map(|selector| selector.story()) + .collect(), + } + } +} + /// The list of all stories available in the storybook. -static ALL_STORIES: OnceLock> = OnceLock::new(); +static ALL_STORY_SELECTORS: OnceLock> = 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::>() + }); + + stories +} impl ValueEnum for StorySelector { fn value_variants<'a>() -> &'a [Self] { - let stories = ALL_STORIES.get_or_init(|| { - let element_stories = ElementStory::iter().map(Self::Element); - let component_stories = ComponentStory::iter().map(Self::Component); - - element_stories.chain(component_stories).collect::>() - }); - - stories + all_story_selectors() } fn to_possible_value(&self) -> Option { let value = match self { Self::Element(story) => format!("elements/{story}"), Self::Component(story) => format!("components/{story}"), + Self::KitchenSink => "kitchen_sink".to_string(), }; Some(PossibleValue::new(value)) diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index d52219f61e..9aea829772 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -1,26 +1,24 @@ #![allow(dead_code, unused_variables)] -mod collab_panel; mod stories; mod story; mod story_selector; -mod workspace; + +use std::sync::Arc; use ::theme as legacy_theme; use clap::Parser; -use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds}; -use legacy_theme::ThemeSettings; +use gpui2::{ + serde_json, vec2f, view, Element, IntoElement, ParentElement, RectF, ViewContext, WindowBounds, +}; +use legacy_theme::{ThemeRegistry, ThemeSettings}; use log::LevelFilter; use settings::{default_settings, SettingsStore}; use simplelog::SimpleLogger; -use stories::components::breadcrumb::BreadcrumbStory; -use stories::components::facepile::FacepileStory; -use stories::components::toolbar::ToolbarStory; -use stories::components::traffic_lights::TrafficLightsStory; -use stories::elements::avatar::AvatarStory; -use ui::{ElementExt, Theme}; +use ui::prelude::*; +use ui::{ElementExt, Theme, WorkspaceElement}; -use crate::story_selector::{ComponentStory, ElementStory, StorySelector}; +use crate::story_selector::StorySelector; gpui2::actions! { storybook, @@ -32,6 +30,12 @@ gpui2::actions! { struct Args { #[arg(value_enum)] story: Option, + + /// The name of the theme to use in the storybook. + /// + /// If not provided, the default theme will be used. + #[arg(long)] + theme: Option, } fn main() { @@ -48,31 +52,60 @@ fn main() { legacy_theme::init(Assets, cx); // load_embedded_fonts(cx.platform().as_ref()); + let theme_registry = cx.global::>(); + + let theme_override = args + .theme + .and_then(|theme| { + theme_registry + .list_names(true) + .find(|known_theme| theme == *known_theme) + }) + .and_then(|theme_name| theme_registry.get(&theme_name).ok()); + cx.add_window( gpui2::WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1600., 900.))), + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))), center: true, ..Default::default() }, |cx| match args.story { - Some(StorySelector::Element(ElementStory::Avatar)) => { - view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default())) - } - Some(StorySelector::Component(ComponentStory::Breadcrumb)) => { - view(|cx| render_story(&mut ViewContext::new(cx), BreadcrumbStory::default())) - } - Some(StorySelector::Component(ComponentStory::Facepile)) => { - view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default())) - } - Some(StorySelector::Component(ComponentStory::Toolbar)) => { - view(|cx| render_story(&mut ViewContext::new(cx), ToolbarStory::default())) - } - Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| { - render_story(&mut ViewContext::new(cx), TrafficLightsStory::default()) + // HACK: Special-case the kitchen sink to fix scrolling. + // There is something about going through `children_any` that messes + // with the scroll interactions. + Some(StorySelector::KitchenSink) => view(move |cx| { + render_story( + &mut ViewContext::new(cx), + theme_override.clone(), + crate::stories::kitchen_sink::KitchenSinkStory::default(), + ) }), - None => { - view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default())) + // HACK: Special-case the panel story to fix scrolling. + // There is something about going through `children_any` that messes + // with the scroll interactions. + Some(StorySelector::Component(story_selector::ComponentStory::Panel)) => { + view(move |cx| { + render_story( + &mut ViewContext::new(cx), + theme_override.clone(), + crate::stories::components::panel::PanelStory::default(), + ) + }) } + Some(selector) => view(move |cx| { + render_story( + &mut ViewContext::new(cx), + theme_override.clone(), + div().children_any(selector.story()), + ) + }), + None => view(move |cx| { + render_story( + &mut ViewContext::new(cx), + theme_override.clone(), + WorkspaceElement::default(), + ) + }), }, ); cx.platform().activate(true); @@ -81,23 +114,32 @@ fn main() { fn render_story>( cx: &mut ViewContext, + theme_override: Option>, story: S, ) -> impl Element { - story.into_element().themed(current_theme(cx)) + let theme = current_theme(cx, theme_override); + + story.into_element().themed(theme) +} + +fn current_theme( + cx: &mut ViewContext, + theme_override: Option>, +) -> Theme { + let legacy_theme = + theme_override.unwrap_or_else(|| settings::get::(cx).theme.clone()); + + let new_theme: Theme = serde_json::from_value(legacy_theme.base_theme.clone()).unwrap(); + + add_base_theme_to_legacy_theme(&legacy_theme, new_theme) } // Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct. -fn current_theme(cx: &mut ViewContext) -> Theme { - settings::get::(cx) - .theme +fn add_base_theme_to_legacy_theme(legacy_theme: &legacy_theme::Theme, new_theme: Theme) -> Theme { + legacy_theme .deserialized_base_theme .lock() - .get_or_insert_with(|| { - let theme: Theme = - serde_json::from_value(settings::get::(cx).theme.base_theme.clone()) - .unwrap(); - Box::new(theme) - }) + .get_or_insert_with(|| Box::new(new_theme)) .downcast_ref::() .unwrap() .clone() @@ -106,7 +148,6 @@ fn current_theme(cx: &mut ViewContext) -> Theme { use anyhow::{anyhow, Result}; use gpui2::AssetSource; use rust_embed::RustEmbed; -use workspace::WorkspaceElement; #[derive(RustEmbed)] #[folder = "../../assets"] diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs deleted file mode 100644 index 3ddaa5caa6..0000000000 --- a/crates/storybook/src/workspace.rs +++ /dev/null @@ -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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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()) - } -} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 5018a91739..821e93a340 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -6,7 +6,10 @@ publish = false [dependencies] anyhow.workspace = true +chrono = "0.4" gpui2 = { path = "../gpui2" } serde.workspace = true settings = { path = "../settings" } +smallvec.workspace = true +strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } diff --git a/crates/ui/docs/_project.md b/crates/ui/docs/_project.md new file mode 100644 index 0000000000..a3b72a9d61 --- /dev/null +++ b/crates/ui/docs/_project.md @@ -0,0 +1,13 @@ +## Project Plan + +- Port existing UI to GPUI2 +- Update UI in places that GPUI1 was limiting us* +- Understand the needs &/|| struggles the engineers have been having with building UI in the past and address as many of those as possible as we go +- Ship a simple, straightforward system with documentation that is easy to use to build UI + +## Component Classification + +To simplify the understanding of components and minimize unnecessary cognitive load, let's categorize components into two types: + +- An element refers to a standalone component that doesn't import any other 'ui' components. +- A component indicates a component that utilizes or imports other 'ui' components. diff --git a/crates/ui/doc/elevation.md b/crates/ui/docs/elevation.md similarity index 100% rename from crates/ui/doc/elevation.md rename to crates/ui/docs/elevation.md diff --git a/crates/ui/src/children.rs b/crates/ui/src/children.rs new file mode 100644 index 0000000000..947f8d98cf --- /dev/null +++ b/crates/ui/src/children.rs @@ -0,0 +1,7 @@ +use std::any::Any; + +use gpui2::{AnyElement, ViewContext}; + +pub type HackyChildren = fn(&mut ViewContext, &dyn Any) -> Vec>; + +pub type HackyChildrenPayload = Box; diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 50a680e0f7..f96964bd27 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,142 +1,153 @@ +mod assistant_panel; mod breadcrumb; +mod buffer; mod chat_panel; mod collab_panel; mod command_palette; +mod context_menu; +mod editor; mod facepile; -mod follow_group; mod icon_button; +mod keybinding; mod list; -mod list_item; -mod list_section_header; mod palette; -mod palette_item; +mod panel; +mod panes; +mod player_stack; mod project_panel; mod status_bar; mod tab; mod tab_bar; +mod terminal; mod title_bar; mod toolbar; mod traffic_lights; mod workspace; +pub use assistant_panel::*; pub use breadcrumb::*; +pub use buffer::*; pub use chat_panel::*; pub use collab_panel::*; pub use command_palette::*; +pub use context_menu::*; +pub use editor::*; pub use facepile::*; -pub use follow_group::*; pub use icon_button::*; +pub use keybinding::*; pub use list::*; -pub use list_item::*; -pub use list_section_header::*; pub use palette::*; -pub use palette_item::*; +pub use panel::*; +pub use panes::*; +pub use player_stack::*; pub use project_panel::*; pub use status_bar::*; pub use tab::*; pub use tab_bar::*; +pub use terminal::*; pub use title_bar::*; pub use toolbar::*; pub use traffic_lights::*; pub use workspace::*; -use std::marker::PhantomData; -use std::rc::Rc; +// Nate: Commenting this out for now, unsure if we need it. -use gpui2::elements::div; -use gpui2::interactive::Interactive; -use gpui2::platform::MouseButton; -use gpui2::style::StyleHelpers; -use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext}; +// use std::marker::PhantomData; +// use std::rc::Rc; -struct ButtonHandlers { - click: Option)>>, -} +// use gpui2::elements::div; +// use gpui2::interactive::Interactive; +// use gpui2::platform::MouseButton; +// use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext}; -impl Default for ButtonHandlers { - fn default() -> Self { - Self { click: None } - } -} +// struct ButtonHandlers { +// click: Option)>>, +// } -#[derive(Element)] -pub struct Button { - handlers: ButtonHandlers, - label: Option>, - icon: Option>, - data: Rc, - view_type: PhantomData, -} +// impl Default for ButtonHandlers { +// fn default() -> Self { +// Self { click: None } +// } +// } -// Impl block for buttons without data. -// See below for an impl block for any button. -impl Button { - fn new() -> Self { - Self { - handlers: ButtonHandlers::default(), - label: None, - icon: None, - data: Rc::new(()), - view_type: PhantomData, - } - } +// #[derive(Element)] +// pub struct Button { +// handlers: ButtonHandlers, +// label: Option>, +// icon: Option>, +// data: Rc, +// view_type: PhantomData, +// } - pub fn data(self, data: D) -> Button { - Button { - handlers: ButtonHandlers::default(), - label: self.label, - icon: self.icon, - data: Rc::new(data), - view_type: PhantomData, - } - } -} +// // Impl block for buttons without data. +// // See below for an impl block for any button. +// impl Button { +// fn new() -> Self { +// Self { +// handlers: ButtonHandlers::default(), +// label: None, +// icon: None, +// data: Rc::new(()), +// view_type: PhantomData, +// } +// } -// Impl block for button regardless of its data type. -impl Button { - pub fn label(mut self, label: impl Into>) -> Self { - self.label = Some(label.into()); - self - } +// pub fn data(self, data: D) -> Button { +// Button { +// handlers: ButtonHandlers::default(), +// label: self.label, +// icon: self.icon, +// data: Rc::new(data), +// view_type: PhantomData, +// } +// } +// } - pub fn icon(mut self, icon: impl Into>) -> Self { - self.icon = Some(icon.into()); - self - } +// // Impl block for button regardless of its data type. +// impl Button { +// pub fn label(mut self, label: impl Into>) -> Self { +// self.label = Some(label.into()); +// self +// } - pub fn on_click( - mut self, - handler: impl Fn(&mut V, &D, &mut EventContext) + 'static, - ) -> Self { - self.handlers.click = Some(Rc::new(handler)); - self - } -} +// pub fn icon(mut self, icon: impl Into>) -> Self { +// self.icon = Some(icon.into()); +// self +// } -pub fn button() -> Button { - Button::new() -} +// pub fn on_click( +// mut self, +// handler: impl Fn(&mut V, &D, &mut EventContext) + 'static, +// ) -> Self { +// self.handlers.click = Some(Rc::new(handler)); +// self +// } +// } -impl Button { - fn render( - &mut self, - view: &mut V, - cx: &mut ViewContext, - ) -> impl IntoElement + Interactive { - // let colors = &cx.theme::().colors; +// pub fn button() -> Button { +// Button::new() +// } - let button = div() - // .fill(colors.error(0.5)) - .h_4() - .children(self.label.clone()); +// impl Button { +// fn render( +// &mut self, +// view: &mut V, +// cx: &mut ViewContext, +// ) -> impl IntoElement + Interactive { +// // let colors = &cx.theme::().colors; - if let Some(handler) = self.handlers.click.clone() { - let data = self.data.clone(); - button.on_mouse_down(MouseButton::Left, move |view, event, cx| { - handler(view, data.as_ref(), cx) - }) - } else { - button - } - } -} +// let button = div() +// // .fill(colors.error(0.5)) +// .h_4() +// .children(self.label.clone()); + +// if let Some(handler) = self.handlers.click.clone() { +// let data = self.data.clone(); +// button.on_mouse_down(MouseButton::Left, move |view, event, cx| { +// handler(view, data.as_ref(), cx) +// }) +// } else { +// button +// } +// } +// } diff --git a/crates/ui/src/components/assistant_panel.rs b/crates/ui/src/components/assistant_panel.rs new file mode 100644 index 0000000000..a0a0c52882 --- /dev/null +++ b/crates/ui/src/components/assistant_panel.rs @@ -0,0 +1,91 @@ +use std::marker::PhantomData; + +use gpui2::geometry::rems; + +use crate::prelude::*; +use crate::theme::theme; +use crate::{Icon, IconButton, Label, Panel, PanelSide}; + +#[derive(Element)] +pub struct AssistantPanel { + view_type: PhantomData, + scroll_state: ScrollState, + current_side: PanelSide, +} + +impl AssistantPanel { + 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) -> impl IntoElement { + let theme = theme(cx); + + struct PanelPayload { + pub scroll_state: ScrollState, + } + + Panel::new( + self.scroll_state.clone(), + |_, payload| { + let payload = payload.downcast_ref::().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.)) + } +} diff --git a/crates/ui/src/components/breadcrumb.rs b/crates/ui/src/components/breadcrumb.rs index 1883e35ae9..30b40011a5 100644 --- a/crates/ui/src/components/breadcrumb.rs +++ b/crates/ui/src/components/breadcrumb.rs @@ -1,24 +1,19 @@ -use gpui2::elements::div; -use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::theme; +use crate::prelude::*; +use crate::{h_stack, theme}; #[derive(Element)] pub struct Breadcrumb {} -pub fn breadcrumb() -> Breadcrumb { - Breadcrumb {} -} - impl Breadcrumb { + pub fn new() -> Self { + Self {} + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - div() + h_stack() .px_1() - .flex() - .flex_row() // TODO: Read font from theme (or settings?). .font("Zed Mono Extended") .text_sm() diff --git a/crates/ui/src/components/buffer.rs b/crates/ui/src/components/buffer.rs new file mode 100644 index 0000000000..88c5a59563 --- /dev/null +++ b/crates/ui/src/components/buffer.rs @@ -0,0 +1,229 @@ +use std::marker::PhantomData; + +use gpui2::{Hsla, WindowContext}; + +use crate::prelude::*; +use crate::{h_stack, theme, v_stack, Icon, IconElement}; + +#[derive(Default, PartialEq, Copy, Clone)] +pub struct PlayerCursor { + color: Hsla, + index: usize, +} + +#[derive(Default, PartialEq, Clone)] +pub struct HighlightedText { + pub text: String, + pub color: Hsla, +} + +#[derive(Default, PartialEq, Clone)] +pub struct HighlightedLine { + pub highlighted_texts: Vec, +} + +#[derive(Default, PartialEq, Clone)] +pub struct BufferRow { + pub line_number: usize, + pub code_action: bool, + pub current: bool, + pub line: Option, + pub cursors: Option>, + pub status: GitStatus, + pub show_line_number: bool, +} + +pub struct BufferRows { + pub show_line_numbers: bool, + pub rows: Vec, +} + +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) -> Self { + self.line = line; + self + } + + pub fn set_cursors(mut self, cursors: Option>) -> 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 { + view_type: PhantomData, + scroll_state: ScrollState, + rows: Option, + readonly: bool, + language: Option, + title: Option, + path: Option, +} + +impl Buffer { + 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>>(mut self, title: T) -> Self { + self.title = title.into(); + self + } + + pub fn set_path>>(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>>(mut self, rows: R) -> Self { + self.rows = rows.into(); + self + } + + pub fn set_language>>(mut self, language: L) -> Self { + self.language = language.into(); + self + } + + fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement { + 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> { + 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) -> impl IntoElement { + 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) + } +} diff --git a/crates/ui/src/components/chat_panel.rs b/crates/ui/src/components/chat_panel.rs index 2e3f73d30f..e5a2d6a556 100644 --- a/crates/ui/src/components/chat_panel.rs +++ b/crates/ui/src/components/chat_panel.rs @@ -1,66 +1,127 @@ use std::marker::PhantomData; -use gpui2::elements::div::ScrollState; -use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use chrono::NaiveDateTime; +use crate::prelude::*; use crate::theme::theme; -use crate::{icon_button, IconAsset}; +use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide}; #[derive(Element)] pub struct ChatPanel { view_type: PhantomData, scroll_state: ScrollState, -} - -pub fn chat_panel(scroll_state: ScrollState) -> ChatPanel { - ChatPanel { - view_type: PhantomData, - scroll_state, - } + current_side: PanelSide, + messages: Vec, } impl ChatPanel { + 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) -> Self { + self.messages = messages; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - div() - .h_full() - .flex() - // Header - .child( - div() - .px_2() - .flex() - .gap_2() - // Nav Buttons - .child("#gpui2"), - ) - // Chat Body - .child( - div() - .w_full() + struct PanelPayload { + pub scroll_state: ScrollState, + pub messages: Vec, + } + + Panel::new( + self.scroll_state.clone(), + |_, payload| { + let payload = payload.downcast_ref::().unwrap(); + + vec![div() .flex() .flex_col() - .overflow_y_scroll(self.scroll_state.clone()) - .child("body"), - ) - // Composer - .child( - div() + .h_full() .px_2() - .flex() .gap_2() - // Nav Buttons + // Header .child( div() .flex() - .items_center() - .gap_px() - .child(icon_button().icon(IconAsset::Plus)) - .child(icon_button().icon(IconAsset::Split)), - ), - ) + .justify_between() + .gap_2() + .child(div().flex().child(Label::new("#design"))) + .child( + div() + .flex() + .items_center() + .gap_px() + .child(IconButton::new(Icon::File)) + .child(IconButton::new(Icon::AudioOn)), + ), + ) + // Chat Body + .child( + div() + .w_full() + .flex() + .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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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()))) } } diff --git a/crates/ui/src/components/collab_panel.rs b/crates/ui/src/components/collab_panel.rs index 3d6efe54f4..14dd43294f 100644 --- a/crates/ui/src/components/collab_panel.rs +++ b/crates/ui/src/components/collab_panel.rs @@ -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 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)] -pub struct CollabPanelElement { +pub struct CollabPanel { view_type: PhantomData, 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(scroll_state: ScrollState) -> CollabPanelElement { - CollabPanelElement { - view_type: PhantomData, - scroll_state, +impl CollabPanel { + pub fn new(scroll_state: ScrollState) -> Self { + Self { + view_type: PhantomData, + scroll_state, + } } -} -impl CollabPanelElement { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - // Panel - div() + v_stack() .w_64() .h_full() - .flex() - .flex_col() - .font("Zed Sans Extended") - .text_color(theme.middle.base.default.foreground) - .border_color(theme.middle.base.default.border) - .border() .fill(theme.middle.base.default.background) .child( - div() + v_stack() .w_full() - .flex() - .flex_col() .overflow_y_scroll(self.scroll_state.clone()) - // List Container .child( div() .fill(theme.lowest.base.default.background) .pb_1() .border_color(theme.lowest.base.default.border) .border_b() - //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state - // .group() - // List Section Header - .child(self.list_section_header("#CRDB", true, &theme)) - // List Item Large - .child(self.list_item( - "http://github.com/maxbrunsfeld.png?s=50", - "maxbrunsfeld", - &theme, - )), - ) - .child( - 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( + List::new(static_collab_panel_current_call()) + .header( + ListHeader::new("CRDB") + .left_icon(Icon::Hash.into()) + .set_toggle(ToggleState::Toggled), + ) + .set_toggle(ToggleState::Toggled), ), + ) + .child( + 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( diff --git a/crates/ui/src/components/command_palette.rs b/crates/ui/src/components/command_palette.rs index 303e2d6de9..797876c05e 100644 --- a/crates/ui/src/components/command_palette.rs +++ b/crates/ui/src/components/command_palette.rs @@ -1,9 +1,7 @@ -use gpui2::elements::div; -use gpui2::{elements::div::ScrollState, ViewContext}; -use gpui2::{Element, IntoElement, ParentElement}; use std::marker::PhantomData; -use crate::{example_editor_actions, palette, OrderMethod}; +use crate::prelude::*; +use crate::{example_editor_actions, OrderMethod, Palette}; #[derive(Element)] pub struct CommandPalette { @@ -11,17 +9,17 @@ pub struct CommandPalette { scroll_state: ScrollState, } -pub fn command_palette(scroll_state: ScrollState) -> CommandPalette { - CommandPalette { - view_type: PhantomData, - scroll_state, - } -} - impl CommandPalette { + pub fn new(scroll_state: ScrollState) -> Self { + Self { + view_type: PhantomData, + scroll_state, + } + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { div().child( - palette(self.scroll_state.clone()) + Palette::new(self.scroll_state.clone()) .items(example_editor_actions()) .placeholder("Execute a command...") .empty_string("No items found.") diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs new file mode 100644 index 0000000000..0808bf6556 --- /dev/null +++ b/crates/ui/src/components/context_menu.rs @@ -0,0 +1,65 @@ +use crate::prelude::*; +use crate::theme::theme; +use crate::{ + v_stack, Label, List, ListEntry, ListItem, ListItemVariant, ListSeparator, ListSubHeader, +}; + +#[derive(Clone)] +pub enum ContextMenuItem { + Header(&'static str), + Entry(Label), + Separator, +} + +impl ContextMenuItem { + fn to_list_item(self) -> ListItem { + match self { + ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), + ContextMenuItem::Entry(label) => { + ListEntry::new(label).variant(ListItemVariant::Inset).into() + } + ContextMenuItem::Separator => ListSeparator::new().into(), + } + } + pub fn header(label: &'static str) -> Self { + Self::Header(label) + } + pub fn separator() -> Self { + Self::Separator + } + pub fn entry(label: Label) -> Self { + Self::Entry(label) + } +} + +#[derive(Element)] +pub struct ContextMenu { + items: Vec, +} + +impl ContextMenu { + pub fn new(items: impl IntoIterator) -> Self { + Self { + items: items.into_iter().collect(), + } + } + fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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()) + } +} diff --git a/crates/ui/src/components/editor.rs b/crates/ui/src/components/editor.rs new file mode 100644 index 0000000000..105ed86c40 --- /dev/null +++ b/crates/ui/src/components/editor.rs @@ -0,0 +1,25 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{Buffer, Toolbar}; + +#[derive(Element)] +struct Editor { + view_type: PhantomData, + toolbar: Toolbar, + buffer: Buffer, +} + +impl Editor { + pub fn new(toolbar: Toolbar, buffer: Buffer) -> Self { + Self { + view_type: PhantomData, + toolbar, + buffer, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child(self.toolbar.clone()) + } +} diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index 39167498a9..949e226c25 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -1,29 +1,27 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::{theme, Avatar}; +use crate::prelude::*; +use crate::{theme, Avatar, Player}; #[derive(Element)] pub struct Facepile { - players: Vec, -} - -pub fn facepile>(players: P) -> Facepile { - Facepile { - players: players.collect(), - } + players: Vec, } impl Facepile { + pub fn new>(players: P) -> Self { + Self { + players: players.collect(), + } + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let player_count = self.players.len(); let player_list = self.players.iter().enumerate().map(|(ix, player)| { let isnt_last = ix < player_count - 1; + div() .when(isnt_last, |div| div.neg_mr_1()) - .child(player.clone()) + .child(Avatar::new(player.avatar_src().to_string())) }); div().p_1().flex().items_center().children(player_list) } diff --git a/crates/ui/src/components/follow_group.rs b/crates/ui/src/components/follow_group.rs deleted file mode 100644 index a522859868..0000000000 --- a/crates/ui/src/components/follow_group.rs +++ /dev/null @@ -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, -} - -pub fn follow_group(players: Vec) -> FollowGroup { - FollowGroup { player: 0, players } -} - -impl FollowGroup { - pub fn player(mut self, player: usize) -> Self { - self.player = player; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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())), - ) - } -} diff --git a/crates/ui/src/components/icon_button.rs b/crates/ui/src/components/icon_button.rs index 893d41871d..8ac122d3eb 100644 --- a/crates/ui/src/components/icon_button.rs +++ b/crates/ui/src/components/icon_button.rs @@ -1,29 +1,16 @@ -use gpui2::elements::div; -use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::{icon, theme, IconColor}; -use crate::{prelude::*, IconAsset}; +use crate::prelude::*; +use crate::{theme, Icon, IconColor, IconElement}; #[derive(Element)] pub struct IconButton { - icon: IconAsset, + icon: Icon, color: IconColor, variant: ButtonVariant, state: InteractionState, } -pub fn icon_button() -> IconButton { - IconButton { - icon: IconAsset::default(), - color: IconColor::default(), - variant: ButtonVariant::default(), - state: InteractionState::default(), - } -} - impl IconButton { - pub fn new(icon: IconAsset) -> Self { + pub fn new(icon: Icon) -> Self { Self { icon, color: IconColor::default(), @@ -32,7 +19,7 @@ impl IconButton { } } - pub fn icon(mut self, icon: IconAsset) -> Self { + pub fn icon(mut self, icon: Icon) -> Self { self.icon = icon; self } @@ -75,6 +62,6 @@ impl IconButton { .fill(theme.highest.base.hovered.background) .active() .fill(theme.highest.base.pressed.background) - .child(icon(self.icon).color(icon_color)) + .child(IconElement::new(self.icon).color(icon_color)) } } diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs new file mode 100644 index 0000000000..c467933e94 --- /dev/null +++ b/crates/ui/src/components/keybinding.rs @@ -0,0 +1,158 @@ +use std::collections::HashSet; + +use strum::{EnumIter, IntoEnumIterator}; + +use crate::prelude::*; +use crate::theme; + +#[derive(Element, Clone)] +pub struct Keybinding { + /// A keybinding consists of a key and a set of modifier keys. + /// More then one keybinding produces a chord. + /// + /// This should always contain at least one element. + keybinding: Vec<(String, ModifierKeys)>, +} + +impl Keybinding { + pub fn new(key: String, modifiers: ModifierKeys) -> Self { + Self { + keybinding: vec![(key, modifiers)], + } + } + + pub fn new_chord( + first_note: (String, ModifierKeys), + second_note: (String, ModifierKeys), + ) -> Self { + Self { + keybinding: vec![first_note, second_note], + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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(key: K) -> Self + where + K: Into, + { + Self { key: key.into() } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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); + +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 + } +} diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs index a7fb06132f..8389c8c2c7 100644 --- a/crates/ui/src/components/list.rs +++ b/crates/ui/src/components/list.rs @@ -1,36 +1,294 @@ -use crate::theme::theme; -use crate::tokens::token; -use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader}; -use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::elements::div::Div; +use gpui2::{Hsla, WindowContext}; -#[derive(Element)] -pub struct List { - header: Option, - items: Vec, - empty_message: &'static str, - toggle: Option, - // footer: Option, +use crate::prelude::*; +use crate::{ + h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor, + IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor, + ToggleState, +}; + +#[derive(Clone, Copy, Default, Debug, PartialEq)] +pub enum ListItemVariant { + /// The list item extends to the far left and right of the list. + #[default] + FullWidth, + Inset, } -pub fn list(items: Vec) -> List { - List { - header: None, - items, - empty_message: "No items", - toggle: None, +#[derive(Element, Clone, Copy)] +pub struct ListHeader { + label: &'static str, + left_icon: Option, + variant: ListItemVariant, + state: InteractionState, + toggleable: Toggleable, +} + +impl ListHeader { + pub fn new(label: &'static str) -> Self { + Self { + label, + left_icon: None, + variant: ListItemVariant::default(), + state: InteractionState::default(), + toggleable: Toggleable::default(), + } } -} -impl List { - pub fn header(mut self, header: ListSectionHeader) -> Self { - self.header = Some(header); + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggleable = toggle.into(); self } - pub fn empty_message(mut self, empty_message: &'static str) -> Self { - self.empty_message = empty_message; + pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self { + self.toggleable = toggleable; + self + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + + fn disclosure_control(&self) -> Div { + 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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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, + 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) -> Self { + self.left_icon = left_icon; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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 for ListItem { + fn from(entry: ListEntry) -> Self { + Self::Entry(entry) + } +} + +impl From for ListItem { + fn from(entry: ListSeparator) -> Self { + Self::Separator(entry) + } +} + +impl From for ListItem { + fn from(entry: ListSubHeader) -> Self { + Self::Header(entry) + } +} + +impl ListItem { + fn render(&mut self, v: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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, + variant: ListItemVariant, + size: ListEntrySize, + state: InteractionState, + toggle: Option, +} + +impl ListEntry { + pub fn new(label: Label) -> Self { + Self { + disclosure_control_style: DisclosureControlVisibility::default(), + indent_level: 0, + label, + variant: ListItemVariant::default(), + left_content: None, + size: ListEntrySize::default(), + state: InteractionState::default(), + toggle: None, + } + } + pub fn variant(mut self, variant: ListItemVariant) -> Self { + self.variant = variant; + self + } + pub fn indent_level(mut self, indent_level: u32) -> Self { + self.indent_level = indent_level; self } @@ -39,26 +297,216 @@ impl List { self } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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( + &mut self, + cx: &mut ViewContext, + ) -> Option> { let theme = theme(cx); let token = token(); - let disclosure_control = match self.toggle { - Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)), - Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)), + let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle { + IconElement::new(Icon::ChevronDown) + } else { + IconElement::new(Icon::ChevronRight) + } + .color(IconColor::Muted) + .size(IconSize::Small); + + match (self.toggle, self.disclosure_control_style) { + (Some(_), DisclosureControlVisibility::OnHover) => { + Some(div().absolute().neg_left_5().child(disclosure_control_icon)) + } + (Some(_), DisclosureControlVisibility::Always) => { + Some(div().child(disclosure_control_icon)) + } + (None, _) => None, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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, }; + let sized_item = match self.size { + ListEntrySize::Small => div().h_6(), + ListEntrySize::Medium => div().h_7(), + }; + div() + .fill(background_color) + .when(self.state == InteractionState::Focused, |this| { + this.border() + .border_color(theme.lowest.accent.default.border) + }) + .relative() .py_1() - .flex() - .flex_col() - .children(self.header.map(|h| h)) - .children( - self.items - .is_empty() - .then(|| label(self.empty_message).color(LabelColor::Muted)), + .child( + sized_item + .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + // .ml(rems(0.75 * self.indent_level as f32)) + .children((0..self.indent_level).map(|_| { + div() + .w(token.list_indent_depth) + .h_full() + .flex() + .justify_center() + .child(h_stack().child(div().w_px().h_full()).child( + div().w_px().h_full().fill(theme.middle.base.default.border), + )) + })) + .flex() + .gap_1() + .items_center() + .relative() + .children(self.disclosure_control(cx)) + .children(left_content) + .child(self.label.clone()), ) - .children(self.items.iter().cloned()) + } +} + +#[derive(Clone, Default, Element)] +pub struct ListSeparator; + +impl ListSeparator { + pub fn new() -> Self { + Self::default() + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div().h_px().w_full().fill(theme.lowest.base.default.border) + } +} + +#[derive(Element)] +pub struct List { + items: Vec, + empty_message: &'static str, + header: Option, + toggleable: Toggleable, +} + +impl List { + pub fn new(items: Vec) -> 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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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) } } diff --git a/crates/ui/src/components/list_item.rs b/crates/ui/src/components/list_item.rs deleted file mode 100644 index 19e5b26abe..0000000000 --- a/crates/ui/src/components/list_item.rs +++ /dev/null @@ -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, - indent_level: u32, - state: InteractionState, - disclosure_control_style: DisclosureControlVisibility, - toggle: Option, -} - -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) -> 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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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()), - ) - } -} diff --git a/crates/ui/src/components/list_section_header.rs b/crates/ui/src/components/list_section_header.rs deleted file mode 100644 index 76a0d4cee7..0000000000 --- a/crates/ui/src/components/list_section_header.rs +++ /dev/null @@ -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, - state: InteractionState, - toggle: Option, -} - -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) -> Self { - self.left_icon = left_icon; - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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), - ) - } -} diff --git a/crates/ui/src/components/palette.rs b/crates/ui/src/components/palette.rs index a540d29169..430ab8be63 100644 --- a/crates/ui/src/components/palette.rs +++ b/crates/ui/src/components/palette.rs @@ -1,12 +1,8 @@ use std::marker::PhantomData; -use crate::prelude::OrderMethod; +use crate::prelude::*; use crate::theme::theme; -use crate::{label, palette_item, LabelColor, PaletteItem}; -use gpui2::elements::div::ScrollState; -use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use crate::{h_stack, v_stack, Keybinding, Label, LabelColor}; #[derive(Element)] pub struct Palette { @@ -18,20 +14,19 @@ pub struct Palette { default_order: OrderMethod, } -pub fn palette(scroll_state: ScrollState) -> Palette { - Palette { - view_type: PhantomData, - scroll_state, - input_placeholder: "Find something...", - empty_string: "No items found.", - items: vec![], - default_order: OrderMethod::default(), - } -} - impl Palette { - pub fn items(mut self, mut items: Vec) -> Self { - items.sort_by_key(|item| item.label); + pub fn new(scroll_state: ScrollState) -> Self { + Self { + view_type: PhantomData, + scroll_state, + input_placeholder: "Find something...", + empty_string: "No items found.", + items: vec![], + default_order: OrderMethod::default(), + } + } + + pub fn items(mut self, items: Vec) -> Self { self.items = items; self } @@ -55,49 +50,33 @@ impl Palette { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - div() + v_stack() .w_96() .rounded_lg() .fill(theme.lowest.base.default.background) .border() .border_color(theme.lowest.base.default.border) - .flex() - .flex_col() .child( - div() - .flex() - .flex_col() + v_stack() .gap_px() - .child( - div().py_0p5().px_1().flex().flex_col().child( - div().px_2().py_0p5().child( - label(self.input_placeholder).color(LabelColor::Placeholder), - ), + .child(v_stack().py_0p5().px_1().child( + div().px_2().py_0p5().child( + Label::new(self.input_placeholder).color(LabelColor::Placeholder), ), - ) + )) .child(div().h_px().w_full().fill(theme.lowest.base.default.border)) .child( - div() + v_stack() .py_0p5() .px_1() - .flex() - .flex_col() .grow() .max_h_96() .overflow_y_scroll(self.scroll_state.clone()) .children( vec![if self.items.is_empty() { - Some( - div() - .flex() - .flex_row() - .justify_between() - .px_2() - .py_1() - .child( - label(self.empty_string).color(LabelColor::Muted), - ), - ) + Some(h_stack().justify_between().px_2().py_1().child( + Label::new(self.empty_string).color(LabelColor::Muted), + )) } else { None }] @@ -105,9 +84,7 @@ impl Palette { .flatten(), ) .children(self.items.iter().map(|item| { - div() - .flex() - .flex_row() + h_stack() .justify_between() .px_2() .py_0p5() @@ -116,9 +93,52 @@ impl Palette { .fill(theme.lowest.base.hovered.background) .active() .fill(theme.lowest.base.pressed.background) - .child(palette_item(item.label, item.keybinding)) + .child( + PaletteItem::new(item.label) + .keybinding(item.keybinding.clone()), + ) })), ), ) } } + +#[derive(Element)] +pub struct PaletteItem { + pub label: &'static str, + pub keybinding: Option, +} + +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(mut self, keybinding: K) -> Self + where + K: Into>, + { + self.keybinding = keybinding.into(); + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .flex() + .flex_row() + .grow() + .justify_between() + .child(Label::new(self.label)) + .children(self.keybinding.clone()) + } +} diff --git a/crates/ui/src/components/palette_item.rs b/crates/ui/src/components/palette_item.rs deleted file mode 100644 index 9e7883d700..0000000000 --- a/crates/ui/src/components/palette_item.rs +++ /dev/null @@ -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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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()), - ) - } -} diff --git a/crates/ui/src/components/panel.rs b/crates/ui/src/components/panel.rs new file mode 100644 index 0000000000..9d64945cc1 --- /dev/null +++ b/crates/ui/src/components/panel.rs @@ -0,0 +1,146 @@ +use std::marker::PhantomData; + +use gpui2::geometry::AbsoluteLength; + +use crate::prelude::*; +use crate::{theme, token, v_stack}; + +#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum PanelAllowedSides { + LeftOnly, + RightOnly, + BottomOnly, + #[default] + LeftAndRight, + All, +} + +impl PanelAllowedSides { + /// Return a `HashSet` that contains the allowable `PanelSide`s. + pub fn allowed_sides(&self) -> HashSet { + 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 { + view_type: PhantomData, + scroll_state: ScrollState, + current_side: PanelSide, + /// Defaults to PanelAllowedSides::LeftAndRight + allowed_sides: PanelAllowedSides, + initial_width: AbsoluteLength, + width: Option, + children: HackyChildren, + payload: HackyChildrenPayload, +} + +impl Panel { + pub fn new( + scroll_state: ScrollState, + children: HackyChildren, + 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) -> impl IntoElement { + 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())) + } +} diff --git a/crates/ui/src/components/panes.rs b/crates/ui/src/components/panes.rs new file mode 100644 index 0000000000..2518eea8ee --- /dev/null +++ b/crates/ui/src/components/panes.rs @@ -0,0 +1,132 @@ +use std::marker::PhantomData; + +use gpui2::geometry::{Length, Size}; +use gpui2::{hsla, Hsla}; + +use crate::prelude::*; +use crate::theme; + +#[derive(Default, PartialEq)] +pub enum SplitDirection { + #[default] + Horizontal, + Vertical, +} + +#[derive(Element)] +pub struct Pane { + view_type: PhantomData, + scroll_state: ScrollState, + size: Size, + fill: Hsla, + children: HackyChildren, + payload: HackyChildrenPayload, +} + +impl Pane { + pub fn new( + scroll_state: ScrollState, + size: Size, + children: HackyChildren, + 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) -> impl IntoElement { + 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 { + view_type: PhantomData, + groups: Vec>, + panes: Vec>, + split_direction: SplitDirection, +} + +impl PaneGroup { + pub fn new_groups(groups: Vec>, split_direction: SplitDirection) -> Self { + Self { + view_type: PhantomData, + groups, + panes: Vec::new(), + split_direction, + } + } + + pub fn new_panes(panes: Vec>, split_direction: SplitDirection) -> Self { + Self { + view_type: PhantomData, + groups: Vec::new(), + panes, + split_direction, + } + } + + fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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!() + } +} diff --git a/crates/ui/src/components/player_stack.rs b/crates/ui/src/components/player_stack.rs new file mode 100644 index 0000000000..4c00aaf2cf --- /dev/null +++ b/crates/ui/src/components/player_stack.rs @@ -0,0 +1,66 @@ +use crate::prelude::*; +use crate::{Avatar, Facepile, PlayerWithCallStatus}; + +#[derive(Element)] +pub struct PlayerStack { + player_with_call_status: PlayerWithCallStatus, +} + +impl PlayerStack { + pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self { + Self { + player_with_call_status, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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())) + })), + ) + } +} diff --git a/crates/ui/src/components/project_panel.rs b/crates/ui/src/components/project_panel.rs index 8204ad26c0..cf6f080b1c 100644 --- a/crates/ui/src/components/project_panel.rs +++ b/crates/ui/src/components/project_panel.rs @@ -1,62 +1,87 @@ -use crate::{ - input, list, list_section_header, prelude::*, static_project_panel_project_items, - static_project_panel_single_items, theme, -}; - -use gpui2::{ - elements::{div, div::ScrollState}, - style::StyleHelpers, - ParentElement, ViewContext, -}; -use gpui2::{Element, IntoElement}; 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)] pub struct ProjectPanel { view_type: PhantomData, scroll_state: ScrollState, -} - -pub fn project_panel(scroll_state: ScrollState) -> ProjectPanel { - ProjectPanel { - view_type: PhantomData, - scroll_state, - } + current_side: PanelSide, } impl ProjectPanel { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); + pub fn new(scroll_state: ScrollState) -> Self { + Self { + view_type: PhantomData, + scroll_state, + current_side: PanelSide::default(), + } + } - div() - .w_56() - .h_full() - .flex() - .flex_col() - .fill(theme.middle.base.default.background) - .child( - div() - .w_56() + pub fn side(mut self, side: PanelSide) -> Self { + self.current_side = side; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + struct PanelPayload { + pub theme: Arc, + pub scroll_state: ScrollState, + } + + Panel::new( + self.scroll_state.clone(), + |_, payload| { + let payload = payload.downcast_ref::().unwrap(); + + let theme = payload.theme.clone(); + + vec![div() .flex() .flex_col() - .overflow_y_scroll(self.scroll_state.clone()) + .w_56() + .h_full() + .px_2() + .fill(theme.middle.base.default.background) .child( - list(static_project_panel_single_items()) - .header(list_section_header("FILES").set_toggle(ToggleState::Toggled)) - .empty_message("No files in directory") - .set_toggle(ToggleState::Toggled), + div() + .w_56() + .flex() + .flex_col() + .overflow_y_scroll(payload.scroll_state.clone()) + .child( + List::new(static_project_panel_single_items()) + .header( + ListHeader::new("FILES").set_toggle(ToggleState::Toggled), + ) + .empty_message("No files in directory") + .set_toggle(ToggleState::Toggled), + ) + .child( + List::new(static_project_panel_project_items()) + .header( + ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled), + ) + .empty_message("No folders in directory") + .set_toggle(ToggleState::Toggled), + ), ) .child( - list(static_project_panel_project_items()) - .header(list_section_header("PROJECT").set_toggle(ToggleState::Toggled)) - .empty_message("No folders in directory") - .set_toggle(ToggleState::Toggled), - ), - ) - .child( - input("Find something...") - .value("buffe".to_string()) - .state(InteractionState::Focused), - ) + Input::new("Find something...") + .value("buffe".to_string()) + .state(InteractionState::Focused), + ) + .into_any()] + }, + Box::new(PanelPayload { + theme: theme(cx), + scroll_state: self.scroll_state.clone(), + }), + ) } } diff --git a/crates/ui/src/components/status_bar.rs b/crates/ui/src/components/status_bar.rs index 21f792c358..1970b0bff9 100644 --- a/crates/ui/src/components/status_bar.rs +++ b/crates/ui/src/components/status_bar.rs @@ -1,11 +1,8 @@ use std::marker::PhantomData; -use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; - +use crate::prelude::*; use crate::theme::{theme, Theme}; -use crate::{icon_button, text_button, tool_divider, IconAsset}; +use crate::{Button, Icon, IconButton, IconColor, ToolDivider}; #[derive(Default, PartialEq)] pub enum Tool { @@ -40,16 +37,16 @@ pub struct StatusBar { bottom_tools: Option, } -pub fn status_bar() -> StatusBar { - StatusBar { - view_type: PhantomData, - left_tools: None, - right_tools: None, - bottom_tools: None, - } -} - impl StatusBar { + 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) -> Self { self.left_tools = { let mut tools = vec![tool]; @@ -106,10 +103,10 @@ impl StatusBar { .flex() .items_center() .gap_1() - .child(icon_button().icon(IconAsset::FileTree)) - .child(icon_button().icon(IconAsset::Hash)) - .child(tool_divider()) - .child(icon_button().icon(IconAsset::XCircle)) + .child(IconButton::new(Icon::FileTree).color(IconColor::Accent)) + .child(IconButton::new(Icon::Hash)) + .child(ToolDivider::new()) + .child(IconButton::new(Icon::XCircle)) } fn right_tools(&self, theme: &Theme) -> impl Element { div() @@ -121,27 +118,27 @@ impl StatusBar { .flex() .items_center() .gap_1() - .child(text_button("116:25")) - .child(text_button("Rust")), + .child(Button::new("116:25")) + .child(Button::new("Rust")), ) - .child(tool_divider()) + .child(ToolDivider::new()) .child( div() .flex() .items_center() .gap_1() - .child(icon_button().icon(IconAsset::Copilot)) - .child(icon_button().icon(IconAsset::Envelope)), + .child(IconButton::new(Icon::Copilot)) + .child(IconButton::new(Icon::Envelope)), ) - .child(tool_divider()) + .child(ToolDivider::new()) .child( div() .flex() .items_center() .gap_1() - .child(icon_button().icon(IconAsset::Terminal)) - .child(icon_button().icon(IconAsset::MessageBubbles)) - .child(icon_button().icon(IconAsset::Ai)), + .child(IconButton::new(Icon::Terminal)) + .child(IconButton::new(Icon::MessageBubbles)) + .child(IconButton::new(Icon::Ai)), ) } } diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index e812a26cd5..9c034d2535 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -1,22 +1,96 @@ -use gpui2::elements::div; -use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::theme; +use crate::prelude::*; +use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor}; #[derive(Element)] pub struct Tab { - title: &'static str, - enabled: bool, -} - -pub fn tab(title: &'static str, enabled: bool) -> impl Element { - Tab { title, enabled } + title: String, + icon: Option, + current: bool, + dirty: bool, + fs_status: FileSystemStatus, + git_status: GitStatus, + diagnostic_status: DiagnosticStatus, + close_side: IconSide, } impl Tab { + pub fn new() -> Self { + Self { + title: "untitled".to_string(), + icon: None, + current: false, + dirty: false, + fs_status: FileSystemStatus::None, + git_status: GitStatus::None, + diagnostic_status: DiagnosticStatus::None, + close_side: IconSide::Right, + } + } + + pub fn current(mut self, current: bool) -> Self { + self.current = current; + self + } + + pub fn title(mut self, title: String) -> Self { + self.title = title; + self + } + + pub fn icon(mut self, icon: I) -> Self + where + I: Into>, + { + 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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict; + let is_deleted = self.fs_status == FileSystemStatus::Deleted; + + let label = match (self.git_status, is_deleted) { + (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone()) + .color(LabelColor::Hidden) + .set_strikethrough(true), + (GitStatus::None, false) => Label::new(self.title.clone()), + (GitStatus::Created, false) => { + Label::new(self.title.clone()).color(LabelColor::Created) + } + (GitStatus::Modified, false) => { + Label::new(self.title.clone()).color(LabelColor::Modified) + } + (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent), + (GitStatus::Conflict, false) => Label::new(self.title.clone()), + }; + + let close_icon = IconElement::new(Icon::Close).color(IconColor::Muted); div() .px_2() @@ -24,33 +98,34 @@ impl Tab { .flex() .items_center() .justify_center() - .rounded_lg() - .fill(if self.enabled { - theme.highest.on.default.background - } else { + .fill(if self.current { theme.highest.base.default.background - }) - .hover() - .fill(if self.enabled { - theme.highest.on.hovered.background } else { - theme.highest.base.hovered.background - }) - .active() - .fill(if self.enabled { - theme.highest.on.pressed.background - } else { - theme.highest.base.pressed.background + theme.middle.base.default.background }) .child( div() - .text_sm() - .text_color(if self.enabled { - theme.highest.base.default.foreground + .px_1() + .flex() + .items_center() + .gap_1() + .children(has_fs_conflict.then(|| { + IconElement::new(Icon::ExclamationTriangle) + .size(crate::IconSize::Small) + .color(IconColor::Warning) + })) + .children(self.icon.map(IconElement::new)) + .children(if self.close_side == IconSide::Left { + Some(close_icon.clone()) } else { - theme.highest.variant.default.foreground + None }) - .child(self.title), + .child(label) + .children(if self.close_side == IconSide::Right { + Some(close_icon) + } else { + None + }), ) } } diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index c8892b4697..43fef77e2c 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -1,13 +1,7 @@ use std::marker::PhantomData; -use gpui2::elements::div::ScrollState; -use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; - -use crate::prelude::InteractionState; -use crate::theme::theme; -use crate::{icon_button, tab, IconAsset}; +use crate::prelude::*; +use crate::{theme, Icon, IconButton, Tab}; #[derive(Element)] pub struct TabBar { @@ -15,14 +9,14 @@ pub struct TabBar { scroll_state: ScrollState, } -pub fn tab_bar(scroll_state: ScrollState) -> TabBar { - TabBar { - view_type: PhantomData, - scroll_state, - } -} - impl TabBar { + pub fn new(scroll_state: ScrollState) -> Self { + Self { + view_type: PhantomData, + scroll_state, + } + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let can_navigate_back = true; @@ -30,6 +24,7 @@ impl TabBar { div() .w_full() .flex() + .fill(theme.middle.base.default.background) // Left Side .child( div() @@ -44,12 +39,11 @@ impl TabBar { .items_center() .gap_px() .child( - icon_button() - .icon(IconAsset::ArrowLeft) + IconButton::new(Icon::ArrowLeft) .state(InteractionState::Enabled.if_enabled(can_navigate_back)), ) .child( - icon_button().icon(IconAsset::ArrowRight).state( + IconButton::new(Icon::ArrowRight).state( InteractionState::Enabled.if_enabled(can_navigate_forward), ), ), @@ -59,17 +53,52 @@ impl TabBar { div().w_0().flex_1().h_full().child( div() .flex() - .gap_1() .overflow_x_scroll(self.scroll_state.clone()) - .child(tab("Cargo.toml", false)) - .child(tab("Channels Panel", true)) - .child(tab("channels_panel.rs", false)) - .child(tab("workspace.rs", false)) - .child(tab("icon_button.rs", false)) - .child(tab("storybook.rs", false)) - .child(tab("theme.rs", false)) - .child(tab("theme_registry.rs", false)) - .child(tab("styleable_helpers.rs", false)), + .child( + Tab::new() + .title("Cargo.toml".to_string()) + .current(false) + .git_status(GitStatus::Modified), + ) + .child( + Tab::new() + .title("Channels Panel".to_string()) + .current(false), + ) + .child( + Tab::new() + .title("channels_panel.rs".to_string()) + .current(true) + .git_status(GitStatus::Modified), + ) + .child( + Tab::new() + .title("workspace.rs".to_string()) + .current(false) + .git_status(GitStatus::Modified), + ) + .child( + Tab::new() + .title("icon_button.rs".to_string()) + .current(false), + ) + .child( + Tab::new() + .title("storybook.rs".to_string()) + .current(false) + .git_status(GitStatus::Created), + ) + .child(Tab::new().title("theme.rs".to_string()).current(false)) + .child( + Tab::new() + .title("theme_registry.rs".to_string()) + .current(false), + ) + .child( + Tab::new() + .title("styleable_helpers.rs".to_string()) + .current(false), + ), ), ) // Right Side @@ -85,8 +114,8 @@ impl TabBar { .flex() .items_center() .gap_px() - .child(icon_button().icon(IconAsset::Plus)) - .child(icon_button().icon(IconAsset::Split)), + .child(IconButton::new(Icon::Plus)) + .child(IconButton::new(Icon::Split)), ), ) } diff --git a/crates/ui/src/components/terminal.rs b/crates/ui/src/components/terminal.rs new file mode 100644 index 0000000000..f5c3a42a64 --- /dev/null +++ b/crates/ui/src/components/terminal.rs @@ -0,0 +1,77 @@ +use gpui2::geometry::{relative, rems, Size}; + +use crate::prelude::*; +use crate::{theme, Icon, IconButton, Pane, Tab}; + +#[derive(Element)] +pub struct Terminal {} + +impl Terminal { + pub fn new() -> Self { + Self {} + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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(()), + )) + } +} diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs index f926adbd26..196b896396 100644 --- a/crates/ui/src/components/title_bar.rs +++ b/crates/ui/src/components/title_bar.rs @@ -1,33 +1,41 @@ use std::marker::PhantomData; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::prelude::Shape; +use crate::prelude::*; use crate::{ - avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights, IconAsset, - IconColor, + static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor, + PlayerStack, ToolDivider, TrafficLights, }; #[derive(Element)] pub struct TitleBar { view_type: PhantomData, -} - -pub fn title_bar() -> TitleBar { - TitleBar { - view_type: PhantomData, - } + is_active: Arc, } impl TitleBar { + pub fn new(cx: &mut ViewContext) -> 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) -> impl IntoElement { let theme = theme(cx); - let player_list = vec![ - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - ]; + let has_focus = cx.window_is_active(); + + let player_list = static_players_with_call_status().into_iter(); div() .flex() @@ -43,20 +51,17 @@ impl TitleBar { .h_full() .gap_4() .px_2() - .child(traffic_lights()) + .child(TrafficLights::new().window_has_focus(has_focus)) // === Project Info === // .child( div() .flex() .items_center() .gap_1() - .child(text_button("maxbrunsfeld")) - .child(text_button("zed")) - .child(text_button("nate/gpui2-ui-components")), + .child(Button::new("zed")) + .child(Button::new("nate/gpui2-ui-components")), ) - .child(follow_group(player_list.clone()).player(0)) - .child(follow_group(player_list.clone()).player(1)) - .child(follow_group(player_list.clone()).player(2)), + .children(player_list.map(|p| PlayerStack::new(p))), ) .child( div() @@ -68,27 +73,23 @@ impl TitleBar { .flex() .items_center() .gap_1() - .child(icon_button().icon(IconAsset::FolderX)) - .child(icon_button().icon(IconAsset::Close)), + .child(IconButton::new(Icon::FolderX)) + .child(IconButton::new(Icon::Close)), ) - .child(tool_divider()) + .child(ToolDivider::new()) .child( div() .px_2() .flex() .items_center() .gap_1() - .child(icon_button().icon(IconAsset::Mic)) - .child(icon_button().icon(IconAsset::AudioOn)) - .child( - icon_button() - .icon(IconAsset::Screen) - .color(IconColor::Accent), - ), + .child(IconButton::new(Icon::Mic)) + .child(IconButton::new(Icon::AudioOn)) + .child(IconButton::new(Icon::Screen).color(IconColor::Accent)), ) .child( div().px_2().flex().items_center().child( - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") .shape(Shape::RoundedRectangle), ), ), diff --git a/crates/ui/src/components/toolbar.rs b/crates/ui/src/components/toolbar.rs index 08e671311f..aedd634743 100644 --- a/crates/ui/src/components/toolbar.rs +++ b/crates/ui/src/components/toolbar.rs @@ -1,21 +1,19 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::{breadcrumb, theme, IconAsset, IconButton}; +use crate::prelude::*; +use crate::{theme, Breadcrumb, Icon, IconButton}; +#[derive(Clone)] pub struct ToolbarItem {} -#[derive(Element)] +#[derive(Element, Clone)] pub struct Toolbar { items: Vec, } -pub fn toolbar() -> Toolbar { - Toolbar { items: Vec::new() } -} - impl Toolbar { + pub fn new() -> Self { + Self { items: Vec::new() } + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); @@ -23,13 +21,13 @@ impl Toolbar { .p_2() .flex() .justify_between() - .child(breadcrumb()) + .child(Breadcrumb::new()) .child( div() .flex() - .child(IconButton::new(IconAsset::InlayHint)) - .child(IconButton::new(IconAsset::MagnifyingGlass)) - .child(IconButton::new(IconAsset::MagicWand)), + .child(IconButton::new(Icon::InlayHint)) + .child(IconButton::new(Icon::MagnifyingGlass)) + .child(IconButton::new(Icon::MagicWand)), ) } } diff --git a/crates/ui/src/components/traffic_lights.rs b/crates/ui/src/components/traffic_lights.rs index 128af9976f..0d644c49ca 100644 --- a/crates/ui/src/components/traffic_lights.rs +++ b/crates/ui/src/components/traffic_lights.rs @@ -1,30 +1,78 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, Hsla, IntoElement, ParentElement, ViewContext}; +use crate::prelude::*; +use crate::{theme, token, SystemColor}; -use crate::theme; +#[derive(Clone, Copy)] +enum TrafficLightColor { + Red, + Yellow, + Green, +} #[derive(Element)] -pub struct TrafficLights {} +struct TrafficLight { + color: TrafficLightColor, + window_has_focus: bool, +} -pub fn traffic_lights() -> TrafficLights { - TrafficLights {} +impl TrafficLight { + fn new(color: TrafficLightColor, window_has_focus: bool) -> Self { + Self { + color, + window_has_focus, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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 { + 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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let token = token(); div() .flex() .items_center() .gap_2() - .child(traffic_light(theme.lowest.negative.default.foreground)) - .child(traffic_light(theme.lowest.warning.default.foreground)) - .child(traffic_light(theme.lowest.positive.default.foreground)) + .child(TrafficLight::new( + TrafficLightColor::Red, + self.window_has_focus, + )) + .child(TrafficLight::new( + TrafficLightColor::Yellow, + self.window_has_focus, + )) + .child(TrafficLight::new( + TrafficLightColor::Green, + self.window_has_focus, + )) } } - -fn traffic_light>(fill: C) -> div::Div { - div().w_3().h_3().rounded_full().fill(fill.into()) -} diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs index fb785a317f..0c6331dc9b 100644 --- a/crates/ui/src/components/workspace.rs +++ b/crates/ui/src/components/workspace.rs @@ -1,30 +1,68 @@ -use crate::{chat_panel, collab_panel, project_panel, status_bar, tab_bar, theme, title_bar}; +use chrono::DateTime; +use gpui2::geometry::{relative, rems, Size}; -use gpui2::{ - elements::{div, div::ScrollState}, - style::StyleHelpers, - Element, IntoElement, ParentElement, ViewContext, +use crate::prelude::*; +use crate::{ + theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, + ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, }; #[derive(Element, Default)] -struct WorkspaceElement { - project_panel_scroll_state: ScrollState, - collab_panel_scroll_state: ScrollState, - right_scroll_state: ScrollState, +pub struct WorkspaceElement { + left_panel_scroll_state: ScrollState, + right_panel_scroll_state: ScrollState, tab_bar_scroll_state: ScrollState, - palette_scroll_state: ScrollState, -} - -pub fn workspace() -> impl Element { - WorkspaceElement::default() + bottom_panel_scroll_state: ScrollState, } impl WorkspaceElement { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); + let temp_size = rems(36.).into(); + + let root_group = PaneGroup::new_groups( + vec![ + PaneGroup::new_panes( + vec![ + Pane::new( + ScrollState::default(), + Size { + width: relative(1.).into(), + height: temp_size, + }, + |_, _| vec![Terminal::new().into_any()], + Box::new(()), + ), + Pane::new( + ScrollState::default(), + Size { + width: relative(1.).into(), + height: temp_size, + }, + |_, _| vec![Terminal::new().into_any()], + Box::new(()), + ), + ], + SplitDirection::Vertical, + ), + PaneGroup::new_panes( + vec![Pane::new( + ScrollState::default(), + Size { + width: relative(1.).into(), + height: relative(1.).into(), + }, + |_, _| vec![Terminal::new().into_any()], + Box::new(()), + )], + SplitDirection::Vertical, + ), + ], + SplitDirection::Horizontal, + ); + + let theme = theme(cx).clone(); div() - // Elevation Level 0 .size_full() .flex() .flex_col() @@ -34,9 +72,7 @@ impl WorkspaceElement { .items_start() .text_color(theme.lowest.base.default.foreground) .fill(theme.lowest.base.default.background) - .relative() - // Elevation Level 1 - .child(title_bar()) + .child(TitleBar::new(cx)) .child( div() .flex_1() @@ -44,37 +80,57 @@ impl WorkspaceElement { .flex() .flex_row() .overflow_hidden() - .child(project_panel(self.project_panel_scroll_state.clone())) - .child(collab_panel(self.collab_panel_scroll_state.clone())) + .border_t() + .border_b() + .border_color(theme.lowest.base.default.border) .child( - div() - .h_full() + ProjectPanel::new(self.left_panel_scroll_state.clone()) + .side(PanelSide::Left), + ) + .child( + v_stack() .flex_1() - .fill(theme.highest.base.default.background) + .h_full() .child( div() .flex() - .flex_col() .flex_1() - .child(tab_bar(self.tab_bar_scroll_state.clone())), + // CSS Hack: Flex 1 has to have a set height to properly fill the space + // Or it will give you a height of 0 + .h_px() + .child(root_group), + ) + .child( + Panel::new( + self.bottom_panel_scroll_state.clone(), + |_, _| vec![Terminal::new().into_any()], + Box::new(()), + ) + .allowed_sides(PanelAllowedSides::BottomOnly) + .side(PanelSide::Bottom), ), ) - .child(chat_panel(self.right_scroll_state.clone())), + .child(ChatPanel::new(ScrollState::default()).with_messages(vec![ + ChatMessage::new( + "osiewicz".to_string(), + "is this thing on?".to_string(), + DateTime::parse_from_rfc3339( + "2023-09-27T15:40:52.707Z", + ) + .unwrap() + .naive_local(), + ), + ChatMessage::new( + "maxdeviant".to_string(), + "Reading you loud and clear!".to_string(), + DateTime::parse_from_rfc3339( + "2023-09-28T15:40:52.707Z", + ) + .unwrap() + .naive_local(), + ), + ])), ) - .child(status_bar()) - // Elevation Level 3 - // .child( - // div() - // .absolute() - // .top_0() - // .left_0() - // .size_full() - // .flex() - // .justify_center() - // .items_center() - // // .fill(theme.lowest.base.default.background) - // // Elevation Level 4 - // .child(command_palette(self.palette_scroll_state.clone())), - // ) + .child(StatusBar::new()) } } diff --git a/crates/ui/src/elements.rs b/crates/ui/src/elements.rs index 0ed40e147e..c60902ae98 100644 --- a/crates/ui/src/elements.rs +++ b/crates/ui/src/elements.rs @@ -1,17 +1,19 @@ mod avatar; +mod button; mod details; mod icon; -mod indicator; mod input; mod label; -mod text_button; +mod player; +mod stack; mod tool_divider; pub use avatar::*; +pub use button::*; pub use details::*; pub use icon::*; -pub use indicator::*; pub use input::*; pub use label::*; -pub use text_button::*; +pub use player::*; +pub use stack::*; pub use tool_divider::*; diff --git a/crates/ui/src/elements/avatar.rs b/crates/ui/src/elements/avatar.rs index 068ec7a28a..2072b0e501 100644 --- a/crates/ui/src/elements/avatar.rs +++ b/crates/ui/src/elements/avatar.rs @@ -1,6 +1,5 @@ use gpui2::elements::img; -use gpui2::style::StyleHelpers; -use gpui2::{ArcCow, Element, IntoElement, ViewContext}; +use gpui2::ArcCow; use crate::prelude::*; use crate::theme; @@ -11,14 +10,14 @@ pub struct Avatar { shape: Shape, } -pub fn avatar(src: impl Into>) -> Avatar { - Avatar { - src: src.into(), - shape: Shape::Circle, - } -} - impl Avatar { + pub fn new(src: impl Into>) -> Self { + Self { + src: src.into(), + shape: Shape::Circle, + } + } + pub fn shape(mut self, shape: Shape) -> Self { self.shape = shape; self diff --git a/crates/ui/src/elements/button.rs b/crates/ui/src/elements/button.rs new file mode 100644 index 0000000000..c516b27908 --- /dev/null +++ b/crates/ui/src/elements/button.rs @@ -0,0 +1,203 @@ +use std::rc::Rc; + +use gpui2::geometry::DefiniteLength; +use gpui2::platform::MouseButton; +use gpui2::{EventContext, Hsla, Interactive, WindowContext}; + +use crate::prelude::*; +use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize}; + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum IconPosition { + #[default] + Left, + Right, +} + +#[derive(Default, Copy, Clone, PartialEq)] +pub enum ButtonVariant { + #[default] + Ghost, + Filled, +} + +struct ButtonHandlers { + click: Option)>>, +} + +impl Default for ButtonHandlers { + fn default() -> Self { + Self { click: None } + } +} + +#[derive(Element)] +pub struct Button { + label: String, + variant: ButtonVariant, + state: InteractionState, + icon: Option, + icon_position: Option, + width: Option, + handlers: ButtonHandlers, +} + +impl Button { + pub fn new(label: L) -> Self + where + L: Into, + { + Self { + label: label.into(), + variant: Default::default(), + state: Default::default(), + icon: None, + icon_position: None, + width: Default::default(), + handlers: ButtonHandlers::default(), + } + } + + pub fn ghost(label: L) -> Self + where + L: Into, + { + 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) -> Self { + self.width = width; + self + } + + pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext) + 'static) -> Self { + self.handlers.click = Some(Rc::new(handler)); + self + } + + fn background_color(&self, cx: &mut ViewContext) -> 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 { + self.icon.map(|i| IconElement::new(i).color(icon_color)) + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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 + } +} diff --git a/crates/ui/src/elements/details.rs b/crates/ui/src/elements/details.rs index f156199f9e..d36b674291 100644 --- a/crates/ui/src/elements/details.rs +++ b/crates/ui/src/elements/details.rs @@ -1,7 +1,4 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - +use crate::prelude::*; use crate::theme; #[derive(Element, Clone)] @@ -10,11 +7,11 @@ pub struct Details { meta: Option<&'static str>, } -pub fn details(text: &'static str) -> Details { - Details { text, meta: None } -} - impl Details { + pub fn new(text: &'static str) -> Self { + Self { text, meta: None } + } + pub fn meta_text(mut self, meta: &'static str) -> Self { self.meta = Some(meta); self diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index 7d2bb45b49..ca357b4f02 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -1,11 +1,19 @@ use std::sync::Arc; +use gpui2::elements::svg; +use gpui2::Hsla; +use strum::EnumIter; + +use crate::prelude::*; use crate::theme::theme; use crate::Theme; -use gpui2::elements::svg; -use gpui2::style::StyleHelpers; -use gpui2::{Element, ViewContext}; -use gpui2::{Hsla, IntoElement}; + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum IconSize { + Small, + #[default] + Large, +} #[derive(Default, PartialEq, Copy, Clone)] pub enum IconColor { @@ -37,8 +45,8 @@ impl IconColor { } } -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconAsset { +#[derive(Default, PartialEq, Copy, Clone, EnumIter)] +pub enum Icon { Ai, ArrowLeft, ArrowRight, @@ -53,6 +61,7 @@ pub enum IconAsset { Close, ExclamationTriangle, File, + FileGeneric, FileDoc, FileGit, FileLock, @@ -67,89 +76,106 @@ pub enum IconAsset { InlayHint, MagicWand, MagnifyingGlass, + Maximize, + Menu, MessageBubbles, Mic, MicMute, Plus, + Quote, Screen, Split, + SplitMessage, Terminal, XCircle, Copilot, Envelope, } -impl IconAsset { +impl Icon { pub fn path(self) -> &'static str { match self { - IconAsset::Ai => "icons/ai.svg", - IconAsset::ArrowLeft => "icons/arrow_left.svg", - IconAsset::ArrowRight => "icons/arrow_right.svg", - IconAsset::ArrowUpRight => "icons/arrow_up_right.svg", - IconAsset::AudioOff => "icons/speaker-off.svg", - IconAsset::AudioOn => "icons/speaker-loud.svg", - IconAsset::Bolt => "icons/bolt.svg", - IconAsset::ChevronDown => "icons/chevron_down.svg", - IconAsset::ChevronLeft => "icons/chevron_left.svg", - IconAsset::ChevronRight => "icons/chevron_right.svg", - IconAsset::ChevronUp => "icons/chevron_up.svg", - IconAsset::Close => "icons/x.svg", - IconAsset::ExclamationTriangle => "icons/warning.svg", - IconAsset::File => "icons/file_icons/file.svg", - IconAsset::FileDoc => "icons/file_icons/book.svg", - IconAsset::FileGit => "icons/file_icons/git.svg", - IconAsset::FileLock => "icons/file_icons/lock.svg", - IconAsset::FileRust => "icons/file_icons/rust.svg", - IconAsset::FileToml => "icons/file_icons/toml.svg", - IconAsset::FileTree => "icons/project.svg", - IconAsset::Folder => "icons/file_icons/folder.svg", - IconAsset::FolderOpen => "icons/file_icons/folder_open.svg", - IconAsset::FolderX => "icons/stop_sharing.svg", - IconAsset::Hash => "icons/hash.svg", - IconAsset::InlayHint => "icons/inlay_hint.svg", - IconAsset::MagicWand => "icons/magic-wand.svg", - IconAsset::MagnifyingGlass => "icons/magnifying_glass.svg", - IconAsset::MessageBubbles => "icons/conversations.svg", - IconAsset::Mic => "icons/mic.svg", - IconAsset::MicMute => "icons/mic-mute.svg", - IconAsset::Plus => "icons/plus.svg", - IconAsset::Screen => "icons/desktop.svg", - IconAsset::Split => "icons/split.svg", - IconAsset::Terminal => "icons/terminal.svg", - IconAsset::XCircle => "icons/error.svg", - IconAsset::Copilot => "icons/copilot.svg", - IconAsset::Envelope => "icons/feedback.svg", + Icon::Ai => "icons/ai.svg", + Icon::ArrowLeft => "icons/arrow_left.svg", + Icon::ArrowRight => "icons/arrow_right.svg", + Icon::ArrowUpRight => "icons/arrow_up_right.svg", + Icon::AudioOff => "icons/speaker-off.svg", + Icon::AudioOn => "icons/speaker-loud.svg", + Icon::Bolt => "icons/bolt.svg", + Icon::ChevronDown => "icons/chevron_down.svg", + Icon::ChevronLeft => "icons/chevron_left.svg", + Icon::ChevronRight => "icons/chevron_right.svg", + Icon::ChevronUp => "icons/chevron_up.svg", + Icon::Close => "icons/x.svg", + Icon::ExclamationTriangle => "icons/warning.svg", + Icon::File => "icons/file.svg", + Icon::FileGeneric => "icons/file_icons/file.svg", + Icon::FileDoc => "icons/file_icons/book.svg", + Icon::FileGit => "icons/file_icons/git.svg", + Icon::FileLock => "icons/file_icons/lock.svg", + Icon::FileRust => "icons/file_icons/rust.svg", + Icon::FileToml => "icons/file_icons/toml.svg", + Icon::FileTree => "icons/project.svg", + Icon::Folder => "icons/file_icons/folder.svg", + Icon::FolderOpen => "icons/file_icons/folder_open.svg", + Icon::FolderX => "icons/stop_sharing.svg", + Icon::Hash => "icons/hash.svg", + Icon::InlayHint => "icons/inlay_hint.svg", + Icon::MagicWand => "icons/magic-wand.svg", + Icon::MagnifyingGlass => "icons/magnifying_glass.svg", + Icon::Maximize => "icons/maximize.svg", + Icon::Menu => "icons/menu.svg", + Icon::MessageBubbles => "icons/conversations.svg", + Icon::Mic => "icons/mic.svg", + Icon::MicMute => "icons/mic-mute.svg", + Icon::Plus => "icons/plus.svg", + Icon::Quote => "icons/quote.svg", + Icon::Screen => "icons/desktop.svg", + Icon::Split => "icons/split.svg", + Icon::SplitMessage => "icons/split_message.svg", + Icon::Terminal => "icons/terminal.svg", + Icon::XCircle => "icons/error.svg", + Icon::Copilot => "icons/copilot.svg", + Icon::Envelope => "icons/feedback.svg", } } } #[derive(Element, Clone)] -pub struct Icon { - asset: IconAsset, +pub struct IconElement { + icon: Icon, color: IconColor, + size: IconSize, } -pub fn icon(asset: IconAsset) -> Icon { - Icon { - asset, - color: IconColor::default(), +impl IconElement { + pub fn new(icon: Icon) -> Self { + Self { + icon, + color: IconColor::default(), + size: IconSize::default(), + } } -} -impl Icon { pub fn color(mut self, color: IconColor) -> Self { self.color = color; self } + pub fn size(mut self, size: IconSize) -> Self { + self.size = size; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let fill = self.color.color(theme); - svg() - .flex_none() - .path(self.asset.path()) - .size_4() - .fill(fill) + let sized_svg = match self.size { + IconSize::Small => svg().size_3p5(), + IconSize::Large => svg().size_4(), + }; + + sized_svg.flex_none().path(self.icon.path()).fill(fill) } } diff --git a/crates/ui/src/elements/indicator.rs b/crates/ui/src/elements/indicator.rs deleted file mode 100644 index 2ee40a57ac..0000000000 --- a/crates/ui/src/elements/indicator.rs +++ /dev/null @@ -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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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) - } -} diff --git a/crates/ui/src/elements/input.rs b/crates/ui/src/elements/input.rs index 5a3da16fdd..1a860028d2 100644 --- a/crates/ui/src/elements/input.rs +++ b/crates/ui/src/elements/input.rs @@ -1,10 +1,13 @@ -use gpui2::elements::div; -use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - use crate::prelude::*; use crate::theme; +#[derive(Default, PartialEq)] +pub enum InputVariant { + #[default] + Ghost, + Filled, +} + #[derive(Element)] pub struct Input { placeholder: &'static str, @@ -13,24 +16,26 @@ pub struct Input { variant: InputVariant, } -pub fn input(placeholder: &'static str) -> Input { - Input { - placeholder, - value: "".to_string(), - state: InteractionState::default(), - variant: InputVariant::default(), - } -} - impl Input { + pub fn new(placeholder: &'static str) -> Self { + Self { + placeholder, + value: "".to_string(), + state: InteractionState::default(), + variant: InputVariant::default(), + } + } + pub fn value(mut self, value: String) -> Self { self.value = value; self } + pub fn state(mut self, state: InteractionState) -> Self { self.state = state; self } + pub fn variant(mut self, variant: InputVariant) -> Self { self.variant = variant; self diff --git a/crates/ui/src/elements/label.rs b/crates/ui/src/elements/label.rs index 90e6b525eb..e7b15aaf02 100644 --- a/crates/ui/src/elements/label.rs +++ b/crates/ui/src/elements/label.rs @@ -1,8 +1,8 @@ +use gpui2::{Hsla, WindowContext}; +use smallvec::SmallVec; + +use crate::prelude::*; use crate::theme::theme; -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, ViewContext}; -use gpui2::{IntoElement, ParentElement}; #[derive(Default, PartialEq, Copy, Clone)] pub enum LabelColor { @@ -12,8 +12,28 @@ pub enum LabelColor { Created, Modified, Deleted, + Disabled, Hidden, Placeholder, + Accent, +} + +impl LabelColor { + pub fn hsla(&self, cx: &WindowContext) -> Hsla { + let theme = theme(cx); + + match self { + Self::Default => theme.middle.base.default.foreground, + Self::Muted => theme.middle.variant.default.foreground, + Self::Created => theme.middle.positive.default.foreground, + Self::Modified => theme.middle.warning.default.foreground, + Self::Deleted => theme.middle.negative.default.foreground, + Self::Disabled => theme.middle.base.disabled.foreground, + Self::Hidden => theme.middle.variant.default.foreground, + Self::Placeholder => theme.middle.base.disabled.foreground, + Self::Accent => theme.middle.accent.default.foreground, + } + } } #[derive(Default, PartialEq, Copy, Clone)] @@ -25,20 +45,27 @@ pub enum LabelSize { #[derive(Element, Clone)] pub struct Label { - label: &'static str, + label: String, color: LabelColor, size: LabelSize, -} - -pub fn label(label: &'static str) -> Label { - Label { - label, - color: LabelColor::Default, - size: LabelSize::Default, - } + highlight_indices: Vec, + strikethrough: bool, } impl Label { + pub fn new(label: L) -> Self + where + L: Into, + { + Self { + label: label.into(), + color: LabelColor::Default, + size: LabelSize::Default, + highlight_indices: Vec::new(), + strikethrough: false, + } + } + pub fn color(mut self, color: LabelColor) -> Self { self.color = color; self @@ -49,27 +76,86 @@ impl Label { self } + pub fn with_highlights(mut self, indices: Vec) -> Self { + self.highlight_indices = indices; + self + } + + pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { + self.strikethrough = strikethrough; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - let color = match self.color { - LabelColor::Default => theme.lowest.base.default.foreground, - LabelColor::Muted => theme.lowest.variant.default.foreground, - LabelColor::Created => theme.lowest.positive.default.foreground, - LabelColor::Modified => theme.lowest.warning.default.foreground, - LabelColor::Deleted => theme.lowest.negative.default.foreground, - LabelColor::Hidden => theme.lowest.variant.default.foreground, - LabelColor::Placeholder => theme.lowest.base.disabled.foreground, - }; + let highlight_color = theme.lowest.accent.default.foreground; - let mut div = div(); + let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - if self.size == LabelSize::Small { - div = div.text_xs(); - } else { - div = div.text_sm(); + let mut runs: SmallVec<[Run; 8]> = SmallVec::new(); + + for (char_ix, char) in self.label.char_indices() { + let mut color = self.color.hsla(cx); + + if let Some(highlight_ix) = highlight_indices.peek() { + if char_ix == *highlight_ix { + color = highlight_color; + + highlight_indices.next(); + } + } + + let last_run = runs.last_mut(); + + let start_new_run = if let Some(last_run) = last_run { + if color == last_run.color { + last_run.text.push(char); + false + } else { + true + } + } else { + true + }; + + if start_new_run { + runs.push(Run { + text: char.to_string(), + color, + }); + } } - div.text_color(color).child(self.label.clone()) + div() + .flex() + .when(self.strikethrough, |this| { + this.relative().child( + div() + .absolute() + .top_px() + .my_auto() + .w_full() + .h_px() + .fill(LabelColor::Hidden.hsla(cx)), + ) + }) + .children(runs.into_iter().map(|run| { + let mut div = div(); + + if self.size == LabelSize::Small { + div = div.text_xs(); + } else { + div = div.text_sm(); + } + + div.text_color(run.color).child(run.text) + })) } } + +/// A run of text that receives the same style. +struct Run { + pub text: String, + pub color: Hsla, +} diff --git a/crates/ui/src/elements/player.rs b/crates/ui/src/elements/player.rs new file mode 100644 index 0000000000..e9e269a2cb --- /dev/null +++ b/crates/ui/src/elements/player.rs @@ -0,0 +1,132 @@ +use gpui2::{Hsla, ViewContext}; + +use crate::theme; + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum PlayerStatus { + #[default] + Offline, + Online, + InCall, + Away, + DoNotDisturb, + Invisible, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum MicStatus { + Muted, + #[default] + Unmuted, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum VideoStatus { + On, + #[default] + Off, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum ScreenShareStatus { + Shared, + #[default] + NotShared, +} + +#[derive(Clone)] +pub struct PlayerCallStatus { + pub mic_status: MicStatus, + /// Indicates if the player is currently speaking + /// And the intensity of the volume coming through + /// + /// 0.0 - 1.0 + pub voice_activity: f32, + pub video_status: VideoStatus, + pub screen_share_status: ScreenShareStatus, + pub in_current_project: bool, + pub disconnected: bool, + pub following: Option>, + pub followers: Option>, +} + +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(&self, cx: &mut ViewContext) -> Hsla { + let theme = theme(cx); + let index = self.index % 8; + theme.players[self.index].cursor + } + + pub fn selection_color(&self, cx: &mut ViewContext) -> 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 + } +} diff --git a/crates/ui/src/elements/stack.rs b/crates/ui/src/elements/stack.rs new file mode 100644 index 0000000000..ef186f5ebe --- /dev/null +++ b/crates/ui/src/elements/stack.rs @@ -0,0 +1,31 @@ +use gpui2::elements::div::Div; + +use crate::prelude::*; + +pub trait Stack: StyleHelpers { + /// Horizontally stacks elements. + fn h_stack(self) -> Self { + self.flex().flex_row().items_center() + } + + /// Vertically stacks elements. + fn v_stack(self) -> Self { + self.flex().flex_col() + } +} + +impl Stack for Div {} + +/// Horizontally stacks elements. +/// +/// Sets `flex()`, `flex_row()`, `items_center()` +pub fn h_stack() -> Div { + div().h_stack() +} + +/// Vertically stacks elements. +/// +/// Sets `flex()`, `flex_col()` +pub fn v_stack() -> Div { + div().v_stack() +} diff --git a/crates/ui/src/elements/text_button.rs b/crates/ui/src/elements/text_button.rs deleted file mode 100644 index 851efdd4f6..0000000000 --- a/crates/ui/src/elements/text_button.rs +++ /dev/null @@ -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(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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()) - } -} diff --git a/crates/ui/src/elements/tool_divider.rs b/crates/ui/src/elements/tool_divider.rs index 2ef29b225f..8b5a191445 100644 --- a/crates/ui/src/elements/tool_divider.rs +++ b/crates/ui/src/elements/tool_divider.rs @@ -1,17 +1,14 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ViewContext}; - +use crate::prelude::*; use crate::theme; #[derive(Element)] pub struct ToolDivider {} -pub fn tool_divider() -> impl Element { - ToolDivider {} -} - impl ToolDivider { + pub fn new() -> Self { + Self {} + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index ae06009238..39156d3ab4 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,5 +1,6 @@ #![allow(dead_code, unused_variables)] +mod children; mod components; mod element_ext; mod elements; @@ -8,10 +9,12 @@ mod static_data; mod theme; mod tokens; -pub use crate::theme::*; +pub use children::*; pub use components::*; pub use element_ext::*; pub use elements::*; pub use prelude::*; pub use static_data::*; pub use tokens::*; + +pub use crate::theme::*; diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 70b9ab4a5e..c3cecbfc61 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -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::(0xEC695E), + mac_os_traffic_light_yellow: rgb::(0xF4BF4F), + mac_os_traffic_light_green: rgb::(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)] pub enum OrderMethod { #[default] @@ -6,20 +154,6 @@ pub enum OrderMethod { MostRecent, } -#[derive(Default, PartialEq)] -pub enum ButtonVariant { - #[default] - Ghost, - Filled, -} - -#[derive(Default, PartialEq)] -pub enum InputVariant { - #[default] - Ghost, - Filled, -} - #[derive(Default, PartialEq, Clone, Copy)] pub enum Shape { #[default] @@ -34,14 +168,13 @@ pub enum DisclosureControlVisibility { Always, } -#[derive(Default, PartialEq, Clone, Copy)] +#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)] pub enum InteractionState { #[default] Enabled, Hovered, Active, Focused, - Dragged, Disabled, } @@ -63,8 +196,60 @@ pub enum SelectedState { Selected, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum Toggleable { + Toggleable(ToggleState), + #[default] + NotToggleable, +} + +impl Toggleable { + pub fn is_toggled(&self) -> bool { + match self { + Self::Toggleable(ToggleState::Toggled) => true, + _ => false, + } + } +} + +impl From for Toggleable { + fn from(state: ToggleState) -> Self { + Self::Toggleable(state) + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum ToggleState { + /// The "on" state of a toggleable element. + /// + /// Example: + /// - A collasable list that is currently expanded + /// - A toggle button that is currently on. Toggled, + /// The "off" state of a toggleable element. + /// + /// Example: + /// - A collasable list that is currently collapsed + /// - A toggle button that is currently off. + #[default] NotToggled, } + +impl From for ToggleState { + fn from(toggleable: Toggleable) -> Self { + match toggleable { + Toggleable::Toggleable(state) => state, + Toggleable::NotToggleable => ToggleState::NotToggled, + } + } +} + +impl From for ToggleState { + fn from(toggled: bool) -> Self { + if toggled { + ToggleState::Toggled + } else { + ToggleState::NotToggled + } + } +} diff --git a/crates/ui/src/static_data.rs b/crates/ui/src/static_data.rs index de946dab28..fed2d40a73 100644 --- a/crates/ui/src/static_data.rs +++ b/crates/ui/src/static_data.rs @@ -1,166 +1,558 @@ +use gpui2::WindowContext; + use crate::{ - label, list_item, palette_item, IconAsset, LabelColor, ListItem, PaletteItem, ToggleState, + Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText, + Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus, + ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, + ToggleState, }; +pub fn static_players() -> Vec { + 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 { + 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 { vec![ - list_item(label("zed")) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("zed")) + .left_icon(Icon::FolderOpen.into()) .indent_level(0) .set_toggle(ToggleState::Toggled), - list_item(label(".cargo")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new(".cargo")) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label(".config")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new(".config")) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label(".git").color(LabelColor::Hidden)) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new(".git").color(LabelColor::Hidden)) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label(".cargo")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new(".cargo")) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label(".idea").color(LabelColor::Hidden)) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new(".idea").color(LabelColor::Hidden)) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label("assets")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("assets")) + .left_icon(Icon::Folder.into()) .indent_level(1) .set_toggle(ToggleState::Toggled), - list_item(label("cargo-target").color(LabelColor::Hidden)) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden)) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label("crates")) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("crates")) + .left_icon(Icon::FolderOpen.into()) .indent_level(1) .set_toggle(ToggleState::Toggled), - list_item(label("activity_indicator")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("activity_indicator")) + .left_icon(Icon::Folder.into()) .indent_level(2), - list_item(label("ai")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("ai")) + .left_icon(Icon::Folder.into()) .indent_level(2), - list_item(label("audio")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("audio")) + .left_icon(Icon::Folder.into()) .indent_level(2), - list_item(label("auto_update")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("auto_update")) + .left_icon(Icon::Folder.into()) .indent_level(2), - list_item(label("breadcrumbs")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("breadcrumbs")) + .left_icon(Icon::Folder.into()) .indent_level(2), - list_item(label("call")) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("call")) + .left_icon(Icon::Folder.into()) .indent_level(2), - list_item(label("sqlez").color(LabelColor::Modified)) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("sqlez").color(LabelColor::Modified)) + .left_icon(Icon::Folder.into()) .indent_level(2) .set_toggle(ToggleState::NotToggled), - list_item(label("gpui2")) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("gpui2")) + .left_icon(Icon::FolderOpen.into()) .indent_level(2) .set_toggle(ToggleState::Toggled), - list_item(label("src")) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("src")) + .left_icon(Icon::FolderOpen.into()) .indent_level(3) .set_toggle(ToggleState::Toggled), - list_item(label("derrive_element.rs")) - .left_icon(IconAsset::FileRust.into()) + ListEntry::new(Label::new("derrive_element.rs")) + .left_icon(Icon::FileRust.into()) .indent_level(4), - list_item(label("storybook").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) + .left_icon(Icon::FolderOpen.into()) .indent_level(1) .set_toggle(ToggleState::Toggled), - list_item(label("docs").color(LabelColor::Default)) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("docs").color(LabelColor::Default)) + .left_icon(Icon::Folder.into()) .indent_level(2) .set_toggle(ToggleState::Toggled), - list_item(label("src").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("src").color(LabelColor::Modified)) + .left_icon(Icon::FolderOpen.into()) .indent_level(3) .set_toggle(ToggleState::Toggled), - list_item(label("ui").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("ui").color(LabelColor::Modified)) + .left_icon(Icon::FolderOpen.into()) .indent_level(4) .set_toggle(ToggleState::Toggled), - list_item(label("component").color(LabelColor::Created)) - .left_icon(IconAsset::FolderOpen.into()) + ListEntry::new(Label::new("component").color(LabelColor::Created)) + .left_icon(Icon::FolderOpen.into()) .indent_level(5) .set_toggle(ToggleState::Toggled), - list_item(label("facepile.rs").color(LabelColor::Default)) - .left_icon(IconAsset::FileRust.into()) + ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default)) + .left_icon(Icon::FileRust.into()) .indent_level(6), - list_item(label("follow_group.rs").color(LabelColor::Default)) - .left_icon(IconAsset::FileRust.into()) + ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default)) + .left_icon(Icon::FileRust.into()) .indent_level(6), - list_item(label("list_item.rs").color(LabelColor::Created)) - .left_icon(IconAsset::FileRust.into()) + ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created)) + .left_icon(Icon::FileRust.into()) .indent_level(6), - list_item(label("tab.rs").color(LabelColor::Default)) - .left_icon(IconAsset::FileRust.into()) + ListEntry::new(Label::new("tab.rs").color(LabelColor::Default)) + .left_icon(Icon::FileRust.into()) .indent_level(6), - list_item(label("target").color(LabelColor::Hidden)) - .left_icon(IconAsset::Folder.into()) + ListEntry::new(Label::new("target").color(LabelColor::Hidden)) + .left_icon(Icon::Folder.into()) .indent_level(1), - list_item(label(".dockerignore")) - .left_icon(IconAsset::File.into()) + ListEntry::new(Label::new(".dockerignore")) + .left_icon(Icon::FileGeneric.into()) .indent_level(1), - list_item(label(".DS_Store").color(LabelColor::Hidden)) - .left_icon(IconAsset::File.into()) + ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden)) + .left_icon(Icon::FileGeneric.into()) .indent_level(1), - list_item(label("Cargo.lock")) - .left_icon(IconAsset::FileLock.into()) + ListEntry::new(Label::new("Cargo.lock")) + .left_icon(Icon::FileLock.into()) .indent_level(1), - list_item(label("Cargo.toml")) - .left_icon(IconAsset::FileToml.into()) + ListEntry::new(Label::new("Cargo.toml")) + .left_icon(Icon::FileToml.into()) .indent_level(1), - list_item(label("Dockerfile")) - .left_icon(IconAsset::File.into()) + ListEntry::new(Label::new("Dockerfile")) + .left_icon(Icon::FileGeneric.into()) .indent_level(1), - list_item(label("Procfile")) - .left_icon(IconAsset::File.into()) + ListEntry::new(Label::new("Procfile")) + .left_icon(Icon::FileGeneric.into()) .indent_level(1), - list_item(label("README.md")) - .left_icon(IconAsset::FileDoc.into()) + ListEntry::new(Label::new("README.md")) + .left_icon(Icon::FileDoc.into()) .indent_level(1), ] + .into_iter() + .map(From::from) + .collect() } pub fn static_project_panel_single_items() -> Vec { vec![ - list_item(label("todo.md")) - .left_icon(IconAsset::FileDoc.into()) + ListEntry::new(Label::new("todo.md")) + .left_icon(Icon::FileDoc.into()) .indent_level(0), - list_item(label("README.md")) - .left_icon(IconAsset::FileDoc.into()) + ListEntry::new(Label::new("README.md")) + .left_icon(Icon::FileDoc.into()) .indent_level(0), - list_item(label("config.json")) - .left_icon(IconAsset::File.into()) + ListEntry::new(Label::new("config.json")) + .left_icon(Icon::FileGeneric.into()) .indent_level(0), ] + .into_iter() + .map(From::from) + .collect() +} + +pub fn static_collab_panel_current_call() -> Vec { + 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 { + 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 { vec![ - palette_item("New File", Some("Ctrl+N")), - palette_item("Open File", Some("Ctrl+O")), - palette_item("Save File", Some("Ctrl+S")), - palette_item("Cut", Some("Ctrl+X")), - palette_item("Copy", Some("Ctrl+C")), - palette_item("Paste", Some("Ctrl+V")), - palette_item("Undo", Some("Ctrl+Z")), - palette_item("Redo", Some("Ctrl+Shift+Z")), - palette_item("Find", Some("Ctrl+F")), - palette_item("Replace", Some("Ctrl+R")), - palette_item("Jump to Line", None), - palette_item("Select All", None), - palette_item("Deselect All", None), - palette_item("Switch Document", None), - palette_item("Insert Line Below", None), - palette_item("Insert Line Above", None), - palette_item("Move Line Up", None), - palette_item("Move Line Down", None), - palette_item("Toggle Comment", None), - palette_item("Delete Line", None), + PaletteItem::new("New File").keybinding(Keybinding::new( + "N".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Open File").keybinding(Keybinding::new( + "O".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Save File").keybinding(Keybinding::new( + "S".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Cut").keybinding(Keybinding::new( + "X".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Copy").keybinding(Keybinding::new( + "C".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Paste").keybinding(Keybinding::new( + "V".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Undo").keybinding(Keybinding::new( + "Z".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Redo").keybinding(Keybinding::new( + "Z".to_string(), + ModifierKeys::new().control(true).shift(true), + )), + PaletteItem::new("Find").keybinding(Keybinding::new( + "F".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Replace").keybinding(Keybinding::new( + "R".to_string(), + ModifierKeys::new().control(true), + )), + PaletteItem::new("Jump to Line"), + PaletteItem::new("Select All"), + PaletteItem::new("Deselect All"), + PaletteItem::new("Switch Document"), + PaletteItem::new("Insert Line Below"), + PaletteItem::new("Insert Line Above"), + PaletteItem::new("Move Line Up"), + PaletteItem::new("Move Line Down"), + PaletteItem::new("Toggle Comment"), + PaletteItem::new("Delete Line"), + ] +} + +pub fn empty_buffer_example() -> Buffer { + Buffer::new().set_rows(Some(BufferRows::default())) +} + +pub fn hello_world_rust_buffer_example(cx: &WindowContext) -> Buffer { + 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(cx: &WindowContext) -> Buffer { + 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 { + 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 { + 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, + }, ] } diff --git a/crates/ui/src/tokens.rs b/crates/ui/src/tokens.rs index 7912820533..5fd5b69a2a 100644 --- a/crates/ui/src/tokens.rs +++ b/crates/ui/src/tokens.rs @@ -1,14 +1,21 @@ use gpui2::geometry::AbsoluteLength; +use gpui2::{hsla, Hsla}; #[derive(Clone, Copy)] pub struct Token { pub list_indent_depth: AbsoluteLength, + pub default_panel_size: AbsoluteLength, + pub state_hover_background: Hsla, + pub state_active_background: Hsla, } impl Default for Token { fn default() -> Self { Self { list_indent_depth: AbsoluteLength::Rems(0.5), + default_panel_size: AbsoluteLength::Rems(16.), + state_hover_background: hsla(0.0, 0.0, 0.0, 0.08), + state_active_background: hsla(0.0, 0.0, 0.0, 0.16), } } }