Cargo.lock 🔗
@@ -3237,6 +3237,7 @@ dependencies = [
"gpui",
"linkme",
"parking_lot",
+ "strum 0.27.1",
"theme",
"workspace-hack",
]
Nate Butler created
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:



Release Notes:
- N/A
Cargo.lock | 1
crates/component/Cargo.toml | 1
crates/component/src/component.rs | 580 +++++--------
crates/component/src/component_layout.rs | 205 ++++
crates/component_preview/src/component_preview.rs | 274 +++--
crates/ui/src/component_prelude.rs | 3
crates/ui/src/components/notification/alert_modal.rs | 5
7 files changed, 598 insertions(+), 471 deletions(-)
@@ -3237,6 +3237,7 @@ dependencies = [
"gpui",
"linkme",
"parking_lot",
+ "strum 0.27.1",
"theme",
"workspace-hack",
]
@@ -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
@@ -1,90 +1,98 @@
-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 trait Component {
- fn scope() -> ComponentScope {
- ComponentScope::None
- }
- 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.
- ///
- /// Example:
- ///
- /// For example, to group related components together when sorted:
- ///
- /// - Button -> ButtonA
- /// - IconButton -> ButtonBIcon
- /// - ToggleButton -> ButtonCToggle
- ///
- /// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
- fn sort_name() -> &'static str {
- Self::name()
- }
- fn description() -> Option<&'static str> {
- None
- }
- fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
- None
+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::new()));
+ LazyLock::new(|| RwLock::new(ComponentRegistry::default()));
+#[derive(Default, Clone)]
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>>,
+ components: HashMap<ComponentId, ComponentMetadata>,
}
impl ComponentRegistry {
- fn new() -> Self {
- ComponentRegistry {
- components: Vec::new(),
- previews: HashMap::default(),
- }
+ pub fn previews(&self) -> Vec<&ComponentMetadata> {
+ self.components
+ .values()
+ .filter(|c| c.preview.is_some())
+ .collect()
}
-}
-pub fn init() {
- let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
- for f in component_fns {
- f();
+ 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 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);
+ 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)]
@@ -93,21 +101,35 @@ pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
id: ComponentId,
- name: SharedString,
- sort_name: SharedString,
- scope: ComponentScope,
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()
}
@@ -122,323 +144,171 @@ impl ComponentMetadata {
.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 fn status(&self) -> ComponentStatus {
+ self.status.clone()
}
}
-pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
-
-impl AllComponents {
- pub fn new() -> Self {
- AllComponents(HashMap::default())
+/// 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())
}
- pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
- self.0.values().filter(|c| c.preview.is_some()).collect()
+ /// 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
}
- 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
+ /// 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
}
- pub fn all(&self) -> Vec<&ComponentMetadata> {
- self.0.values().collect()
+ /// 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>()
}
- 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
+ /// 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.
+ ///
+ /// Example:
+ ///
+ /// For example, to group related components together when sorted:
+ ///
+ /// - Button -> ButtonA
+ /// - IconButton -> ButtonBIcon
+ /// - ToggleButton -> ButtonCToggle
+ ///
+ /// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
+ fn sort_name() -> &'static str {
+ Self::name()
}
-}
-
-impl Deref for AllComponents {
- type Target = HashMap<ComponentId, ComponentMetadata>;
- fn deref(&self) -> &Self::Target {
- &self.0
+ /// 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
}
}
-impl DerefMut for AllComponents {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.0
- }
+/// 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,
}
-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,
- },
- );
+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."
+ }
+ }
}
- 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)
-}
@@ -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)
+}
@@ -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| {
@@ -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;
@@ -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.")
}