diff --git a/Cargo.lock b/Cargo.lock index 4e9b781ed2..1776c4d6d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3047,6 +3047,7 @@ name = "component_preview" version = "0.1.0" dependencies = [ "client", + "collections", "component", "gpui", "languages", diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs index 7360123925..1dfcbb8fb3 100644 --- a/crates/component/src/component.rs +++ b/crates/component/src/component.rs @@ -78,6 +78,7 @@ pub struct ComponentId(pub &'static str); #[derive(Clone)] pub struct ComponentMetadata { + id: ComponentId, name: SharedString, scope: Option, description: Option, @@ -85,6 +86,10 @@ pub struct ComponentMetadata { } impl ComponentMetadata { + pub fn id(&self) -> ComponentId { + self.id.clone() + } + pub fn name(&self) -> SharedString { self.name.clone() } @@ -156,9 +161,11 @@ pub fn components() -> AllComponents { for (ref scope, name, description) in &data.components { let preview = data.previews.get(name).cloned(); let component_name = SharedString::new_static(name); + let id = ComponentId(name); all_components.insert( - ComponentId(name), + id.clone(), ComponentMetadata { + id, name: component_name, scope: scope.clone(), description: description.map(Into::into), diff --git a/crates/component_preview/Cargo.toml b/crates/component_preview/Cargo.toml index b8d52f9370..1242bde2e4 100644 --- a/crates/component_preview/Cargo.toml +++ b/crates/component_preview/Cargo.toml @@ -23,3 +23,4 @@ project.workspace = true ui.workspace = true workspace.workspace = true notifications.workspace = true +collections.workspace = true diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs index bf795c3547..839234724d 100644 --- a/crates/component_preview/src/component_preview.rs +++ b/crates/component_preview/src/component_preview.rs @@ -6,12 +6,14 @@ use std::iter::Iterator; use std::sync::Arc; use client::UserStore; -use component::{components, ComponentMetadata}; +use component::{components, ComponentId, ComponentMetadata}; use gpui::{ list, prelude::*, uniform_list, App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, }; +use collections::HashMap; + use gpui::{ListState, ScrollHandle, UniformListScrollHandle}; use languages::LanguageRegistry; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -59,6 +61,8 @@ pub fn init(app_state: Arc, cx: &mut App) { } enum PreviewEntry { + AllComponents, + Separator, Component(ComponentMetadata), SectionHeader(SharedString), } @@ -75,13 +79,22 @@ impl From for PreviewEntry { } } +#[derive(Default, Debug, Clone, PartialEq, Eq)] +enum PreviewPage { + #[default] + AllComponents, + Component(ComponentId), +} + struct ComponentPreview { focus_handle: FocusHandle, _view_scroll_handle: ScrollHandle, nav_scroll_handle: UniformListScrollHandle, + component_map: HashMap, + active_page: PreviewPage, components: Vec, component_list: ListState, - selected_index: usize, + cursor_index: usize, language_registry: Arc, workspace: WeakEntity, user_store: Entity, @@ -95,22 +108,25 @@ impl ComponentPreview { selected_index: impl Into>, cx: &mut Context, ) -> Self { - let components = components().all_sorted(); - let initial_length = components.len(); + let sorted_components = components().all_sorted(); let selected_index = selected_index.into().unwrap_or(0); - let component_list = - ListState::new(initial_length, gpui::ListAlignment::Top, px(1500.0), { + let component_list = ListState::new( + sorted_components.len(), + gpui::ListAlignment::Top, + px(1500.0), + { let this = cx.entity().downgrade(); move |ix, window: &mut Window, cx: &mut App| { this.update(cx, |this, cx| { let component = this.get_component(ix); - this.render_preview(ix, &component, window, cx) + this.render_preview(&component, window, cx) .into_any_element() }) .unwrap() } - }); + }, + ); let mut component_preview = Self { focus_handle: cx.focus_handle(), @@ -119,13 +135,15 @@ impl ComponentPreview { language_registry, user_store, workspace, - components, + active_page: PreviewPage::AllComponents, + component_map: components().0, + components: sorted_components, component_list, - selected_index, + cursor_index: selected_index, }; - if component_preview.selected_index > 0 { - component_preview.scroll_to_preview(component_preview.selected_index, cx); + if component_preview.cursor_index > 0 { + component_preview.scroll_to_preview(component_preview.cursor_index, cx); } component_preview.update_component_list(cx); @@ -135,7 +153,12 @@ impl ComponentPreview { fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context) { self.component_list.scroll_to_reveal_item(ix); - self.selected_index = ix; + self.cursor_index = ix; + cx.notify(); + } + + fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context) { + self.active_page = page; cx.notify(); } @@ -146,7 +169,6 @@ impl ComponentPreview { fn scope_ordered_entries(&self) -> Vec { use std::collections::HashMap; - // Group components by scope let mut scope_groups: HashMap, Vec> = HashMap::default(); @@ -157,15 +179,12 @@ impl ComponentPreview { .push(component.clone()); } - // Sort components within each scope by name for components in scope_groups.values_mut() { components.sort_by_key(|c| c.name().to_lowercase()); } - // Build entries with scopes in a defined order let mut entries = Vec::new(); - // Define scope order (we want Unknown at the end) let known_scopes = [ ComponentScope::Layout, ComponentScope::Input, @@ -175,15 +194,16 @@ impl ComponentPreview { ComponentScope::VersionControl, ]; - // First add components with known scopes + // Always show all components first + entries.push(PreviewEntry::AllComponents); + entries.push(PreviewEntry::Separator); + for scope in known_scopes.iter() { let scope_key = Some(scope.clone()); if let Some(components) = scope_groups.remove(&scope_key) { if !components.is_empty() { - // Add section header entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); - // Add all components under this scope for component in components { entries.push(PreviewEntry::Component(component)); } @@ -191,16 +211,13 @@ impl ComponentPreview { } } - // Handle components with Unknown scope for (scope, components) in &scope_groups { if let Some(ComponentScope::Unknown(_)) = scope { if !components.is_empty() { - // Add the unknown scope header if let Some(scope_value) = scope { entries.push(PreviewEntry::SectionHeader(scope_value.to_string().into())); } - // Add all components under this unknown scope for component in components { entries.push(PreviewEntry::Component(component.clone())); } @@ -208,9 +225,9 @@ impl ComponentPreview { } } - // Handle components with no scope if let Some(components) = scope_groups.get(&None) { if !components.is_empty() { + entries.push(PreviewEntry::Separator); entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); for component in components { @@ -226,22 +243,42 @@ impl ComponentPreview { &self, ix: usize, entry: &PreviewEntry, - selected: bool, cx: &Context, ) -> impl IntoElement { match entry { - PreviewEntry::Component(component_metadata) => ListItem::new(ix) - .child(Label::new(component_metadata.name().clone()).color(Color::Default)) - .selectable(true) - .toggle_state(selected) - .inset(true) - .on_click(cx.listener(move |this, _, _, cx| { - this.scroll_to_preview(ix, cx); - })) - .into_any_element(), + PreviewEntry::Component(component_metadata) => { + let id = component_metadata.id(); + let selected = self.active_page == PreviewPage::Component(id.clone()); + ListItem::new(ix) + .child(Label::new(component_metadata.name().clone()).color(Color::Default)) + .selectable(true) + .toggle_state(selected) + .inset(true) + .on_click(cx.listener(move |this, _, _, cx| { + let id = id.clone(); + this.set_active_page(PreviewPage::Component(id), cx); + })) + .into_any_element() + } PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string) .inset(true) .into_any_element(), + PreviewEntry::AllComponents => { + let selected = self.active_page == PreviewPage::AllComponents; + + ListItem::new(ix) + .child(Label::new("All Components").color(Color::Default)) + .selectable(true) + .toggle_state(selected) + .inset(true) + .on_click(cx.listener(move |this, _, _, cx| { + this.set_active_page(PreviewPage::AllComponents, cx); + })) + .into_any_element() + } + PreviewEntry::Separator => ListItem::new(ix) + .child(h_flex().pt_3().child(Divider::horizontal_dashed())) + .into_any_element(), } } @@ -260,11 +297,13 @@ impl ComponentPreview { weak_entity .update(cx, |this, cx| match entry { PreviewEntry::Component(component) => this - .render_preview(ix, component, window, cx) + .render_preview(component, window, cx) .into_any_element(), PreviewEntry::SectionHeader(shared_string) => this .render_scope_header(ix, shared_string.clone(), window, cx) .into_any_element(), + PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(), + PreviewEntry::Separator => div().w_full().h_0().into_any_element(), }) .unwrap() }, @@ -290,7 +329,6 @@ impl ComponentPreview { fn render_preview( &self, - _ix: usize, component: &ComponentMetadata, window: &mut Window, cx: &mut App, @@ -341,6 +379,44 @@ impl ComponentPreview { .into_any_element() } + fn render_all_components(&self) -> impl IntoElement { + v_flex() + .id("component-list") + .px_8() + .pt_4() + .size_full() + .child( + list(self.component_list.clone()) + .flex_grow() + .with_sizing_behavior(gpui::ListSizingBehavior::Auto), + ) + } + + fn render_component_page( + &mut self, + component_id: &ComponentId, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let component = self.component_map.get(&component_id); + + if let Some(component) = component { + v_flex() + .w_full() + .flex_initial() + .min_h_full() + .child(self.render_preview(component, window, cx)) + .into_any_element() + } else { + v_flex() + .size_full() + .items_center() + .justify_center() + .child("Component not found") + .into_any_element() + } + } + fn test_status_toast(&self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { @@ -363,8 +439,9 @@ impl ComponentPreview { } impl Render for ComponentPreview { - fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sidebar_entries = self.scope_ordered_entries(); + let active_page = self.active_page.clone(); h_flex() .id("component-preview") @@ -386,12 +463,7 @@ impl Render for ComponentPreview { move |this, range, _window, cx| { range .map(|ix| { - this.render_sidebar_entry( - ix, - &sidebar_entries[ix], - ix == this.selected_index, - cx, - ) + this.render_sidebar_entry(ix, &sidebar_entries[ix], cx) }) .collect() }, @@ -415,18 +487,12 @@ impl Render for ComponentPreview { ), ), ) - .child( - v_flex() - .id("component-list") - .px_8() - .pt_4() - .size_full() - .child( - list(self.component_list.clone()) - .flex_grow() - .with_sizing_behavior(gpui::ListSizingBehavior::Auto), - ), - ) + .child(match active_page { + PreviewPage::AllComponents => self.render_all_components().into_any_element(), + PreviewPage::Component(id) => self + .render_component_page(&id, window, cx) + .into_any_element(), + }) } } @@ -465,7 +531,7 @@ impl Item for ComponentPreview { let language_registry = self.language_registry.clone(); let user_store = self.user_store.clone(); let weak_workspace = self.workspace.clone(); - let selected_index = self.selected_index; + let selected_index = self.cursor_index; Some(cx.new(|cx| { Self::new(