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::*;
11use ui::{AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory};
12
13#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
14#[strum(serialize_all = "snake_case")]
15pub enum ComponentStory {
16 AssistantPanel,
17 Avatar,
18 Breadcrumb,
19 Buffer,
20 Button,
21 ChatPanel,
22 Checkbox,
23 CollabPanel,
24 Colors,
25 CommandPalette,
26 ContextMenu,
27 Copilot,
28 Details,
29 Facepile,
30 Focus,
31 Icon,
32 Input,
33 Keybinding,
34 Label,
35 LanguageSelector,
36 MultiBuffer,
37 NotificationsPanel,
38 Palette,
39 Panel,
40 ProjectPanel,
41 Players,
42 RecentProjects,
43 Scroll,
44 Tab,
45 TabBar,
46 Terminal,
47 Text,
48 ThemeSelector,
49 TitleBar,
50 Toast,
51 Toolbar,
52 TrafficLights,
53 Workspace,
54 ZIndex,
55 Picker,
56}
57
58impl ComponentStory {
59 pub fn story(&self, cx: &mut WindowContext) -> AnyView {
60 match self {
61 Self::AssistantPanel => cx.build_view(|_| ui::AssistantPanelStory).into(),
62 Self::Avatar => cx.build_view(|_| AvatarStory).into(),
63 Self::Breadcrumb => cx.build_view(|_| ui::BreadcrumbStory).into(),
64 Self::Buffer => cx.build_view(|_| ui::BufferStory).into(),
65 Self::Button => cx.build_view(|_| ButtonStory).into(),
66 Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).into(),
67 Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
68 Self::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(),
69 Self::Colors => cx.build_view(|_| ColorsStory).into(),
70 Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(),
71 Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
72 Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(),
73 Self::Details => cx.build_view(|_| DetailsStory).into(),
74 Self::Facepile => cx.build_view(|_| ui::FacepileStory).into(),
75 Self::Focus => FocusStory::view(cx).into(),
76 Self::Icon => cx.build_view(|_| IconStory).into(),
77 Self::Input => cx.build_view(|_| InputStory).into(),
78 Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
79 Self::Label => cx.build_view(|_| LabelStory).into(),
80 Self::LanguageSelector => cx.build_view(|_| ui::LanguageSelectorStory).into(),
81 Self::MultiBuffer => cx.build_view(|_| ui::MultiBufferStory).into(),
82 Self::NotificationsPanel => cx.build_view(|cx| ui::NotificationsPanelStory).into(),
83 Self::Palette => cx.build_view(|cx| ui::PaletteStory).into(),
84 Self::Players => cx.build_view(|_| theme2::PlayerStory).into(),
85 Self::Panel => cx.build_view(|cx| ui::PanelStory).into(),
86 Self::ProjectPanel => cx.build_view(|_| ui::ProjectPanelStory).into(),
87 Self::RecentProjects => cx.build_view(|_| ui::RecentProjectsStory).into(),
88 Self::Scroll => ScrollStory::view(cx).into(),
89 Self::Tab => cx.build_view(|_| ui::TabStory).into(),
90 Self::TabBar => cx.build_view(|_| ui::TabBarStory).into(),
91 Self::Terminal => cx.build_view(|_| ui::TerminalStory).into(),
92 Self::Text => TextStory::view(cx).into(),
93 Self::ThemeSelector => cx.build_view(|_| ui::ThemeSelectorStory).into(),
94 Self::TitleBar => ui::TitleBarStory::view(cx).into(),
95 Self::Toast => cx.build_view(|_| ui::ToastStory).into(),
96 Self::Toolbar => cx.build_view(|_| ui::ToolbarStory).into(),
97 Self::TrafficLights => cx.build_view(|_| ui::TrafficLightsStory).into(),
98 Self::Workspace => ui::WorkspaceStory::view(cx).into(),
99 Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
100 Self::Picker => PickerStory::new(cx).into(),
101 }
102 }
103}
104
105#[derive(Debug, PartialEq, Eq, Clone, Copy)]
106pub enum StorySelector {
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 use anyhow::Context;
116
117 let story = raw_story_name.to_ascii_lowercase();
118
119 if story == "kitchen_sink" {
120 return Ok(Self::KitchenSink);
121 }
122
123 if let Some((_, story)) = story.split_once("components/") {
124 let component_story = ComponentStory::from_str(story)
125 .with_context(|| format!("story not found for component '{story}'"))?;
126
127 return Ok(Self::Component(component_story));
128 }
129
130 Err(anyhow!("story not found for '{raw_story_name}'"))
131 }
132}
133
134impl StorySelector {
135 pub fn story(&self, cx: &mut WindowContext) -> AnyView {
136 match self {
137 Self::Component(component_story) => component_story.story(cx),
138 Self::KitchenSink => KitchenSinkStory::view(cx).into(),
139 }
140 }
141}
142
143/// The list of all stories available in the storybook.
144static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
145
146impl ValueEnum for StorySelector {
147 fn value_variants<'a>() -> &'a [Self] {
148 let stories = ALL_STORY_SELECTORS.get_or_init(|| {
149 let component_stories = ComponentStory::iter().map(StorySelector::Component);
150
151 component_stories
152 .chain(std::iter::once(StorySelector::KitchenSink))
153 .collect::<Vec<_>>()
154 });
155
156 stories
157 }
158
159 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
160 let value = match self {
161 Self::Component(story) => format!("components/{story}"),
162 Self::KitchenSink => "kitchen_sink".to_string(),
163 };
164
165 Some(PossibleValue::new(value))
166 }
167}