1use std::str::FromStr;
2use std::sync::OnceLock;
3
4use anyhow::{anyhow, Context};
5use clap::builder::PossibleValue;
6use clap::ValueEnum;
7use gpui2::{AnyElement, Element};
8use strum::{EnumIter, EnumString, IntoEnumIterator};
9
10#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
11#[strum(serialize_all = "snake_case")]
12pub enum ElementStory {
13 Avatar,
14 Button,
15 Icon,
16 Input,
17 Label,
18}
19
20impl ElementStory {
21 pub fn story<V: 'static>(&self) -> AnyElement<V> {
22 use crate::stories::elements;
23
24 match self {
25 Self::Avatar => elements::avatar::AvatarStory::default().into_any(),
26 Self::Button => elements::button::ButtonStory::default().into_any(),
27 Self::Icon => elements::icon::IconStory::default().into_any(),
28 Self::Input => elements::input::InputStory::default().into_any(),
29 Self::Label => elements::label::LabelStory::default().into_any(),
30 }
31 }
32}
33
34#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
35#[strum(serialize_all = "snake_case")]
36pub enum ComponentStory {
37 AssistantPanel,
38 Breadcrumb,
39 Buffer,
40 ContextMenu,
41 ChatPanel,
42 CollabPanel,
43 Facepile,
44 Keybinding,
45 LanguageSelector,
46 MultiBuffer,
47 Palette,
48 Panel,
49 ProjectPanel,
50 RecentProjects,
51 StatusBar,
52 Tab,
53 TabBar,
54 Terminal,
55 ThemeSelector,
56 TitleBar,
57 Toolbar,
58 TrafficLights,
59}
60
61impl ComponentStory {
62 pub fn story<V: 'static>(&self) -> AnyElement<V> {
63 use crate::stories::components;
64
65 match self {
66 Self::AssistantPanel => {
67 components::assistant_panel::AssistantPanelStory::default().into_any()
68 }
69 Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(),
70 Self::Buffer => components::buffer::BufferStory::default().into_any(),
71 Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(),
72 Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(),
73 Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
74 Self::Facepile => components::facepile::FacepileStory::default().into_any(),
75 Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
76 Self::LanguageSelector => {
77 components::language_selector::LanguageSelectorStory::default().into_any()
78 }
79 Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(),
80 Self::Palette => components::palette::PaletteStory::default().into_any(),
81 Self::Panel => components::panel::PanelStory::default().into_any(),
82 Self::ProjectPanel => {
83 components::project_panel::ProjectPanelStory::default().into_any()
84 }
85 Self::RecentProjects => {
86 components::recent_projects::RecentProjectsStory::default().into_any()
87 }
88 Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
89 Self::Tab => components::tab::TabStory::default().into_any(),
90 Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
91 Self::Terminal => components::terminal::TerminalStory::default().into_any(),
92 Self::ThemeSelector => {
93 components::theme_selector::ThemeSelectorStory::default().into_any()
94 }
95 Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
96 Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
97 Self::TrafficLights => {
98 components::traffic_lights::TrafficLightsStory::default().into_any()
99 }
100 }
101 }
102}
103
104#[derive(Debug, PartialEq, Eq, Clone, Copy)]
105pub enum StorySelector {
106 Element(ElementStory),
107 Component(ComponentStory),
108 KitchenSink,
109}
110
111impl FromStr for StorySelector {
112 type Err = anyhow::Error;
113
114 fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
115 let story = raw_story_name.to_ascii_lowercase();
116
117 if story == "kitchen_sink" {
118 return Ok(Self::KitchenSink);
119 }
120
121 if let Some((_, story)) = story.split_once("elements/") {
122 let element_story = ElementStory::from_str(story)
123 .with_context(|| format!("story not found for element '{story}'"))?;
124
125 return Ok(Self::Element(element_story));
126 }
127
128 if let Some((_, story)) = story.split_once("components/") {
129 let component_story = ComponentStory::from_str(story)
130 .with_context(|| format!("story not found for component '{story}'"))?;
131
132 return Ok(Self::Component(component_story));
133 }
134
135 Err(anyhow!("story not found for '{raw_story_name}'"))
136 }
137}
138
139impl StorySelector {
140 pub fn story<V: 'static>(&self) -> AnyElement<V> {
141 match self {
142 Self::Element(element_story) => element_story.story(),
143 Self::Component(component_story) => component_story.story(),
144 Self::KitchenSink => {
145 crate::stories::kitchen_sink::KitchenSinkStory::default().into_any()
146 }
147 }
148 }
149}
150
151/// The list of all stories available in the storybook.
152static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
153
154impl ValueEnum for StorySelector {
155 fn value_variants<'a>() -> &'a [Self] {
156 let stories = ALL_STORY_SELECTORS.get_or_init(|| {
157 let element_stories = ElementStory::iter().map(StorySelector::Element);
158 let component_stories = ComponentStory::iter().map(StorySelector::Component);
159
160 element_stories
161 .chain(component_stories)
162 .chain(std::iter::once(StorySelector::KitchenSink))
163 .collect::<Vec<_>>()
164 });
165
166 stories
167 }
168
169 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
170 let value = match self {
171 Self::Element(story) => format!("elements/{story}"),
172 Self::Component(story) => format!("components/{story}"),
173 Self::KitchenSink => "kitchen_sink".to_string(),
174 };
175
176 Some(PossibleValue::new(value))
177 }
178}