Cargo.lock 🔗
@@ -7606,6 +7606,7 @@ dependencies = [
"serde",
"settings",
"simplelog",
+ "smallvec",
"strum",
"theme",
"util",
Marshall Bowers created
Cargo.lock | 1
crates/gpui3/src/elements/text.rs | 12 +
crates/storybook2/Cargo.toml | 1
crates/storybook2/src/stories.rs | 3
crates/storybook2/src/stories/components.rs | 1
crates/storybook2/src/stories/components/panel.rs | 35 +++
crates/storybook2/src/stories/elements.rs | 1
crates/storybook2/src/stories/elements/label.rs | 28 ++
crates/storybook2/src/stories/kitchen_sink.rs | 36 +++
crates/storybook2/src/story.rs | 52 +++++
crates/storybook2/src/story_selector.rs | 116 +++++++++++
crates/storybook2/src/storybook2.rs | 3
crates/storybook2/src/ui.rs | 2
crates/storybook2/src/ui/elements.rs | 2
crates/storybook2/src/ui/elements/label.rs | 165 +++++++++++++++++
crates/storybook2/src/ui/prelude.rs | 4
crates/storybook2/src/ui/theme.rs | 10 +
17 files changed, 471 insertions(+), 1 deletion(-)
@@ -7606,6 +7606,7 @@ dependencies = [
"serde",
"settings",
"simplelog",
+ "smallvec",
"strum",
"theme",
"util",
@@ -25,6 +25,18 @@ impl<V: 'static> IntoAnyElement<V> for &'static str {
}
}
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<S: 'static> IntoAnyElement<S> for String {
+ fn into_any(self) -> AnyElement<S> {
+ Text {
+ text: ArcCow::from(self),
+ state_type: PhantomData,
+ }
+ .into_any()
+ }
+}
+
pub struct Text<S> {
text: ArcCow<'static, str>,
state_type: PhantomData<S>,
@@ -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" }
@@ -0,0 +1,3 @@
+pub mod components;
+pub mod elements;
+pub mod kitchen_sink;
@@ -0,0 +1 @@
+pub mod panel;
@@ -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<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync> PanelStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ Story::container(cx)
+ .child(Story::title_for::<_, Panel<S>>(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(()),
+ ))
+ }
+}
@@ -0,0 +1 @@
+pub mod label;
@@ -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<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync> LabelStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ Story::container(cx)
+ .child(Story::title_for::<_, Label<S>>(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]))
+ }
+}
@@ -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<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync> KitchenSinkStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ 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())
+ }
+}
@@ -0,0 +1,52 @@
+use crate::theme::theme;
+use crate::ui::prelude::*;
+use gpui3::Div;
+
+pub struct Story {}
+
+impl Story {
+ pub fn container<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> Div<S> {
+ 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<S: 'static + Send + Sync>(
+ cx: &mut ViewContext<S>,
+ title: &str,
+ ) -> impl Element<State = S> {
+ let theme = theme(cx);
+
+ div()
+ .text_xl()
+ .text_color(theme.lowest.base.default.foreground)
+ .child(title.to_owned())
+ }
+
+ pub fn title_for<S: 'static + Send + Sync, T>(
+ cx: &mut ViewContext<S>,
+ ) -> impl Element<State = S> {
+ Self::title(cx, std::any::type_name::<T>())
+ }
+
+ pub fn label<S: 'static + Send + Sync>(
+ cx: &mut ViewContext<S>,
+ label: &str,
+ ) -> impl Element<State = S> {
+ let theme = theme(cx);
+
+ div()
+ .mt_4()
+ .mb_2()
+ .text_xs()
+ .text_color(theme.lowest.base.default.foreground)
+ .child(label.to_owned())
+ }
+}
@@ -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<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
+ 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<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
+ 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<Self, Self::Err> {
+ 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<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
+ 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<Vec<StorySelector>> = 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::<Vec<_>>()
+ });
+
+ stories
+ }
+
+ fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
+ 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))
+ }
+}
@@ -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;
@@ -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::*;
@@ -1,3 +1,5 @@
+mod label;
mod stack;
+pub use label::*;
pub use stack::*;
@@ -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<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+ label: String,
+ color: LabelColor,
+ size: LabelSize,
+ highlight_indices: Vec<usize>,
+ strikethrough: bool,
+}
+
+impl<S: 'static + Send + Sync> Label<S> {
+ pub fn new<L>(label: L) -> Self
+ where
+ L: Into<String>,
+ {
+ 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<usize>) -> 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<S>) -> impl Element<State = S> {
+ 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,
+}
@@ -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};
@@ -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<Theme> {
+ Arc::new(rose_pine_dawn())
+}