Cargo.lock 🔗
@@ -3047,6 +3047,7 @@ name = "component_preview"
version = "0.1.0"
dependencies = [
"client",
+ "collections",
"component",
"gpui",
"languages",
Nate Butler created
This PR adds pages to component preview when clicking on a given
component in the sidebar.
This will let us create richer previews & better docs for using
components in the future.
Release Notes:
- N/A
Cargo.lock | 1
crates/component/src/component.rs | 9
crates/component_preview/Cargo.toml | 1
crates/component_preview/src/component_preview.rs | 178 +++++++++++-----
4 files changed, 132 insertions(+), 57 deletions(-)
@@ -3047,6 +3047,7 @@ name = "component_preview"
version = "0.1.0"
dependencies = [
"client",
+ "collections",
"component",
"gpui",
"languages",
@@ -78,6 +78,7 @@ pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
+ id: ComponentId,
name: SharedString,
scope: Option<ComponentScope>,
description: Option<SharedString>,
@@ -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),
@@ -23,3 +23,4 @@ project.workspace = true
ui.workspace = true
workspace.workspace = true
notifications.workspace = true
+collections.workspace = true
@@ -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<AppState>, cx: &mut App) {
}
enum PreviewEntry {
+ AllComponents,
+ Separator,
Component(ComponentMetadata),
SectionHeader(SharedString),
}
@@ -75,13 +79,22 @@ impl From<SharedString> 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<ComponentId, ComponentMetadata>,
+ active_page: PreviewPage,
components: Vec<ComponentMetadata>,
component_list: ListState,
- selected_index: usize,
+ cursor_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
@@ -95,22 +108,25 @@ impl ComponentPreview {
selected_index: impl Into<Option<usize>>,
cx: &mut Context<Self>,
) -> 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>) {
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>) {
+ self.active_page = page;
cx.notify();
}
@@ -146,7 +169,6 @@ impl ComponentPreview {
fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
use std::collections::HashMap;
- // Group components by scope
let mut scope_groups: HashMap<Option<ComponentScope>, Vec<ComponentMetadata>> =
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<Self>,
) -> 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<Self>,
+ ) -> 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<Self>) {
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<Self>) -> 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(