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 Palette,
46 Panel,
47 ProjectPanel,
48 StatusBar,
49 Tab,
50 TabBar,
51 Terminal,
52 TitleBar,
53 Toolbar,
54 TrafficLights,
55}
56
57impl ComponentStory {
58 pub fn story<V: 'static>(&self) -> AnyElement<V> {
59 use crate::stories::components;
60
61 match self {
62 Self::AssistantPanel => {
63 components::assistant_panel::AssistantPanelStory::default().into_any()
64 }
65 Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(),
66 Self::Buffer => components::buffer::BufferStory::default().into_any(),
67 Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(),
68 Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(),
69 Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
70 Self::Facepile => components::facepile::FacepileStory::default().into_any(),
71 Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
72 Self::Palette => components::palette::PaletteStory::default().into_any(),
73 Self::Panel => components::panel::PanelStory::default().into_any(),
74 Self::ProjectPanel => {
75 components::project_panel::ProjectPanelStory::default().into_any()
76 }
77 Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
78 Self::Tab => components::tab::TabStory::default().into_any(),
79 Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
80 Self::Terminal => components::terminal::TerminalStory::default().into_any(),
81 Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
82 Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
83 Self::TrafficLights => {
84 components::traffic_lights::TrafficLightsStory::default().into_any()
85 }
86 }
87 }
88}
89
90#[derive(Debug, PartialEq, Eq, Clone, Copy)]
91pub enum StorySelector {
92 Element(ElementStory),
93 Component(ComponentStory),
94 KitchenSink,
95}
96
97impl FromStr for StorySelector {
98 type Err = anyhow::Error;
99
100 fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
101 let story = raw_story_name.to_ascii_lowercase();
102
103 if story == "kitchen_sink" {
104 return Ok(Self::KitchenSink);
105 }
106
107 if let Some((_, story)) = story.split_once("elements/") {
108 let element_story = ElementStory::from_str(story)
109 .with_context(|| format!("story not found for element '{story}'"))?;
110
111 return Ok(Self::Element(element_story));
112 }
113
114 if let Some((_, story)) = story.split_once("components/") {
115 let component_story = ComponentStory::from_str(story)
116 .with_context(|| format!("story not found for component '{story}'"))?;
117
118 return Ok(Self::Component(component_story));
119 }
120
121 Err(anyhow!("story not found for '{raw_story_name}'"))
122 }
123}
124
125impl StorySelector {
126 pub fn story<V: 'static>(&self) -> AnyElement<V> {
127 match self {
128 Self::Element(element_story) => element_story.story(),
129 Self::Component(component_story) => component_story.story(),
130 Self::KitchenSink => {
131 crate::stories::kitchen_sink::KitchenSinkStory::default().into_any()
132 }
133 }
134 }
135}
136
137/// The list of all stories available in the storybook.
138static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
139
140impl ValueEnum for StorySelector {
141 fn value_variants<'a>() -> &'a [Self] {
142 let stories = ALL_STORY_SELECTORS.get_or_init(|| {
143 let element_stories = ElementStory::iter().map(StorySelector::Element);
144 let component_stories = ComponentStory::iter().map(StorySelector::Component);
145
146 element_stories
147 .chain(component_stories)
148 .chain(std::iter::once(StorySelector::KitchenSink))
149 .collect::<Vec<_>>()
150 });
151
152 stories
153 }
154
155 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
156 let value = match self {
157 Self::Element(story) => format!("elements/{story}"),
158 Self::Component(story) => format!("components/{story}"),
159 Self::KitchenSink => "kitchen_sink".to_string(),
160 };
161
162 Some(PossibleValue::new(value))
163 }
164}