mod accordion_story; mod alert_story; mod app_menus; mod avatar_story; mod badge_story; mod button_story; mod calendar_story; mod chart_story; mod checkbox_story; mod clipboard_story; mod collapsible_story; mod color_picker_story; mod date_picker_story; mod description_list_story; mod dialog_story; mod dropdown_button_story; mod form_story; mod group_box_story; mod icon_story; mod image_story; mod input_story; mod kbd_story; mod label_story; mod list_story; mod menu_story; mod notification_story; mod number_input_story; mod otp_input_story; mod popover_story; mod progress_story; mod radio_story; mod resizable_story; mod scrollable_story; mod select_story; mod settings_story; mod sheet_story; mod sidebar_story; mod skeleton_story; mod slider_story; mod spinner_story; mod switch_story; mod table_story; mod tabs_story; mod tag_story; mod textarea_story; mod themes; mod title_bar; mod toggle_story; mod tooltip_story; mod tree_story; mod virtual_list_story; mod webview_story; mod welcome_story; use gpui::{ Action, AnyElement, AnyView, App, AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, RenderOnce, SharedString, Size, StyleRefinement, Styled, Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::FluentBuilder as _, px, rems, size, }; pub use accordion_story::AccordionStory; pub use alert_story::AlertStory; pub use avatar_story::AvatarStory; pub use badge_story::BadgeStory; pub use button_story::ButtonStory; pub use calendar_story::CalendarStory; pub use chart_story::ChartStory; pub use checkbox_story::CheckboxStory; pub use clipboard_story::ClipboardStory; pub use collapsible_story::CollapsibleStory; pub use color_picker_story::ColorPickerStory; pub use date_picker_story::DatePickerStory; pub use description_list_story::DescriptionListStory; pub use dialog_story::DialogStory; pub use dropdown_button_story::DropdownButtonStory; pub use form_story::FormStory; pub use group_box_story::GroupBoxStory; pub use icon_story::IconStory; pub use image_story::ImageStory; pub use input_story::InputStory; pub use kbd_story::KbdStory; pub use label_story::LabelStory; pub use list_story::ListStory; pub use menu_story::MenuStory; pub use notification_story::NotificationStory; pub use number_input_story::NumberInputStory; pub use otp_input_story::OtpInputStory; pub use popover_story::PopoverStory; pub use progress_story::ProgressStory; pub use radio_story::RadioStory; pub use resizable_story::ResizableStory; pub use scrollable_story::ScrollableStory; pub use select_story::SelectStory; use serde::{Deserialize, Serialize}; pub use settings_story::SettingsStory; pub use sheet_story::SheetStory; pub use sidebar_story::SidebarStory; pub use skeleton_story::SkeletonStory; pub use slider_story::SliderStory; pub use spinner_story::SpinnerStory; pub use switch_story::SwitchStory; pub use table_story::TableStory; pub use tabs_story::TabsStory; pub use tag_story::TagStory; pub use textarea_story::TextareaStory; pub use title_bar::AppTitleBar; pub use toggle_story::ToggleStory; pub use tooltip_story::TooltipStory; pub use tree_story::TreeStory; pub use virtual_list_story::VirtualListStory; pub use webview_story::WebViewStory; pub use welcome_story::WelcomeStory; use gpui_component::{ ActiveTheme, IconName, Root, TitleBar, WindowExt, button::Button, dock::{Panel, PanelControl, PanelEvent, PanelInfo, PanelState, TitleStyle, register_panel}, group_box::{GroupBox, GroupBoxVariants as _}, h_flex, menu::PopupMenu, notification::Notification, scroll::{ScrollableElement as _, ScrollbarShow}, v_flex, }; use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectScrollbarShow(ScrollbarShow); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectLocale(SharedString); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectFont(usize); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectRadius(usize); actions!( story, [ About, Open, Quit, CloseWindow, ToggleSearch, TestAction, Tab, TabPrev, ShowPanelInfo ] ); const PANEL_NAME: &str = "StoryContainer"; pub struct AppState { pub invisible_panels: Entity>, } impl AppState { fn init(cx: &mut App) { let state = Self { invisible_panels: cx.new(|_| Vec::new()), }; cx.set_global::(state); } pub fn global(cx: &App) -> &Self { cx.global::() } pub fn global_mut(cx: &mut App) -> &mut Self { cx.global_mut::() } } pub fn create_new_window(title: &str, crate_view_fn: F, cx: &mut App) where E: Into, F: FnOnce(&mut Window, &mut App) -> E + Send + 'static, { create_new_window_with_size(title, None, crate_view_fn, cx); } pub fn create_new_window_with_size( title: &str, window_size: Option>, crate_view_fn: F, cx: &mut App, ) where E: Into, F: FnOnce(&mut Window, &mut App) -> E + Send + 'static, { let mut window_size = window_size.unwrap_or(size(px(1600.0), px(1200.0))); if let Some(display) = cx.primary_display() { let display_size = display.bounds().size; window_size.width = window_size.width.min(display_size.width * 0.85); window_size.height = window_size.height.min(display_size.height * 0.85); } let window_bounds = Bounds::centered(None, window_size, cx); let title = SharedString::from(title.to_string()); cx.spawn(async move |cx| { let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(window_bounds)), titlebar: Some(TitleBar::title_bar_options()), window_min_size: Some(gpui::Size { width: px(480.), height: px(320.), }), kind: WindowKind::Normal, #[cfg(target_os = "linux")] window_background: gpui::WindowBackgroundAppearance::Transparent, #[cfg(target_os = "linux")] window_decorations: Some(gpui::WindowDecorations::Client), ..Default::default() }; let window = cx .open_window(options, |window, cx| { let view = crate_view_fn(window, cx); let root = cx.new(|cx| StoryRoot::new(title.clone(), view, window, cx)); cx.new(|cx| Root::new(root, window, cx)) }) .expect("failed to open window"); window .update(cx, |_, window, _| { window.activate_window(); window.set_window_title(&title); }) .expect("failed to update window"); Ok::<_, anyhow::Error>(()) }) .detach(); } struct StoryRoot { title_bar: Entity, view: AnyView, } impl StoryRoot { pub fn new( title: impl Into, view: impl Into, window: &mut Window, cx: &mut Context, ) -> Self { let title_bar = cx.new(|cx| AppTitleBar::new(title, window, cx)); Self { title_bar, view: view.into(), } } } impl Render for StoryRoot { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); let dialog_layer = Root::render_dialog_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(self.title_bar.clone()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) .children(sheet_layer) .children(dialog_layer) .children(notification_layer) } } impl Global for AppState {} pub fn init(cx: &mut App) { tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) .with( tracing_subscriber::EnvFilter::from_default_env() .add_directive("gpui_component=trace".parse().unwrap()), ) .init(); gpui_component::init(cx); AppState::init(cx); themes::init(cx); input_story::init(cx); number_input_story::init(cx); textarea_story::init(cx); select_story::init(cx); popover_story::init(cx); menu_story::init(cx); webview_story::init(cx); tooltip_story::init(cx); otp_input_story::init(cx); tree_story::init(cx); let http_client = std::sync::Arc::new( reqwest_client::ReqwestClient::user_agent("gpui-component/story").unwrap(), ); cx.set_http_client(http_client); cx.bind_keys([ KeyBinding::new("/", ToggleSearch, None), #[cfg(target_os = "macos")] KeyBinding::new("cmd-o", Open, None), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-o", Open, None), #[cfg(target_os = "macos")] KeyBinding::new("cmd-q", Quit, None), #[cfg(not(target_os = "macos"))] KeyBinding::new("alt-f4", Quit, None), ]); cx.on_action(|_: &Quit, cx: &mut App| { cx.quit(); }); register_panel(cx, PANEL_NAME, |_, _, info, window, cx| { let story_state = match info { PanelInfo::Panel(value) => StoryState::from_value(value.clone()), _ => { unreachable!("Invalid PanelInfo: {:?}", info) } }; let view = cx.new(|cx| { let (title, description, closable, zoomable, story, on_active) = story_state.to_story(window, cx); let mut container = StoryContainer::new(window, cx) .story(story, story_state.story_klass) .on_active(on_active); cx.on_focus_in( &container.focus_handle, window, |this: &mut StoryContainer, _, _| { println!("StoryContainer focus in: {}", this.name); }, ) .detach(); container.name = title.into(); container.description = description.into(); container.closable = closable; container.zoomable = zoomable; container }); Box::new(view) }); cx.activate(true); } #[derive(IntoElement)] struct StorySection { base: Div, title: SharedString, sub_title: Vec, children: Vec, } impl StorySection { pub fn sub_title(mut self, sub_title: impl IntoElement) -> Self { self.sub_title.push(sub_title.into_any_element()); self } #[allow(unused)] fn max_w_md(mut self) -> Self { self.base = self.base.max_w(rems(48.)); self } #[allow(unused)] fn max_w_lg(mut self) -> Self { self.base = self.base.max_w(rems(64.)); self } #[allow(unused)] fn max_w_xl(mut self) -> Self { self.base = self.base.max_w(rems(80.)); self } #[allow(unused)] fn max_w_2xl(mut self) -> Self { self.base = self.base.max_w(rems(96.)); self } } impl ParentElement for StorySection { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl Styled for StorySection { fn style(&mut self) -> &mut gpui::StyleRefinement { self.base.style() } } impl RenderOnce for StorySection { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { GroupBox::new() .id(self.title.clone()) .outline() .title( h_flex() .justify_between() .w_full() .gap_4() .child(self.title) .children(self.sub_title), ) .content_style( StyleRefinement::default() .rounded(cx.theme().radius_lg) .overflow_x_hidden() .items_center() .justify_center(), ) .child(self.base.children(self.children)) } } pub(crate) fn section(title: impl Into) -> StorySection { StorySection { title: title.into(), sub_title: vec![], base: h_flex() .flex_wrap() .justify_center() .items_center() .w_full() .gap_4(), children: vec![], } } pub struct StoryContainer { focus_handle: gpui::FocusHandle, pub name: SharedString, pub title_bg: Option, pub description: SharedString, width: Option, height: Option, story: Option, story_klass: Option, closable: bool, zoomable: Option, paddings: Pixels, on_active: Option, } #[derive(Debug)] pub enum ContainerEvent { Close, } pub trait Story: Render + Sized { fn klass() -> &'static str { std::any::type_name::().split("::").last().unwrap() } fn title() -> &'static str; fn description() -> &'static str { "" } fn closable() -> bool { true } fn zoomable() -> Option { Some(PanelControl::default()) } fn title_bg() -> Option { None } fn paddings() -> Pixels { px(16.) } fn new_view(window: &mut Window, cx: &mut App) -> Entity; fn on_active(&mut self, active: bool, window: &mut Window, cx: &mut App) { let _ = active; let _ = window; let _ = cx; } fn on_active_any(view: AnyView, active: bool, window: &mut Window, cx: &mut App) where Self: 'static, { if let Some(story) = view.downcast::().ok() { cx.update_entity(&story, |story, cx| { story.on_active(active, window, cx); }); } } } impl EventEmitter for StoryContainer {} impl StoryContainer { pub fn new(_window: &mut Window, cx: &mut App) -> Self { let focus_handle = cx.focus_handle(); Self { focus_handle, name: "".into(), title_bg: None, description: "".into(), width: None, height: None, story: None, story_klass: None, closable: true, zoomable: Some(PanelControl::default()), paddings: px(16.), on_active: None, } } pub fn panel(window: &mut Window, cx: &mut App) -> Entity { let name = S::title(); let description = S::description(); let story = S::new_view(window, cx); let story_klass = S::klass(); let view = cx.new(|cx| { let mut story = Self::new(window, cx) .story(story.into(), story_klass) .on_active(S::on_active_any); story.focus_handle = cx.focus_handle(); story.closable = S::closable(); story.zoomable = S::zoomable(); story.name = name.into(); story.description = description.into(); story.title_bg = S::title_bg(); story.paddings = S::paddings(); story }); view } pub fn width(mut self, width: gpui::Pixels) -> Self { self.width = Some(width); self } pub fn height(mut self, height: gpui::Pixels) -> Self { self.height = Some(height); self } pub fn story(mut self, story: AnyView, story_klass: impl Into) -> Self { self.story = Some(story); self.story_klass = Some(story_klass.into()); self } pub fn on_active(mut self, on_active: fn(AnyView, bool, &mut Window, &mut App)) -> Self { self.on_active = Some(on_active); self } fn on_action_panel_info( &mut self, _: &ShowPanelInfo, window: &mut Window, cx: &mut Context, ) { struct Info; let note = Notification::new() .message(format!("You have clicked panel info on: {}", self.name)) .id::(); window.push_notification(note, cx); } fn on_action_toggle_search( &mut self, _: &ToggleSearch, window: &mut Window, cx: &mut Context, ) { cx.propagate(); if window.has_focused_input(cx) { return; } struct Search; let note = Notification::new() .message(format!("You have toggled search on: {}", self.name)) .id::(); window.push_notification(note, cx); } } #[derive(Debug, Serialize, Deserialize)] pub struct StoryState { pub story_klass: SharedString, } impl StoryState { fn to_value(&self) -> serde_json::Value { serde_json::json!({ "story_klass": self.story_klass, }) } fn from_value(value: serde_json::Value) -> Self { serde_json::from_value(value).unwrap() } fn to_story( &self, window: &mut Window, cx: &mut App, ) -> ( &'static str, &'static str, bool, Option, AnyView, fn(AnyView, bool, &mut Window, &mut App), ) { macro_rules! story { ($klass:tt) => { ( $klass::title(), $klass::description(), $klass::closable(), $klass::zoomable(), $klass::view(window, cx).into(), $klass::on_active_any, ) }; } match self.story_klass.to_string().as_str() { "ButtonStory" => story!(ButtonStory), "CalendarStory" => story!(CalendarStory), "SelectStory" => story!(SelectStory), "IconStory" => story!(IconStory), "ImageStory" => story!(ImageStory), "InputStory" => story!(InputStory), "ListStory" => story!(ListStory), "DialogStory" => story!(DialogStory), "PopoverStory" => story!(PopoverStory), "ProgressStory" => story!(ProgressStory), "ResizableStory" => story!(ResizableStory), "ScrollableStory" => story!(ScrollableStory), "SwitchStory" => story!(SwitchStory), "TableStory" => story!(TableStory), "LabelStory" => story!(LabelStory), "TooltipStory" => story!(TooltipStory), "WebViewStory" => story!(WebViewStory), "AccordionStory" => story!(AccordionStory), "SidebarStory" => story!(SidebarStory), "FormStory" => story!(FormStory), "NotificationStory" => story!(NotificationStory), _ => { unreachable!("Invalid story klass: {}", self.story_klass) } } } } impl Panel for StoryContainer { fn panel_name(&self) -> &'static str { "StoryContainer" } fn title(&self, _window: &Window, _cx: &App) -> AnyElement { self.name.clone().into_any_element() } fn title_style(&self, cx: &App) -> Option { if let Some(bg) = self.title_bg { Some(TitleStyle { background: bg, foreground: cx.theme().foreground, }) } else { None } } fn closable(&self, _cx: &App) -> bool { self.closable } fn zoomable(&self, _cx: &App) -> Option { self.zoomable } fn visible(&self, cx: &App) -> bool { !AppState::global(cx) .invisible_panels .read(cx) .contains(&self.name) } fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut App) { println!("panel: {} zoomed: {}", self.name, zoomed); } fn set_active(&mut self, active: bool, _window: &mut Window, cx: &mut App) { println!("panel: {} active: {}", self.name, active); if let Some(on_active) = self.on_active { if let Some(story) = self.story.clone() { on_active(story, active, _window, cx); } } } fn dropdown_menu(&self, menu: PopupMenu, _window: &Window, _cx: &App) -> PopupMenu { menu.menu("Info", Box::new(ShowPanelInfo)) } fn toolbar_buttons(&self, _window: &mut Window, _cx: &mut App) -> Option> { Some(vec![ Button::new("info") .icon(IconName::Info) .on_click(|_, window, cx| { window.push_notification("You have clicked info button", cx); }), Button::new("search") .icon(IconName::Search) .on_click(|_, window, cx| { window.push_notification("You have clicked search button", cx); }), ]) } fn dump(&self, _cx: &App) -> PanelState { let mut state = PanelState::new(self); let story_state = StoryState { story_klass: self.story_klass.clone().unwrap(), }; state.info = PanelInfo::panel(story_state.to_value()); state } } impl EventEmitter for StoryContainer {} impl Focusable for StoryContainer { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for StoryContainer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("story-container") .size_full() .overflow_y_scrollbar() .p(self.paddings) .track_focus(&self.focus_handle) .on_action(cx.listener(Self::on_action_panel_info)) .on_action(cx.listener(Self::on_action_toggle_search)) .when_some(self.story.clone(), |this, story| this.child(story)) } }