1use std::fmt::Display;
2use std::ops::{Deref, DerefMut};
3use std::sync::LazyLock;
4
5use collections::HashMap;
6use gpui::{
7 AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
8 prelude::*, px, rems,
9};
10use linkme::distributed_slice;
11use parking_lot::RwLock;
12use theme::ActiveTheme;
13
14pub trait Component {
15 fn scope() -> ComponentScope {
16 ComponentScope::None
17 }
18 fn name() -> &'static str {
19 std::any::type_name::<Self>()
20 }
21 fn id() -> ComponentId {
22 ComponentId(Self::name())
23 }
24 /// Returns a name that the component should be sorted by.
25 ///
26 /// Implement this if the component should be sorted in an alternate order than its name.
27 ///
28 /// Example:
29 ///
30 /// For example, to group related components together when sorted:
31 ///
32 /// - Button -> ButtonA
33 /// - IconButton -> ButtonBIcon
34 /// - ToggleButton -> ButtonCToggle
35 ///
36 /// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
37 fn sort_name() -> &'static str {
38 Self::name()
39 }
40 fn description() -> Option<&'static str> {
41 None
42 }
43 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
44 None
45 }
46}
47
48#[distributed_slice]
49pub static __ALL_COMPONENTS: [fn()] = [..];
50
51pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
52 LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
53
54pub struct ComponentRegistry {
55 components: Vec<(
56 ComponentScope,
57 // name
58 &'static str,
59 // sort name
60 &'static str,
61 // description
62 Option<&'static str>,
63 )>,
64 previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
65}
66
67impl ComponentRegistry {
68 fn new() -> Self {
69 ComponentRegistry {
70 components: Vec::new(),
71 previews: HashMap::default(),
72 }
73 }
74}
75
76pub fn init() {
77 let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
78 for f in component_fns {
79 f();
80 }
81}
82
83pub fn register_component<T: Component>() {
84 let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
85 let mut data = COMPONENT_DATA.write();
86 data.components.push(component_data);
87 data.previews.insert(T::id().0, T::preview);
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct ComponentId(pub &'static str);
92
93#[derive(Clone)]
94pub struct ComponentMetadata {
95 id: ComponentId,
96 name: SharedString,
97 sort_name: SharedString,
98 scope: ComponentScope,
99 description: Option<SharedString>,
100 preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
101}
102
103impl ComponentMetadata {
104 pub fn id(&self) -> ComponentId {
105 self.id.clone()
106 }
107 pub fn name(&self) -> SharedString {
108 self.name.clone()
109 }
110
111 pub fn sort_name(&self) -> SharedString {
112 self.sort_name.clone()
113 }
114
115 pub fn scopeless_name(&self) -> SharedString {
116 self.name
117 .clone()
118 .split("::")
119 .last()
120 .unwrap_or(&self.name)
121 .to_string()
122 .into()
123 }
124
125 pub fn scope(&self) -> ComponentScope {
126 self.scope.clone()
127 }
128 pub fn description(&self) -> Option<SharedString> {
129 self.description.clone()
130 }
131 pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
132 self.preview
133 }
134}
135
136pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
137
138impl AllComponents {
139 pub fn new() -> Self {
140 AllComponents(HashMap::default())
141 }
142 pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
143 self.0.values().filter(|c| c.preview.is_some()).collect()
144 }
145 pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
146 let mut previews: Vec<ComponentMetadata> =
147 self.all_previews().into_iter().cloned().collect();
148 previews.sort_by_key(|a| a.name());
149 previews
150 }
151 pub fn all(&self) -> Vec<&ComponentMetadata> {
152 self.0.values().collect()
153 }
154 pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
155 let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
156 components.sort_by_key(|a| a.name());
157 components
158 }
159}
160
161impl Deref for AllComponents {
162 type Target = HashMap<ComponentId, ComponentMetadata>;
163 fn deref(&self) -> &Self::Target {
164 &self.0
165 }
166}
167
168impl DerefMut for AllComponents {
169 fn deref_mut(&mut self) -> &mut Self::Target {
170 &mut self.0
171 }
172}
173
174pub fn components() -> AllComponents {
175 let data = COMPONENT_DATA.read();
176 let mut all_components = AllComponents::new();
177 for (scope, name, sort_name, description) in &data.components {
178 let preview = data.previews.get(name).cloned();
179 let component_name = SharedString::new_static(name);
180 let sort_name = SharedString::new_static(sort_name);
181 let id = ComponentId(name);
182 all_components.insert(
183 id.clone(),
184 ComponentMetadata {
185 id,
186 name: component_name,
187 sort_name,
188 scope: scope.clone(),
189 description: description.map(Into::into),
190 preview,
191 },
192 );
193 }
194 all_components
195}
196
197// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
198// pub enum ComponentStatus {
199// WorkInProgress,
200// EngineeringReady,
201// Live,
202// Deprecated,
203// }
204
205#[derive(Debug, Clone, PartialEq, Eq, Hash)]
206pub enum ComponentScope {
207 Agent,
208 Collaboration,
209 DataDisplay,
210 Editor,
211 Images,
212 Input,
213 Layout,
214 Loading,
215 Navigation,
216 None,
217 Notification,
218 Overlays,
219 Status,
220 Typography,
221 VersionControl,
222}
223
224impl Display for ComponentScope {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 ComponentScope::Agent => write!(f, "Agent"),
228 ComponentScope::Collaboration => write!(f, "Collaboration"),
229 ComponentScope::DataDisplay => write!(f, "Data Display"),
230 ComponentScope::Editor => write!(f, "Editor"),
231 ComponentScope::Images => write!(f, "Images & Icons"),
232 ComponentScope::Input => write!(f, "Forms & Input"),
233 ComponentScope::Layout => write!(f, "Layout & Structure"),
234 ComponentScope::Loading => write!(f, "Loading & Progress"),
235 ComponentScope::Navigation => write!(f, "Navigation"),
236 ComponentScope::None => write!(f, "Unsorted"),
237 ComponentScope::Notification => write!(f, "Notification"),
238 ComponentScope::Overlays => write!(f, "Overlays & Layering"),
239 ComponentScope::Status => write!(f, "Status"),
240 ComponentScope::Typography => write!(f, "Typography"),
241 ComponentScope::VersionControl => write!(f, "Version Control"),
242 }
243 }
244}
245
246/// A single example of a component.
247#[derive(IntoElement)]
248pub struct ComponentExample {
249 pub variant_name: SharedString,
250 pub description: Option<SharedString>,
251 pub element: AnyElement,
252 pub width: Option<Pixels>,
253}
254
255impl RenderOnce for ComponentExample {
256 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
257 div()
258 .pt_2()
259 .map(|this| {
260 if let Some(width) = self.width {
261 this.w(width)
262 } else {
263 this.w_full()
264 }
265 })
266 .flex()
267 .flex_col()
268 .gap_3()
269 .child(
270 div()
271 .flex()
272 .flex_col()
273 .child(
274 div()
275 .child(self.variant_name.clone())
276 .text_size(rems(1.0))
277 .text_color(cx.theme().colors().text),
278 )
279 .when_some(self.description, |this, description| {
280 this.child(
281 div()
282 .text_size(rems(0.875))
283 .text_color(cx.theme().colors().text_muted)
284 .child(description.clone()),
285 )
286 }),
287 )
288 .child(
289 div()
290 .flex()
291 .w_full()
292 .rounded_xl()
293 .min_h(px(100.))
294 .justify_center()
295 .p_8()
296 .border_1()
297 .border_color(cx.theme().colors().border.opacity(0.5))
298 .bg(pattern_slash(
299 cx.theme().colors().surface_background.opacity(0.5),
300 12.0,
301 12.0,
302 ))
303 .shadow_sm()
304 .child(self.element),
305 )
306 .into_any_element()
307 }
308}
309
310impl ComponentExample {
311 pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
312 Self {
313 variant_name: variant_name.into(),
314 element,
315 description: None,
316 width: None,
317 }
318 }
319
320 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
321 self.description = Some(description.into());
322 self
323 }
324
325 pub fn width(mut self, width: Pixels) -> Self {
326 self.width = Some(width);
327 self
328 }
329}
330
331/// A group of component examples.
332#[derive(IntoElement)]
333pub struct ComponentExampleGroup {
334 pub title: Option<SharedString>,
335 pub examples: Vec<ComponentExample>,
336 pub width: Option<Pixels>,
337 pub grow: bool,
338 pub vertical: bool,
339}
340
341impl RenderOnce for ComponentExampleGroup {
342 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
343 div()
344 .flex_col()
345 .text_sm()
346 .text_color(cx.theme().colors().text_muted)
347 .map(|this| {
348 if let Some(width) = self.width {
349 this.w(width)
350 } else {
351 this.w_full()
352 }
353 })
354 .when_some(self.title, |this, title| {
355 this.gap_4().child(
356 div()
357 .flex()
358 .items_center()
359 .gap_3()
360 .pb_1()
361 .child(div().h_px().w_4().bg(cx.theme().colors().border))
362 .child(
363 div()
364 .flex_none()
365 .text_size(px(10.))
366 .child(title.to_uppercase()),
367 )
368 .child(
369 div()
370 .h_px()
371 .w_full()
372 .flex_1()
373 .bg(cx.theme().colors().border),
374 ),
375 )
376 })
377 .child(
378 div()
379 .flex()
380 .flex_col()
381 .items_start()
382 .w_full()
383 .gap_6()
384 .children(self.examples)
385 .into_any_element(),
386 )
387 .into_any_element()
388 }
389}
390
391impl ComponentExampleGroup {
392 pub fn new(examples: Vec<ComponentExample>) -> Self {
393 Self {
394 title: None,
395 examples,
396 width: None,
397 grow: false,
398 vertical: false,
399 }
400 }
401 pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
402 Self {
403 title: Some(title.into()),
404 examples,
405 width: None,
406 grow: false,
407 vertical: false,
408 }
409 }
410 pub fn width(mut self, width: Pixels) -> Self {
411 self.width = Some(width);
412 self
413 }
414 pub fn grow(mut self) -> Self {
415 self.grow = true;
416 self
417 }
418 pub fn vertical(mut self) -> Self {
419 self.vertical = true;
420 self
421 }
422}
423
424pub fn single_example(
425 variant_name: impl Into<SharedString>,
426 example: AnyElement,
427) -> ComponentExample {
428 ComponentExample::new(variant_name, example)
429}
430
431pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
432 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())
433}
434
435pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
436 ComponentExampleGroup::new(examples)
437}
438
439pub fn example_group_with_title(
440 title: impl Into<SharedString>,
441 examples: Vec<ComponentExample>,
442) -> ComponentExampleGroup {
443 ComponentExampleGroup::with_title(title, examples)
444}