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