1use std::str::FromStr;
2use std::sync::OnceLock;
3
4use crate::stories::*;
5use clap::ValueEnum;
6use clap::builder::PossibleValue;
7use gpui::AnyView;
8use strum::{EnumIter, EnumString, IntoEnumIterator};
9use ui::prelude::*;
10
11#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
12#[strum(serialize_all = "snake_case")]
13pub enum ComponentStory {
14 ApplicationMenu,
15 AutoHeightEditor,
16 CollabNotification,
17 ContextMenu,
18 Cursor,
19 Focus,
20 OverflowScroll,
21 Picker,
22 Scroll,
23 Text,
24 ViewportUnits,
25 WithRemSize,
26 IndentGuides,
27}
28
29impl ComponentStory {
30 pub fn story(&self, window: &mut Window, cx: &mut App) -> AnyView {
31 match self {
32 Self::ApplicationMenu => cx
33 .new(|cx| title_bar::ApplicationMenuStory::new(window, cx))
34 .into(),
35 Self::AutoHeightEditor => AutoHeightEditorStory::new(window, cx).into(),
36 Self::CollabNotification => cx
37 .new(|_| collab_ui::notifications::CollabNotificationStory)
38 .into(),
39 Self::ContextMenu => cx.new(|_| ui::ContextMenuStory).into(),
40 Self::Cursor => cx.new(|_| crate::stories::CursorStory).into(),
41 Self::Focus => FocusStory::model(window, cx).into(),
42 Self::OverflowScroll => cx.new(|_| crate::stories::OverflowScrollStory).into(),
43 Self::Picker => PickerStory::new(window, cx).into(),
44 Self::Scroll => ScrollStory::model(cx).into(),
45 Self::Text => TextStory::model(cx).into(),
46 Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(),
47 Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(),
48 Self::IndentGuides => crate::stories::IndentGuidesStory::model(window, cx).into(),
49 }
50 }
51}
52
53#[derive(Debug, PartialEq, Eq, Clone, Copy)]
54pub enum StorySelector {
55 Component(ComponentStory),
56 KitchenSink,
57}
58
59impl FromStr for StorySelector {
60 type Err = anyhow::Error;
61
62 fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
63 use anyhow::Context as _;
64
65 let story = raw_story_name.to_ascii_lowercase();
66
67 if story == "kitchen_sink" {
68 return Ok(Self::KitchenSink);
69 }
70
71 if let Some((_, story)) = story.split_once("components/") {
72 let component_story = ComponentStory::from_str(story)
73 .with_context(|| format!("story not found for component '{story}'"))?;
74
75 return Ok(Self::Component(component_story));
76 }
77
78 anyhow::bail!("story not found for '{raw_story_name}'")
79 }
80}
81
82impl StorySelector {
83 pub fn story(&self, window: &mut Window, cx: &mut App) -> AnyView {
84 match self {
85 Self::Component(component_story) => component_story.story(window, cx),
86 Self::KitchenSink => KitchenSinkStory::model(cx).into(),
87 }
88 }
89}
90
91/// The list of all stories available in the storybook.
92static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
93
94impl ValueEnum for StorySelector {
95 fn value_variants<'a>() -> &'a [Self] {
96 (ALL_STORY_SELECTORS.get_or_init(|| {
97 let component_stories = ComponentStory::iter().map(StorySelector::Component);
98
99 component_stories
100 .chain(std::iter::once(StorySelector::KitchenSink))
101 .collect::<Vec<_>>()
102 })) as _
103 }
104
105 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
106 let value = match self {
107 Self::Component(story) => format!("components/{story}"),
108 Self::KitchenSink => "kitchen_sink".to_string(),
109 };
110
111 Some(PossibleValue::new(value))
112 }
113}