component: Component crate cleanup (#29967)

This PR further organizes and documents the component crate. It:

- Simplifies the component registry
- Gives access to `ComponentMetadata` sooner
- Enables lookup by id in preview extension implementations
(`ComponentId` -> `ComponentMetadata`)
- Should slightly improve the performance of ComponentPreview

It also brings component statuses to the Component trait:

![CleanShot 2025-05-05 at 23 27
11@2x](https://github.com/user-attachments/assets/dd95ede6-bc90-4de4-90c6-3e5e064fd676)

![CleanShot 2025-05-05 at 23 27
40@2x](https://github.com/user-attachments/assets/9520aece-04c2-418b-95e1-c11aa60a66ca)

![CleanShot 2025-05-05 at 23 27
57@2x](https://github.com/user-attachments/assets/db1713d5-9831-4d00-9b29-1fd51c25fcba)

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2025-05-05 23:41:52 -04:00 committed by GitHub
parent 377909a646
commit c5d8407df4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 627 additions and 500 deletions

1
Cargo.lock generated
View file

@ -3237,6 +3237,7 @@ dependencies = [
"gpui",
"linkme",
"parking_lot",
"strum 0.27.1",
"theme",
"workspace-hack",
]

View file

@ -16,6 +16,7 @@ collections.workspace = true
gpui.workspace = true
linkme.workspace = true
parking_lot.workspace = true
strum.workspace = true
theme.workspace = true
workspace-hack.workspace = true

View file

@ -1,26 +1,197 @@
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
//! # Component
//!
//! This module provides the Component trait, which is used to define
//! components for visual testing and debugging.
//!
//! Additionally, it includes layouts for rendering component examples
//! and example groups, as well as the distributed slice mechanism for
//! registering components.
mod component_layout;
pub use component_layout::*;
use std::sync::LazyLock;
use collections::HashMap;
use gpui::{
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
prelude::*, px, rems,
};
use gpui::{AnyElement, App, SharedString, Window};
use linkme::distributed_slice;
use parking_lot::RwLock;
use theme::ActiveTheme;
use strum::{Display, EnumString};
pub fn components() -> ComponentRegistry {
COMPONENT_DATA.read().clone()
}
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
for f in component_fns {
f();
}
}
pub fn register_component<T: Component>() {
let id = T::id();
let metadata = ComponentMetadata {
id: id.clone(),
description: T::description().map(Into::into),
name: SharedString::new_static(T::name()),
preview: Some(T::preview),
scope: T::scope(),
sort_name: SharedString::new_static(T::sort_name()),
status: T::status(),
};
let mut data = COMPONENT_DATA.write();
data.components.insert(id, metadata);
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::default()));
#[derive(Default, Clone)]
pub struct ComponentRegistry {
components: HashMap<ComponentId, ComponentMetadata>,
}
impl ComponentRegistry {
pub fn previews(&self) -> Vec<&ComponentMetadata> {
self.components
.values()
.filter(|c| c.preview.is_some())
.collect()
}
pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> = self.previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
pub fn components(&self) -> Vec<&ComponentMetadata> {
self.components.values().collect()
}
pub fn sorted_components(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> =
self.components().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
components
}
pub fn component_map(&self) -> HashMap<ComponentId, ComponentMetadata> {
self.components.clone()
}
pub fn get(&self, id: &ComponentId) -> Option<&ComponentMetadata> {
self.components.get(id)
}
pub fn len(&self) -> usize {
self.components.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
id: ComponentId,
description: Option<SharedString>,
name: SharedString,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
scope: ComponentScope,
sort_name: SharedString,
status: ComponentStatus,
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
self.preview
}
pub fn scope(&self) -> ComponentScope {
self.scope.clone()
}
pub fn sort_name(&self) -> SharedString {
self.sort_name.clone()
}
pub fn scopeless_name(&self) -> SharedString {
self.name
.clone()
.split("::")
.last()
.unwrap_or(&self.name)
.to_string()
.into()
}
pub fn status(&self) -> ComponentStatus {
self.status.clone()
}
}
/// Implement this trait to define a UI component. This will allow you to
/// derive `RegisterComponent` on it, in tutn allowing you to preview the
/// contents of the preview fn in `workspace: open component preview`.
///
/// This can be useful for visual debugging and testing, documenting UI
/// patterns, or simply showing all the variants of a component.
///
/// Generally you will want to implement at least `scope` and `preview`
/// from this trait, so you can preview the component, and it will show up
/// in a section that makes sense.
pub trait Component {
/// The component's unique identifier.
///
/// Used to access previews, or state for more
/// complex, stateful components.
fn id() -> ComponentId {
ComponentId(Self::name())
}
/// Returns the scope of the component.
///
/// This scope is used to determine how components and
/// their previews are displayed and organized.
fn scope() -> ComponentScope {
ComponentScope::None
}
/// The ready status of this component.
///
/// Use this to mark when components are:
/// - `WorkInProgress`: Still being designed or are partially implemented.
/// - `EngineeringReady`: Ready to be implemented.
/// - `Deprecated`: No longer recommended for use.
///
/// Defaults to [`Live`](ComponentStatus::Live).
fn status() -> ComponentStatus {
ComponentStatus::Live
}
/// The name of the component.
///
/// This name is used to identify the component
/// and is usually derived from the component's type.
fn name() -> &'static str {
std::any::type_name::<Self>()
}
fn id() -> ComponentId {
ComponentId(Self::name())
}
/// Returns a name that the component should be sorted by.
///
/// Implement this if the component should be sorted in an alternate order than its name.
@ -37,408 +208,107 @@ pub trait Component {
fn sort_name() -> &'static str {
Self::name()
}
/// An optional description of the component.
///
/// This will be displayed in the component's preview. To show a
/// component's doc comment as it's description, derive `Documented`.
///
/// Example:
///
/// ```
/// /// This is a doc comment.
/// #[derive(Documented)]
/// struct MyComponent;
///
/// impl MyComponent {
/// fn description() -> Option<&'static str> {
/// Some(Self::DOCS)
/// }
/// }
/// ```
///
/// This will result in "This is a doc comment." being passed
/// to the component's description.
fn description() -> Option<&'static str> {
None
}
/// The component's preview.
///
/// An element returned here will be shown in the component's preview.
///
/// Useful component helpers:
/// - [`component::single_example`]
/// - [`component::component_group`]
/// - [`component::component_group_with_title`]
///
/// Note: Any arbitrary element can be returned here.
///
/// This is useful for displaying related UI to the component you are
/// trying to preview, such as a button that opens a modal or shows a
/// tooltip on hover, or a grid of icons showcasing all the icons available.
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
None
}
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
pub struct ComponentRegistry {
components: Vec<(
ComponentScope,
// name
&'static str,
// sort name
&'static str,
// description
Option<&'static str>,
)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
/// The ready status of this component.
///
/// Use this to mark when components are:
/// - `WorkInProgress`: Still being designed or are partially implemented.
/// - `EngineeringReady`: Ready to be implemented.
/// - `Deprecated`: No longer recommended for use.
///
/// Defaults to [`Live`](ComponentStatus::Live).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
pub enum ComponentStatus {
#[strum(serialize = "Work In Progress")]
WorkInProgress,
#[strum(serialize = "Ready To Build")]
EngineeringReady,
Live,
Deprecated,
}
impl ComponentRegistry {
fn new() -> Self {
ComponentRegistry {
components: Vec::new(),
previews: HashMap::default(),
impl ComponentStatus {
pub fn description(&self) -> &str {
match self {
ComponentStatus::WorkInProgress => {
"These components are still being designed or refined. They shouldn't be used in the app yet."
}
ComponentStatus::EngineeringReady => {
"These components are design complete or partially implemented, and are ready for an engineer to complete their implementation."
}
ComponentStatus::Live => "These components are ready for use in the app.",
ComponentStatus::Deprecated => {
"These components are no longer recommended for use in the app, and may be removed in a future release."
}
}
}
}
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
for f in component_fns {
f();
}
}
pub fn register_component<T: Component>() {
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
let mut data = COMPONENT_DATA.write();
data.components.push(component_data);
data.previews.insert(T::id().0, T::preview);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
id: ComponentId,
name: SharedString,
sort_name: SharedString,
scope: ComponentScope,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn sort_name(&self) -> SharedString {
self.sort_name.clone()
}
pub fn scopeless_name(&self) -> SharedString {
self.name
.clone()
.split("::")
.last()
.unwrap_or(&self.name)
.to_string()
.into()
}
pub fn scope(&self) -> ComponentScope {
self.scope.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
self.preview
}
}
pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
impl AllComponents {
pub fn new() -> Self {
AllComponents(HashMap::default())
}
pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
self.0.values().filter(|c| c.preview.is_some()).collect()
}
pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> =
self.all_previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
pub fn all(&self) -> Vec<&ComponentMetadata> {
self.0.values().collect()
}
pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
components
}
}
impl Deref for AllComponents {
type Target = HashMap<ComponentId, ComponentMetadata>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AllComponents {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub fn components() -> AllComponents {
let data = COMPONENT_DATA.read();
let mut all_components = AllComponents::new();
for (scope, name, sort_name, description) in &data.components {
let preview = data.previews.get(name).cloned();
let component_name = SharedString::new_static(name);
let sort_name = SharedString::new_static(sort_name);
let id = ComponentId(name);
all_components.insert(
id.clone(),
ComponentMetadata {
id,
name: component_name,
sort_name,
scope: scope.clone(),
description: description.map(Into::into),
preview,
},
);
}
all_components
}
// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
// pub enum ComponentStatus {
// WorkInProgress,
// EngineeringReady,
// Live,
// Deprecated,
// }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
pub enum ComponentScope {
Agent,
Collaboration,
#[strum(serialize = "Data Display")]
DataDisplay,
Editor,
#[strum(serialize = "Images & Icons")]
Images,
#[strum(serialize = "Forms & Input")]
Input,
#[strum(serialize = "Layout & Structure")]
Layout,
#[strum(serialize = "Loading & Progress")]
Loading,
Navigation,
#[strum(serialize = "Unsorted")]
None,
Notification,
#[strum(serialize = "Overlays & Layering")]
Overlays,
Status,
Typography,
#[strum(serialize = "Version Control")]
VersionControl,
}
impl Display for ComponentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentScope::Agent => write!(f, "Agent"),
ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::DataDisplay => write!(f, "Data Display"),
ComponentScope::Editor => write!(f, "Editor"),
ComponentScope::Images => write!(f, "Images & Icons"),
ComponentScope::Input => write!(f, "Forms & Input"),
ComponentScope::Layout => write!(f, "Layout & Structure"),
ComponentScope::Loading => write!(f, "Loading & Progress"),
ComponentScope::Navigation => write!(f, "Navigation"),
ComponentScope::None => write!(f, "Unsorted"),
ComponentScope::Notification => write!(f, "Notification"),
ComponentScope::Overlays => write!(f, "Overlays & Layering"),
ComponentScope::Status => write!(f, "Status"),
ComponentScope::Typography => write!(f, "Typography"),
ComponentScope::VersionControl => write!(f, "Version Control"),
}
}
}
/// A single example of a component.
#[derive(IntoElement)]
pub struct ComponentExample {
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
pub width: Option<Pixels>,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.pt_2()
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.flex_col()
.child(
div()
.child(self.variant_name.clone())
.text_size(rems(1.0))
.text_color(cx.theme().colors().text),
)
.when_some(self.description, |this, description| {
this.child(
div()
.text_size(rems(0.875))
.text_color(cx.theme().colors().text_muted)
.child(description.clone()),
)
}),
)
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.p_8()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
12.0,
12.0,
))
.shadow_sm()
.child(self.element),
)
.into_any_element()
}
}
impl ComponentExample {
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
Self {
variant_name: variant_name.into(),
element,
description: None,
width: None,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
}
/// A group of component examples.
#[derive(IntoElement)]
pub struct ComponentExampleGroup {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample>,
pub width: Option<Pixels>,
pub grow: bool,
pub vertical: bool,
}
impl RenderOnce for ComponentExampleGroup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
.flex()
.items_center()
.gap_3()
.pb_1()
.child(div().h_px().w_4().bg(cx.theme().colors().border))
.child(
div()
.flex_none()
.text_size(px(10.))
.child(title.to_uppercase()),
)
.child(
div()
.h_px()
.w_full()
.flex_1()
.bg(cx.theme().colors().border),
),
)
})
.child(
div()
.flex()
.flex_col()
.items_start()
.w_full()
.gap_6()
.children(self.examples)
.into_any_element(),
)
.into_any_element()
}
}
impl ComponentExampleGroup {
pub fn new(examples: Vec<ComponentExample>) -> Self {
Self {
title: None,
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
Self {
title: Some(title.into()),
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
pub fn single_example(
variant_name: impl Into<SharedString>,
example: AnyElement,
) -> ComponentExample {
ComponentExample::new(variant_name, example)
}
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
}
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,
) -> ComponentExampleGroup {
ComponentExampleGroup::with_title(title, examples)
}

View file

@ -0,0 +1,205 @@
use gpui::{
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
prelude::*, px, rems,
};
use theme::ActiveTheme;
/// A single example of a component.
#[derive(IntoElement)]
pub struct ComponentExample {
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
pub width: Option<Pixels>,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.pt_2()
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.flex_col()
.child(
div()
.child(self.variant_name.clone())
.text_size(rems(1.0))
.text_color(cx.theme().colors().text),
)
.when_some(self.description, |this, description| {
this.child(
div()
.text_size(rems(0.875))
.text_color(cx.theme().colors().text_muted)
.child(description.clone()),
)
}),
)
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.p_8()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
12.0,
12.0,
))
.shadow_sm()
.child(self.element),
)
.into_any_element()
}
}
impl ComponentExample {
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
Self {
variant_name: variant_name.into(),
element,
description: None,
width: None,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
}
/// A group of component examples.
#[derive(IntoElement)]
pub struct ComponentExampleGroup {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample>,
pub width: Option<Pixels>,
pub grow: bool,
pub vertical: bool,
}
impl RenderOnce for ComponentExampleGroup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.map(|this| {
if let Some(width) = self.width {
this.w(width)
} else {
this.w_full()
}
})
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
.flex()
.items_center()
.gap_3()
.pb_1()
.child(div().h_px().w_4().bg(cx.theme().colors().border))
.child(
div()
.flex_none()
.text_size(px(10.))
.child(title.to_uppercase()),
)
.child(
div()
.h_px()
.w_full()
.flex_1()
.bg(cx.theme().colors().border),
),
)
})
.child(
div()
.flex()
.flex_col()
.items_start()
.w_full()
.gap_6()
.children(self.examples)
.into_any_element(),
)
.into_any_element()
}
}
impl ComponentExampleGroup {
pub fn new(examples: Vec<ComponentExample>) -> Self {
Self {
title: None,
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
Self {
title: Some(title.into()),
examples,
width: None,
grow: false,
vertical: false,
}
}
pub fn width(mut self, width: Pixels) -> Self {
self.width = Some(width);
self
}
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
pub fn single_example(
variant_name: impl Into<SharedString>,
example: AnyElement,
) -> ComponentExample {
ComponentExample::new(variant_name, example)
}
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
}
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,
) -> ComponentExampleGroup {
ComponentExampleGroup::with_title(title, examples)
}

View file

@ -5,12 +5,13 @@
mod persistence;
mod preview_support;
use std::iter::Iterator;
use std::sync::Arc;
use std::iter::Iterator;
use agent::{ActiveThread, TextThreadStore, ThreadStore};
use client::UserStore;
use component::{ComponentId, ComponentMetadata, components};
use component::{ComponentId, ComponentMetadata, ComponentStatus, components};
use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
};
@ -25,7 +26,7 @@ use preview_support::active_thread::{
load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
};
use project::Project;
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use util::ResultExt as _;
use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
@ -104,26 +105,24 @@ enum PreviewPage {
}
struct ComponentPreview {
workspace_id: Option<WorkspaceId>,
focus_handle: FocusHandle,
_view_scroll_handle: ScrollHandle,
nav_scroll_handle: UniformListScrollHandle,
component_map: HashMap<ComponentId, ComponentMetadata>,
active_page: PreviewPage,
components: Vec<ComponentMetadata>,
active_thread: Option<Entity<ActiveThread>>,
component_list: ListState,
component_map: HashMap<ComponentId, ComponentMetadata>,
components: Vec<ComponentMetadata>,
cursor_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
user_store: Entity<UserStore>,
filter_editor: Entity<SingleLineInput>,
filter_text: String,
// preview support
thread_store: Option<Entity<ThreadStore>>,
focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>,
nav_scroll_handle: UniformListScrollHandle,
project: Entity<Project>,
text_thread_store: Option<Entity<TextThreadStore>>,
active_thread: Option<Entity<ActiveThread>>,
thread_store: Option<Entity<ThreadStore>>,
user_store: Entity<UserStore>,
workspace: WeakEntity<Workspace>,
workspace_id: Option<WorkspaceId>,
_view_scroll_handle: ScrollHandle,
}
impl ComponentPreview {
@ -164,7 +163,8 @@ impl ComponentPreview {
})
.detach();
let sorted_components = components().all_sorted();
let component_registry = Arc::new(components());
let sorted_components = component_registry.sorted_components();
let selected_index = selected_index.into().unwrap_or(0);
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
let filter_editor =
@ -188,24 +188,24 @@ impl ComponentPreview {
);
let mut component_preview = Self {
workspace_id: None,
focus_handle: cx.focus_handle(),
_view_scroll_handle: ScrollHandle::new(),
nav_scroll_handle: UniformListScrollHandle::new(),
language_registry,
user_store,
workspace,
project,
active_page,
component_map: components().0,
components: sorted_components,
active_thread: None,
component_list,
component_map: component_registry.component_map(),
components: sorted_components,
cursor_index: selected_index,
filter_editor,
filter_text: String::new(),
thread_store: None,
focus_handle: cx.focus_handle(),
language_registry,
nav_scroll_handle: UniformListScrollHandle::new(),
project,
text_thread_store: None,
active_thread: None,
thread_store: None,
user_store,
workspace,
workspace_id: None,
_view_scroll_handle: ScrollHandle::new(),
};
if component_preview.cursor_index > 0 {
@ -412,6 +412,88 @@ impl ComponentPreview {
entries
}
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
if new_len > 0 {
self.nav_scroll_handle
.scroll_to_item(0, ScrollStrategy::Top);
}
let filtered_components = self.filtered_components();
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
if let PreviewPage::Component(ref component_id) = self.active_page {
let component_still_visible = filtered_components
.iter()
.any(|component| component.id() == *component_id);
if !component_still_visible {
if !filtered_components.is_empty() {
let first_component = &filtered_components[0];
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
} else {
self.set_active_page(PreviewPage::AllComponents, cx);
}
}
}
}
self.component_list = ListState::new(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.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::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
fn render_sidebar_entry(
&self,
ix: usize,
@ -495,88 +577,6 @@ impl ComponentPreview {
}
}
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
if new_len > 0 {
self.nav_scroll_handle
.scroll_to_item(0, ScrollStrategy::Top);
}
let filtered_components = self.filtered_components();
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
if let PreviewPage::Component(ref component_id) = self.active_page {
let component_still_visible = filtered_components
.iter()
.any(|component| component.id() == *component_id);
if !component_still_visible {
if !filtered_components.is_empty() {
let first_component = &filtered_components[0];
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
} else {
self.set_active_page(PreviewPage::AllComponents, cx);
}
}
}
}
self.component_list = ListState::new(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.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::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
fn render_scope_header(
&self,
_ix: usize,
@ -695,7 +695,7 @@ impl ComponentPreview {
if let Some(component) = component {
v_flex()
.id("render-component-page")
.size_full()
.flex_1()
.child(ComponentPreviewPage::new(
component.clone(),
self.workspace.clone(),
@ -971,7 +971,7 @@ impl SerializableItem for ComponentPreview {
} else {
let component_str = deserialized_active_page.0;
let component_registry = components();
let all_components = component_registry.all();
let all_components = component_registry.components();
let found_component = all_components.iter().find(|c| c.id().0 == component_str);
if let Some(component) = found_component {
@ -1065,6 +1065,43 @@ impl ComponentPreviewPage {
}
}
/// Renders the component status when it would be useful
///
/// Doesn't render if the component is `ComponentStatus::Live`
/// as that is the default state
fn render_component_status(&self, cx: &App) -> Option<impl IntoElement> {
let status = self.component.status();
let status_description = status.description().to_string();
let color = match status {
ComponentStatus::Deprecated => Color::Error,
ComponentStatus::EngineeringReady => Color::Info,
ComponentStatus::Live => Color::Success,
ComponentStatus::WorkInProgress => Color::Warning,
};
if status != ComponentStatus::Live {
Some(
ButtonLike::new("component_status")
.child(
div()
.px_1p5()
.rounded_sm()
.bg(color.color(cx).alpha(0.12))
.child(
Label::new(status.clone().to_string())
.size(LabelSize::Small)
.color(color),
),
)
.tooltip(Tooltip::text(status_description))
.disabled(true),
)
} else {
None
}
}
fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
v_flex()
.px_12()
@ -1083,7 +1120,14 @@ impl ComponentPreviewPage {
.color(Color::Muted),
)
.child(
Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
h_flex()
.items_center()
.gap_2()
.child(
Headline::new(self.component.scopeless_name())
.size(HeadlineSize::XLarge),
)
.children(self.render_component_status(cx)),
),
)
.when_some(self.component.description(), |this, description| {

View file

@ -1,5 +1,6 @@
pub use component::{
Component, ComponentScope, example_group, example_group_with_title, single_example,
Component, ComponentId, ComponentScope, ComponentStatus, example_group,
example_group_with_title, single_example,
};
pub use documented::Documented;
pub use ui_macros::RegisterComponent;

View file

@ -1,3 +1,4 @@
use crate::component_prelude::*;
use crate::prelude::*;
use gpui::IntoElement;
use smallvec::{SmallVec, smallvec};
@ -81,6 +82,10 @@ impl Component for AlertModal {
ComponentScope::Notification
}
fn status() -> ComponentStatus {
ComponentStatus::WorkInProgress
}
fn description() -> Option<&'static str> {
Some("A modal dialog that presents an alert message with primary and dismiss actions.")
}