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