diff --git a/Cargo.lock b/Cargo.lock index 37a34317de..f9f9e76cc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3237,6 +3237,7 @@ dependencies = [ "gpui", "linkme", "parking_lot", + "strum 0.27.1", "theme", "workspace-hack", ] diff --git a/crates/component/Cargo.toml b/crates/component/Cargo.toml index 547251429e..9591773fcb 100644 --- a/crates/component/Cargo.toml +++ b/crates/component/Cargo.toml @@ -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 diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs index 83e09d8f57..ebab5d2cde 100644 --- a/crates/component/src/component.rs +++ b/crates/component/src/component.rs @@ -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() { + 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> = + LazyLock::new(|| RwLock::new(ComponentRegistry::default())); + +#[derive(Default, Clone)] +pub struct ComponentRegistry { + components: HashMap, +} + +impl ComponentRegistry { + pub fn previews(&self) -> Vec<&ComponentMetadata> { + self.components + .values() + .filter(|c| c.preview.is_some()) + .collect() + } + + pub fn sorted_previews(&self) -> Vec { + let mut previews: Vec = 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 { + let mut components: Vec = + self.components().into_iter().cloned().collect(); + components.sort_by_key(|a| a.name()); + components + } + + pub fn component_map(&self) -> HashMap { + 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, + name: SharedString, + preview: Option Option>, + scope: ComponentScope, + sort_name: SharedString, + status: ComponentStatus, +} + +impl ComponentMetadata { + pub fn id(&self) -> ComponentId { + self.id.clone() + } + + pub fn description(&self) -> Option { + self.description.clone() + } + + pub fn name(&self) -> SharedString { + self.name.clone() + } + + pub fn preview(&self) -> Option Option> { + 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::() } - 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 { None } } -#[distributed_slice] -pub static __ALL_COMPONENTS: [fn()] = [..]; - -pub static COMPONENT_DATA: LazyLock> = - 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>, +/// 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() { - 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, - preview: Option Option>, -} - -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 { - self.description.clone() - } - pub fn preview(&self) -> Option Option> { - self.preview - } -} - -pub struct AllComponents(pub HashMap); - -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 { - let mut previews: Vec = - 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 { - let mut components: Vec = self.all().into_iter().cloned().collect(); - components.sort_by_key(|a| a.name()); - components - } -} - -impl Deref for AllComponents { - type Target = HashMap; - 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, - pub element: AnyElement, - pub width: Option, -} - -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, element: AnyElement) -> Self { - Self { - variant_name: variant_name.into(), - element, - description: None, - width: None, - } - } - - pub fn description(mut self, description: impl Into) -> 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, - pub examples: Vec, - pub width: Option, - 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) -> Self { - Self { - title: None, - examples, - width: None, - grow: false, - vertical: false, - } - } - pub fn with_title(title: impl Into, examples: Vec) -> 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, - example: AnyElement, -) -> ComponentExample { - ComponentExample::new(variant_name, example) -} - -pub fn empty_example(variant_name: impl Into) -> 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) -> ComponentExampleGroup { - ComponentExampleGroup::new(examples) -} - -pub fn example_group_with_title( - title: impl Into, - examples: Vec, -) -> ComponentExampleGroup { - ComponentExampleGroup::with_title(title, examples) -} diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs new file mode 100644 index 0000000000..9090c49cf9 --- /dev/null +++ b/crates/component/src/component_layout.rs @@ -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, + pub element: AnyElement, + pub width: Option, +} + +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, element: AnyElement) -> Self { + Self { + variant_name: variant_name.into(), + element, + description: None, + width: None, + } + } + + pub fn description(mut self, description: impl Into) -> 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, + pub examples: Vec, + pub width: Option, + 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) -> Self { + Self { + title: None, + examples, + width: None, + grow: false, + vertical: false, + } + } + pub fn with_title(title: impl Into, examples: Vec) -> 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, + example: AnyElement, +) -> ComponentExample { + ComponentExample::new(variant_name, example) +} + +pub fn empty_example(variant_name: impl Into) -> 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) -> ComponentExampleGroup { + ComponentExampleGroup::new(examples) +} + +pub fn example_group_with_title( + title: impl Into, + examples: Vec, +) -> ComponentExampleGroup { + ComponentExampleGroup::with_title(title, examples) +} diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs index a87a517815..e0912d5b99 100644 --- a/crates/component_preview/src/component_preview.rs +++ b/crates/component_preview/src/component_preview.rs @@ -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, - focus_handle: FocusHandle, - _view_scroll_handle: ScrollHandle, - nav_scroll_handle: UniformListScrollHandle, - component_map: HashMap, active_page: PreviewPage, - components: Vec, + active_thread: Option>, component_list: ListState, + component_map: HashMap, + components: Vec, cursor_index: usize, - language_registry: Arc, - workspace: WeakEntity, - project: Entity, - user_store: Entity, filter_editor: Entity, filter_text: String, - - // preview support - thread_store: Option>, + focus_handle: FocusHandle, + language_registry: Arc, + nav_scroll_handle: UniformListScrollHandle, + project: Entity, text_thread_store: Option>, - active_thread: Option>, + thread_store: Option>, + user_store: Entity, + workspace: WeakEntity, + workspace_id: Option, + _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) { + 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) { - 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 { + 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| { diff --git a/crates/ui/src/component_prelude.rs b/crates/ui/src/component_prelude.rs index 3a0a31b290..0a01372970 100644 --- a/crates/ui/src/component_prelude.rs +++ b/crates/ui/src/component_prelude.rs @@ -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; diff --git a/crates/ui/src/components/notification/alert_modal.rs b/crates/ui/src/components/notification/alert_modal.rs index 25fd64135c..acba0c7e9a 100644 --- a/crates/ui/src/components/notification/alert_modal.rs +++ b/crates/ui/src/components/notification/alert_modal.rs @@ -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.") }