gpui-component/crates/story/src/main.rs
Jason Lee 80689264e2
sheet: Rename Drawer to Sheet. (#1527)
## Break Change

- Renamed `Drawer` to `Sheet`, also renamed relative method contains
`drawer` to `sheet`.
- Renamed `ContextModal` to `WindowExt`.

```diff
- use gpui_component::drawer::Drawer
+ use gpui_component::sheet::Sheet

- use gpui_component::ContextModal
+ use gpui_component::WindowExt
```
2025-11-06 07:19:10 +00:00

309 lines
15 KiB
Rust

use gpui::{prelude::*, *};
use gpui_component::{
ActiveTheme as _, Icon, IconName, h_flex,
input::{Input, InputEvent, InputState},
resizable::{h_resizable, resizable_panel},
sidebar::{Sidebar, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem},
v_flex,
};
use story::*;
pub struct Gallery {
stories: Vec<(&'static str, Vec<Entity<StoryContainer>>)>,
active_group_index: Option<usize>,
active_index: Option<usize>,
collapsed: bool,
search_input: Entity<InputState>,
_subscriptions: Vec<Subscription>,
}
impl Gallery {
pub fn new(init_story: Option<&str>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let search_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
let _subscriptions = vec![cx.subscribe(&search_input, |this, _, e, cx| match e {
InputEvent::Change => {
this.active_group_index = Some(0);
this.active_index = Some(0);
cx.notify()
}
_ => {}
})];
let stories = vec![
(
"Getting Started",
vec![StoryContainer::panel::<WelcomeStory>(window, cx)],
),
(
"Components",
vec![
StoryContainer::panel::<AccordionStory>(window, cx),
StoryContainer::panel::<AlertStory>(window, cx),
StoryContainer::panel::<AvatarStory>(window, cx),
StoryContainer::panel::<BadgeStory>(window, cx),
StoryContainer::panel::<ButtonStory>(window, cx),
StoryContainer::panel::<CalendarStory>(window, cx),
StoryContainer::panel::<ChartStory>(window, cx),
StoryContainer::panel::<CheckboxStory>(window, cx),
StoryContainer::panel::<ClipboardStory>(window, cx),
StoryContainer::panel::<CollapsibleStory>(window, cx),
StoryContainer::panel::<ColorPickerStory>(window, cx),
StoryContainer::panel::<DatePickerStory>(window, cx),
StoryContainer::panel::<DescriptionListStory>(window, cx),
StoryContainer::panel::<FormStory>(window, cx),
StoryContainer::panel::<GroupBoxStory>(window, cx),
StoryContainer::panel::<IconStory>(window, cx),
StoryContainer::panel::<ImageStory>(window, cx),
StoryContainer::panel::<InputStory>(window, cx),
StoryContainer::panel::<KbdStory>(window, cx),
StoryContainer::panel::<LabelStory>(window, cx),
StoryContainer::panel::<ListStory>(window, cx),
StoryContainer::panel::<MenuStory>(window, cx),
StoryContainer::panel::<ModalStory>(window, cx),
StoryContainer::panel::<NotificationStory>(window, cx),
StoryContainer::panel::<NumberInputStory>(window, cx),
StoryContainer::panel::<OtpInputStory>(window, cx),
StoryContainer::panel::<PopoverStory>(window, cx),
StoryContainer::panel::<ProgressStory>(window, cx),
StoryContainer::panel::<RadioStory>(window, cx),
StoryContainer::panel::<ResizableStory>(window, cx),
StoryContainer::panel::<ScrollableStory>(window, cx),
StoryContainer::panel::<SelectStory>(window, cx),
StoryContainer::panel::<SheetStory>(window, cx),
StoryContainer::panel::<SidebarStory>(window, cx),
StoryContainer::panel::<SkeletonStory>(window, cx),
StoryContainer::panel::<SliderStory>(window, cx),
StoryContainer::panel::<SpinnerStory>(window, cx),
StoryContainer::panel::<SwitchStory>(window, cx),
StoryContainer::panel::<TableStory>(window, cx),
StoryContainer::panel::<TabsStory>(window, cx),
StoryContainer::panel::<TagStory>(window, cx),
StoryContainer::panel::<TextareaStory>(window, cx),
StoryContainer::panel::<ToggleStory>(window, cx),
StoryContainer::panel::<TooltipStory>(window, cx),
StoryContainer::panel::<TreeStory>(window, cx),
StoryContainer::panel::<VirtualListStory>(window, cx),
],
),
];
let mut this = Self {
search_input,
stories,
active_group_index: Some(0),
active_index: Some(0),
collapsed: false,
_subscriptions,
};
if let Some(init_story) = init_story {
this.set_active_story(init_story, window, cx);
}
this
}
fn set_active_story(&mut self, name: &str, window: &mut Window, cx: &mut App) {
let name = name.to_string();
self.search_input.update(cx, |this, cx| {
this.set_value(&name, window, cx);
})
}
fn view(init_story: Option<&str>, window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(init_story, window, cx))
}
}
impl Render for Gallery {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let query = self.search_input.read(cx).value().trim().to_lowercase();
let stories: Vec<_> = self
.stories
.iter()
.filter_map(|(name, items)| {
let filtered_items: Vec<_> = items
.iter()
.filter(|story| story.read(cx).name.to_lowercase().contains(&query))
.cloned()
.collect();
if !filtered_items.is_empty() {
Some((name, filtered_items))
} else {
None
}
})
.collect();
let active_group = self.active_group_index.and_then(|index| stories.get(index));
let active_story = self
.active_index
.and(active_group)
.and_then(|group| group.1.get(self.active_index.unwrap()));
let (story_name, description) =
if let Some(story) = active_story.as_ref().map(|story| story.read(cx)) {
(story.name.clone(), story.description.clone())
} else {
("".into(), "".into())
};
h_resizable("gallery-container")
.child(
resizable_panel()
.size(px(255.))
.size_range(px(200.)..px(320.))
.child(
Sidebar::left()
.width(relative(1.))
.border_width(px(0.))
.collapsed(self.collapsed)
.header(
v_flex()
.w_full()
.gap_4()
.child(
SidebarHeader::new()
.w_full()
.child(
div()
.flex()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(cx.theme().primary)
.text_color(cx.theme().primary_foreground)
.size_8()
.flex_shrink_0()
.when(!self.collapsed, |this| {
this.child(Icon::new(
IconName::GalleryVerticalEnd,
))
})
.when(self.collapsed, |this| {
this.size_4()
.bg(cx.theme().transparent)
.text_color(cx.theme().foreground)
.child(Icon::new(
IconName::GalleryVerticalEnd,
))
})
.rounded_lg(),
)
.when(!self.collapsed, |this| {
this.child(
v_flex()
.gap_0()
.text_sm()
.flex_1()
.line_height(relative(1.25))
.overflow_hidden()
.text_ellipsis()
.child("GPUI Component")
.child(
div()
.text_color(
cx.theme().muted_foreground,
)
.child("Gallery")
.text_xs(),
),
)
}),
)
.child(
div()
.bg(cx.theme().sidebar_accent)
.rounded_full()
.when(cx.theme().radius.is_zero(), |this| {
this.rounded(px(0.))
})
.flex_1()
.mx_1()
.child(
Input::new(&self.search_input)
.appearance(false)
.cleanable(true),
),
),
)
.children(stories.clone().into_iter().enumerate().map(
|(group_ix, (group_name, sub_stories))| {
SidebarGroup::new(*group_name).child(
SidebarMenu::new().children(
sub_stories.iter().enumerate().map(|(ix, story)| {
SidebarMenuItem::new(story.read(cx).name.clone())
.active(
self.active_group_index == Some(group_ix)
&& self.active_index == Some(ix),
)
.on_click(cx.listener(
move |this, _: &ClickEvent, _, cx| {
this.active_group_index =
Some(group_ix);
this.active_index = Some(ix);
cx.notify();
},
))
}),
),
)
},
)),
),
)
.child(
v_flex()
.flex_1()
.h_full()
.overflow_x_hidden()
.child(
h_flex()
.id("header")
.p_4()
.border_b_1()
.border_color(cx.theme().border)
.justify_between()
.items_start()
.child(
v_flex()
.gap_1()
.child(div().text_xl().child(story_name))
.child(
div()
.text_color(cx.theme().muted_foreground)
.child(description),
),
),
)
.child(
div()
.id("story")
.flex_1()
.overflow_y_scroll()
.when_some(active_story, |this, active_story| {
this.child(active_story.clone())
}),
)
.into_any_element(),
)
}
}
fn main() {
let app = Application::new().with_assets(Assets);
// Parse `cargo run -- <story_name>`
let name = std::env::args().nth(1);
app.run(move |cx| {
story::init(cx);
cx.activate(true);
story::create_new_window(
"GPUI Component",
move |window, cx| Gallery::view(name.as_deref(), window, cx),
cx,
);
});
}