component_preview: Add component pages (#26284)
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
This commit is contained in:
parent
3ff2c8fc38
commit
1b34437839
4 changed files with 132 additions and 57 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue