From 8db7f7ed37529aa24966e335d5e93ff190820f29 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 6 Oct 2023 18:43:25 -0400 Subject: [PATCH] Add `Tab` component --- crates/storybook2/src/stories/components.rs | 1 + .../storybook2/src/stories/components/tab.rs | 101 +++++++++++++ crates/storybook2/src/story_selector.rs | 2 + crates/ui2/src/components.rs | 2 + crates/ui2/src/components/tab.rs | 135 ++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 crates/storybook2/src/stories/components/tab.rs create mode 100644 crates/ui2/src/components/tab.rs diff --git a/crates/storybook2/src/stories/components.rs b/crates/storybook2/src/stories/components.rs index d1b30fbbf08c78023dc5628e35bd0a6830f88a86..51dea1b675a67de56daa8dc6ee60a4ef013ec5eb 100644 --- a/crates/storybook2/src/stories/components.rs +++ b/crates/storybook2/src/stories/components.rs @@ -2,4 +2,5 @@ pub mod assistant_panel; pub mod buffer; pub mod panel; pub mod project_panel; +pub mod tab; pub mod workspace; diff --git a/crates/storybook2/src/stories/components/tab.rs b/crates/storybook2/src/stories/components/tab.rs new file mode 100644 index 0000000000000000000000000000000000000000..d2435d780b1dea31e4016525d70c8e1692b1a59a --- /dev/null +++ b/crates/storybook2/src/stories/components/tab.rs @@ -0,0 +1,101 @@ +use std::marker::PhantomData; + +use strum::IntoEnumIterator; +use ui::prelude::*; +use ui::{h_stack, v_stack, Tab}; + +use crate::story::Story; + +#[derive(Element)] +pub struct TabStory { + state_type: PhantomData, +} + +impl TabStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + 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/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 4823b0b9bf538f10826813aadc7224325c525137..e3e04b56090344628d4e7c279cda4086ea5e28bb 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -38,6 +38,7 @@ pub enum ComponentStory { Buffer, Panel, ProjectPanel, + Tab, Workspace, } @@ -52,6 +53,7 @@ impl ComponentStory { Self::Buffer => components::buffer::BufferStory::new().into_any(), Self::Panel => components::panel::PanelStory::new().into_any(), Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(), + Self::Tab => components::tab::TabStory::new().into_any(), Self::Workspace => components::workspace::WorkspaceStory::new().into_any(), } } diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 7ff6453cb8e8eb15fa12d110fbcb3026d25c348e..31073b74bd1a07f77b32bf7f567f5df40df21320 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -6,6 +6,7 @@ mod panel; mod panes; mod project_panel; mod status_bar; +mod tab; mod workspace; pub use assistant_panel::*; @@ -16,4 +17,5 @@ pub use panel::*; pub use panes::*; pub use project_panel::*; pub use status_bar::*; +pub use tab::*; pub use workspace::*; diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs new file mode 100644 index 0000000000000000000000000000000000000000..c53ac6f8f626458ae4dc694623d320c06d31a7ec --- /dev/null +++ b/crates/ui2/src/components/tab.rs @@ -0,0 +1,135 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor}; + +#[derive(Element, Clone)] +pub struct Tab { + state_type: PhantomData, + 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 { + state_type: PhantomData, + 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, cx: &mut ViewContext) -> impl Element { + 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() + .py_0p5() + .flex() + .items_center() + .justify_center() + .fill(if self.current { + theme.highest.base.default.background + } else { + theme.middle.base.default.background + }) + .child( + div() + .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 { + None + }) + .child(label) + .children(if self.close_side == IconSide::Right { + Some(close_icon) + } else { + None + }), + ) + } +}