Factor story boilerplate out into separate components (#3016)

Marshall Bowers created

This PR factors out the bulk of the boilerplate required to setup a
story in the storybook out into separate components.

The pattern we're using here is adapted from the "[associated
component](https://maxdeviant.com/posts/2021/react-associated-components/)"
pattern in React.

Release Notes:

- N/A

Change summary

crates/storybook/src/stories/components/facepile.rs       | 24 ++------
crates/storybook/src/stories/components/traffic_lights.rs | 22 +------
crates/storybook/src/stories/elements/avatar.rs           | 20 +-----
crates/storybook/src/story.rs                             | 25 +++++++++
crates/storybook/src/storybook.rs                         | 13 ++--
5 files changed, 49 insertions(+), 55 deletions(-)

Detailed changes

crates/storybook/src/stories/components/facepile.rs 🔗

@@ -1,8 +1,10 @@
 use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext};
-use ui::{avatar, theme};
-use ui::{facepile, prelude::*};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+use ui::prelude::*;
+use ui::{avatar, facepile, theme};
+
+use crate::story::Story;
 
 #[derive(Element, Default)]
 pub struct FacepileStory {}
@@ -11,20 +13,8 @@ impl FacepileStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        div()
-            .size_full()
-            .flex()
-            .flex_col()
-            .pt_2()
-            .px_4()
-            .font("Zed Mono Extended")
-            .fill(rgb::<Hsla>(0x282c34))
-            .child(
-                div()
-                    .text_2xl()
-                    .text_color(rgb::<Hsla>(0xffffff))
-                    .child(std::any::type_name::<ui::Facepile>()),
-            )
+        Story::container()
+            .child(Story::title(std::any::type_name::<ui::Facepile>()))
             .child(
                 div()
                     .flex()

crates/storybook/src/stories/components/traffic_lights.rs 🔗

@@ -1,8 +1,8 @@
-use gpui2::elements::div;
-use gpui2::style::StyleHelpers;
-use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 use ui::{theme, traffic_lights};
 
+use crate::story::Story;
+
 #[derive(Element, Default)]
 pub struct TrafficLightsStory {}
 
@@ -10,20 +10,8 @@ impl TrafficLightsStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        div()
-            .size_full()
-            .flex()
-            .flex_col()
-            .pt_2()
-            .px_4()
-            .font("Zed Mono Extended")
-            .fill(rgb::<Hsla>(0x282c34))
-            .child(
-                div()
-                    .text_2xl()
-                    .text_color(rgb::<Hsla>(0xffffff))
-                    .child(std::any::type_name::<ui::TrafficLights>()),
-            )
+        Story::container()
+            .child(Story::title(std::any::type_name::<ui::TrafficLights>()))
             .child(traffic_lights())
     }
 }

crates/storybook/src/stories/elements/avatar.rs 🔗

@@ -1,9 +1,11 @@
 use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 use ui::prelude::*;
 use ui::{avatar, theme};
 
+use crate::story::Story;
+
 #[derive(Element, Default)]
 pub struct AvatarStory {}
 
@@ -11,20 +13,8 @@ impl AvatarStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        div()
-            .size_full()
-            .flex()
-            .flex_col()
-            .pt_2()
-            .px_4()
-            .font("Zed Mono Extended")
-            .fill(rgb::<Hsla>(0x282c34))
-            .child(
-                div()
-                    .text_2xl()
-                    .text_color(rgb::<Hsla>(0xffffff))
-                    .child(std::any::type_name::<ui::Avatar>()),
-            )
+        Story::container()
+            .child(Story::title(std::any::type_name::<ui::Avatar>()))
             .child(
                 div()
                     .flex()

crates/storybook/src/story.rs 🔗

@@ -0,0 +1,25 @@
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{rgb, Element, Hsla, ParentElement};
+
+pub struct Story {}
+
+impl Story {
+    pub fn container<V: 'static>() -> div::Div<V> {
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .pt_2()
+            .px_4()
+            .font("Zed Mono Extended")
+            .fill(rgb::<Hsla>(0x282c34))
+    }
+
+    pub fn title<V: 'static>(title: &str) -> impl Element<V> {
+        div()
+            .text_2xl()
+            .text_color(rgb::<Hsla>(0xffffff))
+            .child(title.to_owned())
+    }
+}

crates/storybook/src/storybook.rs 🔗

@@ -2,6 +2,7 @@
 
 mod collab_panel;
 mod stories;
+mod story;
 mod workspace;
 
 use std::str::FromStr;
@@ -24,12 +25,12 @@ gpui2::actions! {
 }
 
 #[derive(Debug, Clone, Copy)]
-enum Story {
+enum StorySelector {
     Element(ElementStory),
     Component(ComponentStory),
 }
 
-impl FromStr for Story {
+impl FromStr for StorySelector {
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
@@ -55,7 +56,7 @@ enum ComponentStory {
 
 #[derive(Parser)]
 struct Args {
-    story: Option<Story>,
+    story: Option<StorySelector>,
 }
 
 fn main() {
@@ -79,13 +80,13 @@ fn main() {
                 ..Default::default()
             },
             |cx| match args.story {
-                Some(Story::Element(ElementStory::Avatar)) => {
+                Some(StorySelector::Element(ElementStory::Avatar)) => {
                     view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default()))
                 }
-                Some(Story::Component(ComponentStory::Facepile)) => {
+                Some(StorySelector::Component(ComponentStory::Facepile)) => {
                     view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default()))
                 }
-                Some(Story::Component(ComponentStory::TrafficLights)) => view(|cx| {
+                Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| {
                     render_story(&mut ViewContext::new(cx), TrafficLightsStory::default())
                 }),
                 None => {