diff --git a/Cargo.lock b/Cargo.lock index 4281553aff..c5ec126de5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9211,6 +9211,7 @@ name = "story" version = "0.1.0" dependencies = [ "gpui2", + "smallvec", ] [[package]] @@ -9224,6 +9225,7 @@ dependencies = [ "editor2", "fuzzy2", "gpui2", + "indoc", "itertools 0.11.0", "language2", "log", diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 384447af8f..b042cbab80 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -8,3 +8,4 @@ publish = false [dependencies] gpui = { package = "gpui2", path = "../gpui2" } +smallvec.workspace = true diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs index d95c879ce0..c656f3729d 100644 --- a/crates/story/src/story.rs +++ b/crates/story/src/story.rs @@ -1,22 +1,109 @@ -use gpui::prelude::*; -use gpui::{div, hsla, Div, SharedString}; +use gpui::{div, hsla, AnyElement, Div, ElementId, Hsla, SharedString, Stateful, WindowContext}; +use gpui::{prelude::*, px}; +use smallvec::SmallVec; + +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub fn reasonably_unique_id() -> String { + let now = SystemTime::now(); + let timestamp = now.duration_since(UNIX_EPOCH).unwrap(); + + let cnt = COUNTER.fetch_add(1, Ordering::Relaxed); + + let id = format!("{}_{}", timestamp.as_nanos(), cnt); + + id +} + +pub struct StoryColor { + pub primary: Hsla, + pub secondary: Hsla, + pub border: Hsla, + pub background: Hsla, + pub card_background: Hsla, + pub divider: Hsla, + pub link: Hsla, +} + +impl StoryColor { + pub fn new() -> Self { + Self { + primary: hsla(216. / 360., 11. / 100., 0. / 100., 1.), + secondary: hsla(216. / 360., 11. / 100., 16. / 100., 1.), + border: hsla(216. / 360., 11. / 100., 91. / 100., 1.), + background: hsla(0. / 360., 0. / 100., 100. / 100., 1.), + card_background: hsla(0. / 360., 0. / 100., 96. / 100., 1.), + divider: hsla(216. / 360., 11. / 100., 91. / 100., 1.), + link: hsla(206. / 360., 100. / 100., 50. / 100., 1.), + } + } +} + +pub fn story_color() -> StoryColor { + StoryColor::new() +} pub struct Story {} impl Story { pub fn container() -> Div { - div().size_full().flex().flex_col().pt_2().px_4().bg(hsla( - 0. / 360., - 0. / 100., - 100. / 100., - 1., - )) + div() + .size_full() + .flex() + .flex_col() + .bg(story_color().background) + } + + // TODO: Move all stories to container2, then rename + pub fn container2(relative_path: &'static str) -> Div { + div() + .size_full() + .flex() + .flex_col() + .bg(story_color().background) + .child( + div() + .flex() + .justify_between() + .p_2() + .border_b() + .border_color(story_color().border) + .child(Story::title_for::()) + .child( + div() + .text_xs() + .text_color(story_color().primary) + .child(Story::open_story_link(relative_path)), + ), + ) + } + + pub fn open_story_link(relative_path: &'static str) -> impl Element { + let path = PathBuf::from_iter([relative_path]); + div() + .id(SharedString::from(format!("id_{}", relative_path))) + .text_xs() + .text_color(story_color().primary) + .on_click({ + let path = path.clone(); + + move |_event, _cx| { + let path = format!("{}:0:0", path.to_string_lossy()); + + std::process::Command::new("zed").arg(path).spawn().ok(); + } + }) + .child(Story::link(path.to_string_lossy().to_string())) } pub fn title(title: impl Into) -> impl Element { div() - .text_xl() - .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.)) + .text_xs() + .text_color(story_color().primary) .child(title.into()) } @@ -24,12 +111,173 @@ impl Story { Self::title(std::any::type_name::()) } + pub fn section() -> Div { + div().mt_4().mb_2() + } + + pub fn section_title() -> Div { + div().text_lg().text_color(story_color().primary) + } + + pub fn group() -> Div { + div().my_2().bg(story_color().background) + } + + pub fn code_block(code: impl Into) -> Div { + div() + .size_full() + .p_2() + .bg(gpui::black()) + .border() + .border_color(story_color().border) + .rounded_md() + .text_sm() + .text_color(gpui::white()) + .child(code.into()) + } + + pub fn divider() -> Div { + div().my_2().h(px(1.)).bg(story_color().divider) + } + + pub fn link(link: impl Into) -> impl Element { + div() + .id(ElementId::from(SharedString::from(reasonably_unique_id()))) + .text_xs() + .text_color(story_color().link) + .cursor(gpui::CursorStyle::PointingHand) + .child(link.into()) + } + + pub fn description(description: impl Into) -> impl Element { + div() + .text_sm() + .text_color(story_color().secondary) + .min_w_96() + .child(description.into()) + } + pub fn label(label: impl Into) -> impl Element { div() - .mt_4() - .mb_2() .text_xs() - .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.)) + .text_color(story_color().primary) .child(label.into()) } + + /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate. + pub fn v_stack() -> Div { + div().flex().flex_col().gap_1() + } +} + +#[derive(IntoElement)] +pub struct StoryItem { + label: SharedString, + item: AnyElement, + description: Option, + usage: Option, +} + +impl StoryItem { + pub fn new(label: impl Into, item: impl IntoElement) -> Self { + Self { + label: label.into(), + item: item.into_any_element(), + description: None, + usage: None, + } + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn usage(mut self, code: impl Into) -> Self { + self.usage = Some(code.into()); + self + } +} + +impl RenderOnce for StoryItem { + type Rendered = Div; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + div() + .my_2() + .flex() + .w_full() + .child( + Story::v_stack() + .px_2() + .flex_none() + .w_1_2() + .min_h_px() + .child(Story::label(self.label)) + .child( + div() + .rounded_sm() + .bg(story_color().card_background) + .border() + .border_color(story_color().border) + .child(self.item), + ) + .when_some(self.description, |this, description| { + this.child(Story::description(description)) + }), + ) + .child( + Story::v_stack() + .px_2() + .flex_none() + .w_1_2() + .min_h_px() + .when_some(self.usage, |this, usage| { + this.child(Story::label("Usage")) + .child(Story::code_block(usage)) + }), + ) + } +} + +#[derive(IntoElement)] +pub struct StorySection { + description: Option, + children: SmallVec<[AnyElement; 2]>, +} + +impl StorySection { + pub fn new() -> Self { + Self { + description: None, + children: SmallVec::new(), + } + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } +} + +impl RenderOnce for StorySection { + type Rendered = Div; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + Story::section() + // Section title + .py_2() + // Section description + .when_some(self.description.clone(), |section, description| { + section.child(Story::description(description)) + }) + .child(div().flex().flex_col().gap_2().children(self.children)) + .child(Story::divider()) + } +} + +impl ParentElement for StorySection { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } } diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 949d07b06f..672bebe20e 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -22,6 +22,7 @@ editor = { package = "editor2", path = "../editor2" } chrono = "0.4" fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } +indoc.workspace = true itertools = "0.11.0" language = { package = "language2", path = "../language2" } log.workspace = true diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index b279e004c7..279a2dbf4d 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,8 +1,6 @@ -use gpui::{ - blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render, - Styled, StyledText, View, VisualContext, WindowContext, -}; -use ui::v_stack; +use gpui::{div, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext}; +use indoc::indoc; +use story::*; pub struct TextStory; @@ -16,59 +14,82 @@ impl Render for TextStory { type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - v_stack() - .bg(blue()) - .child( - div() - .flex() - .child(div().max_w_96().bg(white()).child(concat!( - "max-width: 96. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - ))), - ) - .child(div().h_5()) - .child(div().flex().flex_col().w_96().bg(white()).child(concat!( - "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - ))) - .child(div().h_5()) - .child( - div() - .flex() - .child(div().min_w_96().bg(white()).child(concat!( - "min-width: 96. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", -)))) - .child(div().h_5()) - .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!( - "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )))) - // NOTE: When rendering text in a horizonal flex container, - // Taffy will not pass width constraints down from the parent. - // To fix this, render text in a parent with overflow: hidden - .child(div().h_5()) - .child(div().flex().w_96().bg(red()).child(concat!( - "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - ))).child( - InteractiveText::new( - "interactive", - StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [ - (6..11, HighlightStyle { - background_color: Some(green()), - ..Default::default() - }), - ]), + // let # = "The quick brown fox jumps over the lazy dog. Meanwhile, the lazy dog decided it was time for a change. He started daily workout routines, ate healthier and became the fastest dog in town."; + + Story::container2::("crates/storybook2/src/stories/text.rs").child( + StorySection::new().child( + StoryItem::new( + "Default Text", + div().flex().child(div().max_w_96().child("foo")), ) - .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { - println!("Clicked range {range_ix}"); - }) - ) + .description("Text with a max-width. Wraps based on set max-width.") + .usage(indoc! {r##" + div().max_w_96() + .child("Some text that you want to wrap.") + "## + }), + ), + ) } } + +// impl Render for TextStory { +// type Element = Div; + +// fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { +// v_stack() +// .bg(blue()) +// .child( +// div() +// .flex() +// .child(div().max_w_96().bg(white()).child(concat!( +// "max-width: 96. The quick brown fox jumps over the lazy dog. ", +// "Meanwhile, the lazy dog decided it was time for a change. ", +// "He started daily workout routines, ate healthier and became the fastest dog in town.", +// ))), +// ) +// .child(div().h_5()) +// .child(div().flex().flex_col().w_96().bg(white()).child(concat!( +// "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ", +// "Meanwhile, the lazy dog decided it was time for a change. ", +// "He started daily workout routines, ate healthier and became the fastest dog in town.", +// ))) +// .child(div().h_5()) +// .child( +// div() +// .flex() +// .child(div().min_w_96().bg(white()).child(concat!( +// "min-width: 96. The quick brown fox jumps over the lazy dog. ", +// "Meanwhile, the lazy dog decided it was time for a change. ", +// "He started daily workout routines, ate healthier and became the fastest dog in town.", +// )))) +// .child(div().h_5()) +// .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!( +// "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ", +// "Meanwhile, the lazy dog decided it was time for a change. ", +// "He started daily workout routines, ate healthier and became the fastest dog in town.", +// )))) +// // NOTE: When rendering text in a horizonal flex container, +// // Taffy will not pass width constraints down from the parent. +// // To fix this, render text in a parent with overflow: hidden +// .child(div().h_5()) +// .child(div().flex().w_96().bg(red()).child(concat!( +// "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", +// "Meanwhile, the lazy dog decided it was time for a change. ", +// "He started daily workout routines, ate healthier and became the fastest dog in town.", +// ))).child( +// InteractiveText::new( +// "interactive", +// StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [ +// (6..11, HighlightStyle { +// background_color: Some(green()), +// ..Default::default() +// }), +// ]), +// ) +// .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { +// println!("Clicked range {range_ix}"); +// }) +// ) +// } +// }