component.rs

  1//! # Component
  2//!
  3//! This module provides the Component trait, which is used to define
  4//! components for visual testing and debugging.
  5//!
  6//! Additionally, it includes layouts for rendering component examples
  7//! and example groups, as well as the distributed slice mechanism for
  8//! registering components.
  9
 10mod component_layout;
 11
 12pub use component_layout::*;
 13
 14use std::sync::LazyLock;
 15
 16use collections::HashMap;
 17use gpui::{AnyElement, App, SharedString, Window};
 18use linkme::distributed_slice;
 19use parking_lot::RwLock;
 20use strum::{Display, EnumString};
 21
 22pub fn components() -> ComponentRegistry {
 23    COMPONENT_DATA.read().clone()
 24}
 25
 26pub fn init() {
 27    let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
 28    for f in component_fns {
 29        f();
 30    }
 31}
 32
 33pub fn register_component<T: Component>() {
 34    let id = T::id();
 35    let metadata = ComponentMetadata {
 36        id: id.clone(),
 37        description: T::description().map(Into::into),
 38        name: SharedString::new_static(T::name()),
 39        preview: Some(T::preview),
 40        scope: T::scope(),
 41        sort_name: SharedString::new_static(T::sort_name()),
 42        status: T::status(),
 43    };
 44
 45    let mut data = COMPONENT_DATA.write();
 46    data.components.insert(id, metadata);
 47}
 48
 49#[distributed_slice]
 50pub static __ALL_COMPONENTS: [fn()] = [..];
 51
 52pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
 53    LazyLock::new(|| RwLock::new(ComponentRegistry::default()));
 54
 55#[derive(Default, Clone)]
 56pub struct ComponentRegistry {
 57    components: HashMap<ComponentId, ComponentMetadata>,
 58}
 59
 60impl ComponentRegistry {
 61    pub fn previews(&self) -> Vec<&ComponentMetadata> {
 62        self.components
 63            .values()
 64            .filter(|c| c.preview.is_some())
 65            .collect()
 66    }
 67
 68    pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
 69        let mut previews: Vec<ComponentMetadata> = self.previews().into_iter().cloned().collect();
 70        previews.sort_by_key(|a| a.name());
 71        previews
 72    }
 73
 74    pub fn components(&self) -> Vec<&ComponentMetadata> {
 75        self.components.values().collect()
 76    }
 77
 78    pub fn sorted_components(&self) -> Vec<ComponentMetadata> {
 79        let mut components: Vec<ComponentMetadata> =
 80            self.components().into_iter().cloned().collect();
 81        components.sort_by_key(|a| a.name());
 82        components
 83    }
 84
 85    pub fn component_map(&self) -> HashMap<ComponentId, ComponentMetadata> {
 86        self.components.clone()
 87    }
 88
 89    pub fn get(&self, id: &ComponentId) -> Option<&ComponentMetadata> {
 90        self.components.get(id)
 91    }
 92
 93    pub fn len(&self) -> usize {
 94        self.components.len()
 95    }
 96}
 97
 98#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 99pub struct ComponentId(pub &'static str);
100
101#[derive(Clone)]
102pub struct ComponentMetadata {
103    id: ComponentId,
104    description: Option<SharedString>,
105    name: SharedString,
106    preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
107    scope: ComponentScope,
108    sort_name: SharedString,
109    status: ComponentStatus,
110}
111
112impl ComponentMetadata {
113    pub fn id(&self) -> ComponentId {
114        self.id.clone()
115    }
116
117    pub fn description(&self) -> Option<SharedString> {
118        self.description.clone()
119    }
120
121    pub fn name(&self) -> SharedString {
122        self.name.clone()
123    }
124
125    pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
126        self.preview
127    }
128
129    pub fn scope(&self) -> ComponentScope {
130        self.scope.clone()
131    }
132
133    pub fn sort_name(&self) -> SharedString {
134        self.sort_name.clone()
135    }
136
137    pub fn scopeless_name(&self) -> SharedString {
138        self.name
139            .clone()
140            .split("::")
141            .last()
142            .unwrap_or(&self.name)
143            .to_string()
144            .into()
145    }
146
147    pub fn status(&self) -> ComponentStatus {
148        self.status.clone()
149    }
150}
151
152/// Implement this trait to define a UI component. This will allow you to
153/// derive `RegisterComponent` on it, in tutn allowing you to preview the
154/// contents of the preview fn in `workspace: open component preview`.
155///
156/// This can be useful for visual debugging and testing, documenting UI
157/// patterns, or simply showing all the variants of a component.
158///
159/// Generally you will want to implement at least `scope` and `preview`
160/// from this trait, so you can preview the component, and it will show up
161/// in a section that makes sense.
162pub trait Component {
163    /// The component's unique identifier.
164    ///
165    /// Used to access previews, or state for more
166    /// complex, stateful components.
167    fn id() -> ComponentId {
168        ComponentId(Self::name())
169    }
170    /// Returns the scope of the component.
171    ///
172    /// This scope is used to determine how components and
173    /// their previews are displayed and organized.
174    fn scope() -> ComponentScope {
175        ComponentScope::None
176    }
177    /// The ready status of this component.
178    ///
179    /// Use this to mark when components are:
180    /// - `WorkInProgress`: Still being designed or are partially implemented.
181    /// - `EngineeringReady`: Ready to be implemented.
182    /// - `Deprecated`: No longer recommended for use.
183    ///
184    /// Defaults to [`Live`](ComponentStatus::Live).
185    fn status() -> ComponentStatus {
186        ComponentStatus::Live
187    }
188    /// The name of the component.
189    ///
190    /// This name is used to identify the component
191    /// and is usually derived from the component's type.
192    fn name() -> &'static str {
193        std::any::type_name::<Self>()
194    }
195    /// Returns a name that the component should be sorted by.
196    ///
197    /// Implement this if the component should be sorted in an alternate order than its name.
198    ///
199    /// Example:
200    ///
201    /// For example, to group related components together when sorted:
202    ///
203    /// - Button      -> ButtonA
204    /// - IconButton  -> ButtonBIcon
205    /// - ToggleButton -> ButtonCToggle
206    ///
207    /// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
208    fn sort_name() -> &'static str {
209        Self::name()
210    }
211    /// An optional description of the component.
212    ///
213    /// This will be displayed in the component's preview. To show a
214    /// component's doc comment as it's description, derive `Documented`.
215    ///
216    /// Example:
217    ///
218    /// ```
219    /// /// This is a doc comment.
220    /// #[derive(Documented)]
221    /// struct MyComponent;
222    ///
223    /// impl MyComponent {
224    ///     fn description() -> Option<&'static str> {
225    ///         Some(Self::DOCS)
226    ///     }
227    /// }
228    /// ```
229    ///
230    /// This will result in "This is a doc comment." being passed
231    /// to the component's description.
232    fn description() -> Option<&'static str> {
233        None
234    }
235    /// The component's preview.
236    ///
237    /// An element returned here will be shown in the component's preview.
238    ///
239    /// Useful component helpers:
240    /// - [`component::single_example`]
241    /// - [`component::component_group`]
242    /// - [`component::component_group_with_title`]
243    ///
244    /// Note: Any arbitrary element can be returned here.
245    ///
246    /// This is useful for displaying related UI to the component you are
247    /// trying to preview, such as a button that opens a modal or shows a
248    /// tooltip on hover, or a grid of icons showcasing all the icons available.
249    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
250        None
251    }
252}
253
254/// The ready status of this component.
255///
256/// Use this to mark when components are:
257/// - `WorkInProgress`: Still being designed or are partially implemented.
258/// - `EngineeringReady`: Ready to be implemented.
259/// - `Deprecated`: No longer recommended for use.
260///
261/// Defaults to [`Live`](ComponentStatus::Live).
262#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
263pub enum ComponentStatus {
264    #[strum(serialize = "Work In Progress")]
265    WorkInProgress,
266    #[strum(serialize = "Ready To Build")]
267    EngineeringReady,
268    Live,
269    Deprecated,
270}
271
272impl ComponentStatus {
273    pub fn description(&self) -> &str {
274        match self {
275            ComponentStatus::WorkInProgress => {
276                "These components are still being designed or refined. They shouldn't be used in the app yet."
277            }
278            ComponentStatus::EngineeringReady => {
279                "These components are design complete or partially implemented, and are ready for an engineer to complete their implementation."
280            }
281            ComponentStatus::Live => "These components are ready for use in the app.",
282            ComponentStatus::Deprecated => {
283                "These components are no longer recommended for use in the app, and may be removed in a future release."
284            }
285        }
286    }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
290pub enum ComponentScope {
291    Agent,
292    Collaboration,
293    #[strum(serialize = "Data Display")]
294    DataDisplay,
295    Editor,
296    #[strum(serialize = "Images & Icons")]
297    Images,
298    #[strum(serialize = "Forms & Input")]
299    Input,
300    #[strum(serialize = "Layout & Structure")]
301    Layout,
302    #[strum(serialize = "Loading & Progress")]
303    Loading,
304    Navigation,
305    #[strum(serialize = "Unsorted")]
306    None,
307    Notification,
308    #[strum(serialize = "Overlays & Layering")]
309    Overlays,
310    Status,
311    Typography,
312    #[strum(serialize = "Version Control")]
313    VersionControl,
314}