1use std::str::FromStr;
2use std::sync::OnceLock;
3
4use crate::stories::*;
5use anyhow::anyhow;
6use clap::builder::PossibleValue;
7use clap::ValueEnum;
8use gpui::{AnyView, VisualContext};
9use strum::{EnumIter, EnumString, IntoEnumIterator};
10use ui::prelude::*;
11
12#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
13#[strum(serialize_all = "snake_case")]
14pub enum ComponentStory {
15 ApplicationMenu,
16 AutoHeightEditor,
17 Avatar,
18 Button,
19 Checkbox,
20 CollabNotification,
21 ContextMenu,
22 Cursor,
23 DefaultColors,
24 Disclosure,
25 Focus,
26 Icon,
27 IconButton,
28 Keybinding,
29 Label,
30 List,
31 ListHeader,
32 ListItem,
33 OverflowScroll,
34 Picker,
35 Scroll,
36 Tab,
37 TabBar,
38 Text,
39 ToggleButton,
40 ToolStrip,
41 ViewportUnits,
42 WithRemSize,
43 Vector,
44}
45
46impl ComponentStory {
47 pub fn story(&self, cx: &mut WindowContext) -> AnyView {
48 match self {
49 Self::ApplicationMenu => cx
50 .new_view(|cx| title_bar::ApplicationMenuStory::new(cx))
51 .into(),
52 Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
53 Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
54 Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
55 Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(),
56 Self::CollabNotification => cx
57 .new_view(|_| collab_ui::notifications::CollabNotificationStory)
58 .into(),
59 Self::ContextMenu => cx.new_view(|_| ui::ContextMenuStory).into(),
60 Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(),
61 Self::DefaultColors => DefaultColorsStory::view(cx).into(),
62 Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(),
63 Self::Focus => FocusStory::view(cx).into(),
64 Self::Icon => cx.new_view(|_| ui::IconStory).into(),
65 Self::IconButton => cx.new_view(|_| ui::IconButtonStory).into(),
66 Self::Keybinding => cx.new_view(|_| ui::KeybindingStory).into(),
67 Self::Label => cx.new_view(|_| ui::LabelStory).into(),
68 Self::List => cx.new_view(|_| ui::ListStory).into(),
69 Self::ListHeader => cx.new_view(|_| ui::ListHeaderStory).into(),
70 Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
71 Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
72 Self::Picker => PickerStory::new(cx).into(),
73 Self::Scroll => ScrollStory::view(cx).into(),
74 Self::Tab => cx.new_view(|_| ui::TabStory).into(),
75 Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
76 Self::Text => TextStory::view(cx).into(),
77 Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(),
78 Self::ToolStrip => cx.new_view(|_| ui::ToolStripStory).into(),
79 Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(),
80 Self::WithRemSize => cx.new_view(|_| crate::stories::WithRemSizeStory).into(),
81 Self::Vector => cx.new_view(|_| ui::VectorStory).into(),
82 }
83 }
84}
85
86#[derive(Debug, PartialEq, Eq, Clone, Copy)]
87pub enum StorySelector {
88 Component(ComponentStory),
89 KitchenSink,
90}
91
92impl FromStr for StorySelector {
93 type Err = anyhow::Error;
94
95 fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
96 use anyhow::Context;
97
98 let story = raw_story_name.to_ascii_lowercase();
99
100 if story == "kitchen_sink" {
101 return Ok(Self::KitchenSink);
102 }
103
104 if let Some((_, story)) = story.split_once("components/") {
105 let component_story = ComponentStory::from_str(story)
106 .with_context(|| format!("story not found for component '{story}'"))?;
107
108 return Ok(Self::Component(component_story));
109 }
110
111 Err(anyhow!("story not found for '{raw_story_name}'"))
112 }
113}
114
115impl StorySelector {
116 pub fn story(&self, cx: &mut WindowContext) -> AnyView {
117 match self {
118 Self::Component(component_story) => component_story.story(cx),
119 Self::KitchenSink => KitchenSinkStory::view(cx).into(),
120 }
121 }
122}
123
124/// The list of all stories available in the storybook.
125static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
126
127impl ValueEnum for StorySelector {
128 fn value_variants<'a>() -> &'a [Self] {
129 let stories = ALL_STORY_SELECTORS.get_or_init(|| {
130 let component_stories = ComponentStory::iter().map(StorySelector::Component);
131
132 component_stories
133 .chain(std::iter::once(StorySelector::KitchenSink))
134 .collect::<Vec<_>>()
135 });
136
137 stories
138 }
139
140 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
141 let value = match self {
142 Self::Component(story) => format!("components/{story}"),
143 Self::KitchenSink => "kitchen_sink".to_string(),
144 };
145
146 Some(PossibleValue::new(value))
147 }
148}