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;
Marshall Bowers created
crates/storybook2/src/stories/components.rs | 1
crates/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(+)
@@ -2,4 +2,5 @@ pub mod assistant_panel;
pub mod buffer;
pub mod panel;
pub mod project_panel;
+pub mod tab;
pub mod workspace;
@@ -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<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> TabStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let git_statuses = GitStatus::iter();
+ let fs_statuses = FileSystemStatus::iter();
+
+ Story::container(cx)
+ .child(Story::title_for::<_, Tab<S>>(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)
+ }))),
+ )
+ }
+}
@@ -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(),
}
}
@@ -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::*;
@@ -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<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+ title: String,
+ icon: Option<Icon>,
+ current: bool,
+ dirty: bool,
+ fs_status: FileSystemStatus,
+ git_status: GitStatus,
+ diagnostic_status: DiagnosticStatus,
+ close_side: IconSide,
+}
+
+impl<S: 'static + Send + Sync + Clone> Tab<S> {
+ 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<I>(mut self, icon: I) -> Self
+ where
+ I: Into<Option<Icon>>,
+ {
+ self.icon = icon.into();
+ self
+ }
+
+ pub fn dirty(mut self, dirty: bool) -> Self {
+ self.dirty = dirty;
+ self
+ }
+
+ pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
+ self.fs_status = fs_status;
+ self
+ }
+
+ pub fn git_status(mut self, git_status: GitStatus) -> Self {
+ self.git_status = git_status;
+ self
+ }
+
+ pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
+ self.diagnostic_status = diagnostic_status;
+ self
+ }
+
+ pub fn close_side(mut self, close_side: IconSide) -> Self {
+ self.close_side = close_side;
+ self
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ 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
+ }),
+ )
+ }
+}