diff --git a/Cargo.lock b/Cargo.lock index f1be323064..c0b4539a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7606,6 +7606,7 @@ dependencies = [ "serde", "settings", "simplelog", + "smallvec", "strum", "theme", "util", diff --git a/crates/gpui3/src/elements/text.rs b/crates/gpui3/src/elements/text.rs index 77390e79a4..1ed31716fd 100644 --- a/crates/gpui3/src/elements/text.rs +++ b/crates/gpui3/src/elements/text.rs @@ -25,6 +25,18 @@ impl IntoAnyElement for &'static str { } } +// TODO: Figure out how to pass `String` to `child` without this. +// This impl doesn't exist in the `gpui2` crate. +impl IntoAnyElement for String { + fn into_any(self) -> AnyElement { + Text { + text: ArcCow::from(self), + state_type: PhantomData, + } + .into_any() + } +} + pub struct Text { text: ArcCow<'static, str>, state_type: PhantomData, diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 1afcf1e298..021bf9a46a 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -20,6 +20,7 @@ rust-embed.workspace = true serde.workspace = true settings = { path = "../settings" } simplelog = "0.9" +smallvec.workspace = true strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/storybook2/src/stories.rs b/crates/storybook2/src/stories.rs new file mode 100644 index 0000000000..95b8844157 --- /dev/null +++ b/crates/storybook2/src/stories.rs @@ -0,0 +1,3 @@ +pub mod components; +pub mod elements; +pub mod kitchen_sink; diff --git a/crates/storybook2/src/stories/components.rs b/crates/storybook2/src/stories/components.rs new file mode 100644 index 0000000000..7a6b95837a --- /dev/null +++ b/crates/storybook2/src/stories/components.rs @@ -0,0 +1 @@ +pub mod panel; diff --git a/crates/storybook2/src/stories/components/panel.rs b/crates/storybook2/src/stories/components/panel.rs new file mode 100644 index 0000000000..15b900c5ff --- /dev/null +++ b/crates/storybook2/src/stories/components/panel.rs @@ -0,0 +1,35 @@ +use std::marker::PhantomData; + +use crate::ui::prelude::*; +use crate::ui::{Label, Panel}; + +use crate::story::Story; + +#[derive(Element)] +pub struct PanelStory { + state_type: PhantomData, +} + +impl PanelStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + Story::container(cx) + .child(Story::title_for::<_, Panel>(cx)) + .child(Story::label(cx, "Default")) + .child(Panel::new( + ScrollState::default(), + |_, _| { + vec![div() + .overflow_y_scroll(ScrollState::default()) + .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))) + .into_any()] + }, + Box::new(()), + )) + } +} diff --git a/crates/storybook2/src/stories/elements.rs b/crates/storybook2/src/stories/elements.rs new file mode 100644 index 0000000000..0006163a0e --- /dev/null +++ b/crates/storybook2/src/stories/elements.rs @@ -0,0 +1 @@ +pub mod label; diff --git a/crates/storybook2/src/stories/elements/label.rs b/crates/storybook2/src/stories/elements/label.rs new file mode 100644 index 0000000000..5255b35286 --- /dev/null +++ b/crates/storybook2/src/stories/elements/label.rs @@ -0,0 +1,28 @@ +use std::marker::PhantomData; + +use crate::ui::prelude::*; +use crate::ui::Label; + +use crate::story::Story; + +#[derive(Element)] +pub struct LabelStory { + state_type: PhantomData, +} + +impl LabelStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + 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/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs new file mode 100644 index 0000000000..a944dcded3 --- /dev/null +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -0,0 +1,36 @@ +use std::marker::PhantomData; + +use strum::IntoEnumIterator; + +use crate::story::Story; +use crate::story_selector::{ComponentStory, ElementStory}; +use crate::ui::prelude::*; + +#[derive(Element)] +pub struct KitchenSinkStory { + state_type: PhantomData, +} + +impl KitchenSinkStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let element_stories = ElementStory::iter().map(|selector| selector.story()); + let component_stories = ComponentStory::iter().map(|selector| selector.story()); + + Story::container(cx) + .overflow_y_scroll(ScrollState::default()) + .child(Story::title(cx, "Kitchen Sink")) + .child(Story::label(cx, "Elements")) + .child(div().flex().flex_col().children_any(element_stories)) + .child(Story::label(cx, "Components")) + .child(div().flex().flex_col().children_any(component_stories)) + // Add a bit of space at the bottom of the kitchen sink so elements + // don't end up squished right up against the bottom of the screen. + .child(div().p_4()) + } +} diff --git a/crates/storybook2/src/story.rs b/crates/storybook2/src/story.rs new file mode 100644 index 0000000000..2b35274240 --- /dev/null +++ b/crates/storybook2/src/story.rs @@ -0,0 +1,52 @@ +use crate::theme::theme; +use crate::ui::prelude::*; +use gpui3::Div; + +pub struct Story {} + +impl Story { + pub fn container(cx: &mut ViewContext) -> Div { + let theme = theme(cx); + + div() + .size_full() + .flex() + .flex_col() + .pt_2() + .px_4() + .font("Zed Mono Extended") + .fill(theme.lowest.base.default.background) + } + + pub fn title( + cx: &mut ViewContext, + title: &str, + ) -> impl Element { + let theme = theme(cx); + + div() + .text_xl() + .text_color(theme.lowest.base.default.foreground) + .child(title.to_owned()) + } + + pub fn title_for( + cx: &mut ViewContext, + ) -> impl Element { + Self::title(cx, std::any::type_name::()) + } + + pub fn label( + cx: &mut ViewContext, + label: &str, + ) -> impl Element { + let theme = theme(cx); + + div() + .mt_4() + .mb_2() + .text_xs() + .text_color(theme.lowest.base.default.foreground) + .child(label.to_owned()) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs new file mode 100644 index 0000000000..ef867b16f5 --- /dev/null +++ b/crates/storybook2/src/story_selector.rs @@ -0,0 +1,116 @@ +use std::str::FromStr; +use std::sync::OnceLock; + +use anyhow::{anyhow, Context}; +use clap::builder::PossibleValue; +use clap::ValueEnum; +use gpui3::AnyElement; +use strum::{EnumIter, EnumString, IntoEnumIterator}; + +use crate::ui::prelude::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] +#[strum(serialize_all = "snake_case")] +pub enum ElementStory { + Label, +} + +impl ElementStory { + pub fn story(&self) -> AnyElement { + use crate::stories::elements; + + match self { + Self::Label => elements::label::LabelStory::new().into_any(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] +#[strum(serialize_all = "snake_case")] +pub enum ComponentStory { + Panel, +} + +impl ComponentStory { + pub fn story(&self) -> AnyElement { + use crate::stories::components; + + match self { + Self::Panel => components::panel::PanelStory::new().into_any(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum StorySelector { + Element(ElementStory), + Component(ComponentStory), + KitchenSink, +} + +impl FromStr for StorySelector { + type Err = anyhow::Error; + + 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}'"))?; + + return Ok(Self::Element(element_story)); + } + + if let Some((_, story)) = story.split_once("components/") { + let component_story = ComponentStory::from_str(story) + .with_context(|| format!("story not found for component '{story}'"))?; + + return Ok(Self::Component(component_story)); + } + + Err(anyhow!("story not found for '{raw_story_name}'")) + } +} + +impl StorySelector { + pub fn story(&self) -> AnyElement { + match self { + Self::Element(element_story) => element_story.story(), + Self::Component(component_story) => component_story.story(), + Self::KitchenSink => crate::stories::kitchen_sink::KitchenSinkStory::new().into_any(), + } + } +} + +/// The list of all stories available in the storybook. +static ALL_STORY_SELECTORS: OnceLock> = OnceLock::new(); + +impl ValueEnum for StorySelector { + fn value_variants<'a>() -> &'a [Self] { + 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 + } + + 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/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 4fdd1140fb..5745f209cf 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -9,6 +9,9 @@ use workspace::workspace; mod assets; mod collab_panel; +mod stories; +mod story; +mod story_selector; mod theme; mod themes; mod ui; diff --git a/crates/storybook2/src/ui.rs b/crates/storybook2/src/ui.rs index 8b09f6f0ff..bac8dc9dcc 100644 --- a/crates/storybook2/src/ui.rs +++ b/crates/storybook2/src/ui.rs @@ -2,9 +2,11 @@ mod children; mod components; mod elements; pub mod prelude; +mod theme; mod tokens; pub use children::*; pub use components::*; pub use elements::*; +pub use theme::*; pub use tokens::*; diff --git a/crates/storybook2/src/ui/elements.rs b/crates/storybook2/src/ui/elements.rs index b21354ddfa..fe0a9dbbc7 100644 --- a/crates/storybook2/src/ui/elements.rs +++ b/crates/storybook2/src/ui/elements.rs @@ -1,3 +1,5 @@ +mod label; mod stack; +pub use label::*; pub use stack::*; diff --git a/crates/storybook2/src/ui/elements/label.rs b/crates/storybook2/src/ui/elements/label.rs new file mode 100644 index 0000000000..c2d5a7422b --- /dev/null +++ b/crates/storybook2/src/ui/elements/label.rs @@ -0,0 +1,165 @@ +use std::marker::PhantomData; + +use gpui3::{Hsla, WindowContext}; +use smallvec::SmallVec; + +use crate::theme::theme; +use crate::ui::prelude::*; + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum LabelColor { + #[default] + Default, + Muted, + 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)] +pub enum LabelSize { + #[default] + Default, + Small, +} + +#[derive(Element, Clone)] +pub struct Label { + state_type: PhantomData, + label: String, + color: LabelColor, + size: LabelSize, + highlight_indices: Vec, + strikethrough: bool, +} + +impl Label { + pub fn new(label: L) -> Self + where + L: Into, + { + Self { + state_type: PhantomData, + 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 + } + + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + 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, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + + let highlight_color = theme.lowest.accent.default.foreground; + + let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); + + 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() + .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/storybook2/src/ui/prelude.rs b/crates/storybook2/src/ui/prelude.rs index d89bb944e5..646567fa73 100644 --- a/crates/storybook2/src/ui/prelude.rs +++ b/crates/storybook2/src/ui/prelude.rs @@ -1,3 +1,5 @@ -pub use gpui3::{Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext}; +pub use gpui3::{ + div, Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext, +}; pub use crate::ui::{HackyChildren, HackyChildrenPayload}; diff --git a/crates/storybook2/src/ui/theme.rs b/crates/storybook2/src/ui/theme.rs new file mode 100644 index 0000000000..d81dc444d4 --- /dev/null +++ b/crates/storybook2/src/ui/theme.rs @@ -0,0 +1,10 @@ +use std::sync::Arc; + +use gpui3::WindowContext; + +use crate::theme::Theme; +use crate::themes::rose_pine_dawn; + +pub fn theme(cx: &WindowContext) -> Arc { + Arc::new(rose_pine_dawn()) +}