1use std::fmt::Display;
2use std::ops::{Deref, DerefMut};
3use std::sync::LazyLock;
4
5use collections::HashMap;
6use gpui::{AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px};
7use linkme::distributed_slice;
8use parking_lot::RwLock;
9use theme::ActiveTheme;
10
11pub trait Component {
12 fn scope() -> Option<ComponentScope>;
13 fn name() -> &'static str {
14 std::any::type_name::<Self>()
15 }
16 fn description() -> Option<&'static str> {
17 None
18 }
19}
20
21pub trait ComponentPreview: Component {
22 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
23}
24
25#[distributed_slice]
26pub static __ALL_COMPONENTS: [fn()] = [..];
27
28#[distributed_slice]
29pub static __ALL_PREVIEWS: [fn()] = [..];
30
31pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
32 LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
33
34pub struct ComponentRegistry {
35 components: Vec<(Option<ComponentScope>, &'static str, Option<&'static str>)>,
36 previews: HashMap<&'static str, fn(&mut Window, &mut App) -> AnyElement>,
37}
38
39impl ComponentRegistry {
40 fn new() -> Self {
41 ComponentRegistry {
42 components: Vec::new(),
43 previews: HashMap::default(),
44 }
45 }
46}
47
48pub fn init() {
49 let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
50 let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect();
51
52 for f in component_fns {
53 f();
54 }
55 for f in preview_fns {
56 f();
57 }
58}
59
60pub fn register_component<T: Component>() {
61 let component_data = (T::scope(), T::name(), T::description());
62 COMPONENT_DATA.write().components.push(component_data);
63}
64
65pub fn register_preview<T: ComponentPreview>() {
66 let preview_data = (
67 T::name(),
68 T::preview as fn(&mut Window, &mut App) -> AnyElement,
69 );
70 COMPONENT_DATA
71 .write()
72 .previews
73 .insert(preview_data.0, preview_data.1);
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub struct ComponentId(pub &'static str);
78
79#[derive(Clone)]
80pub struct ComponentMetadata {
81 id: ComponentId,
82 name: SharedString,
83 scope: Option<ComponentScope>,
84 description: Option<SharedString>,
85 preview: Option<fn(&mut Window, &mut App) -> AnyElement>,
86}
87
88impl ComponentMetadata {
89 pub fn id(&self) -> ComponentId {
90 self.id.clone()
91 }
92
93 pub fn name(&self) -> SharedString {
94 self.name.clone()
95 }
96
97 pub fn scope(&self) -> Option<ComponentScope> {
98 self.scope.clone()
99 }
100
101 pub fn description(&self) -> Option<SharedString> {
102 self.description.clone()
103 }
104
105 pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> AnyElement> {
106 self.preview
107 }
108}
109
110pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
111
112impl AllComponents {
113 pub fn new() -> Self {
114 AllComponents(HashMap::default())
115 }
116
117 /// Returns all components with previews
118 pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
119 self.0.values().filter(|c| c.preview.is_some()).collect()
120 }
121
122 /// Returns all components with previews sorted by name
123 pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
124 let mut previews: Vec<ComponentMetadata> =
125 self.all_previews().into_iter().cloned().collect();
126 previews.sort_by_key(|a| a.name());
127 previews
128 }
129
130 /// Returns all components
131 pub fn all(&self) -> Vec<&ComponentMetadata> {
132 self.0.values().collect()
133 }
134
135 /// Returns all components sorted by name
136 pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
137 let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
138 components.sort_by_key(|a| a.name());
139 components
140 }
141}
142
143impl Deref for AllComponents {
144 type Target = HashMap<ComponentId, ComponentMetadata>;
145
146 fn deref(&self) -> &Self::Target {
147 &self.0
148 }
149}
150
151impl DerefMut for AllComponents {
152 fn deref_mut(&mut self) -> &mut Self::Target {
153 &mut self.0
154 }
155}
156
157pub fn components() -> AllComponents {
158 let data = COMPONENT_DATA.read();
159 let mut all_components = AllComponents::new();
160
161 for (scope, name, description) in &data.components {
162 let preview = data.previews.get(name).cloned();
163 let component_name = SharedString::new_static(name);
164 let id = ComponentId(name);
165 all_components.insert(
166 id.clone(),
167 ComponentMetadata {
168 id,
169 name: component_name,
170 scope: scope.clone(),
171 description: description.map(Into::into),
172 preview,
173 },
174 );
175 }
176
177 all_components
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Hash)]
181pub enum ComponentScope {
182 Layout,
183 Input,
184 Notification,
185 Editor,
186 Collaboration,
187 VersionControl,
188 Unknown(SharedString),
189}
190
191impl Display for ComponentScope {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 ComponentScope::Layout => write!(f, "Layout"),
195 ComponentScope::Input => write!(f, "Input"),
196 ComponentScope::Notification => write!(f, "Notification"),
197 ComponentScope::Editor => write!(f, "Editor"),
198 ComponentScope::Collaboration => write!(f, "Collaboration"),
199 ComponentScope::VersionControl => write!(f, "Version Control"),
200 ComponentScope::Unknown(name) => write!(f, "Unknown: {}", name),
201 }
202 }
203}
204
205impl From<&str> for ComponentScope {
206 fn from(value: &str) -> Self {
207 match value {
208 "Layout" => ComponentScope::Layout,
209 "Input" => ComponentScope::Input,
210 "Notification" => ComponentScope::Notification,
211 "Editor" => ComponentScope::Editor,
212 "Collaboration" => ComponentScope::Collaboration,
213 "Version Control" | "VersionControl" => ComponentScope::VersionControl,
214 _ => ComponentScope::Unknown(SharedString::new(value)),
215 }
216 }
217}
218
219impl From<String> for ComponentScope {
220 fn from(value: String) -> Self {
221 match value.as_str() {
222 "Layout" => ComponentScope::Layout,
223 "Input" => ComponentScope::Input,
224 "Notification" => ComponentScope::Notification,
225 "Editor" => ComponentScope::Editor,
226 "Collaboration" => ComponentScope::Collaboration,
227 "Version Control" | "VersionControl" => ComponentScope::VersionControl,
228 _ => ComponentScope::Unknown(SharedString::new(value)),
229 }
230 }
231}
232
233/// Which side of the preview to show labels on
234#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
235pub enum ExampleLabelSide {
236 /// Left side
237 Left,
238 /// Right side
239 Right,
240 /// Top side
241 #[default]
242 Top,
243 /// Bottom side
244 Bottom,
245}
246
247/// A single example of a component.
248#[derive(IntoElement)]
249pub struct ComponentExample {
250 variant_name: SharedString,
251 element: AnyElement,
252 label_side: ExampleLabelSide,
253 grow: bool,
254}
255
256impl RenderOnce for ComponentExample {
257 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
258 let base = div().flex();
259
260 let base = match self.label_side {
261 ExampleLabelSide::Right => base.flex_row(),
262 ExampleLabelSide::Left => base.flex_row_reverse(),
263 ExampleLabelSide::Bottom => base.flex_col(),
264 ExampleLabelSide::Top => base.flex_col_reverse(),
265 };
266
267 base.gap_2()
268 .p_2()
269 .text_size(px(10.))
270 .text_color(cx.theme().colors().text_muted)
271 .when(self.grow, |this| this.flex_1())
272 .when(!self.grow, |this| this.flex_none())
273 .child(self.element)
274 .child(self.variant_name)
275 .into_any_element()
276 }
277}
278
279impl ComponentExample {
280 /// Create a new example with the given variant name and example value.
281 pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
282 Self {
283 variant_name: variant_name.into(),
284 element,
285 label_side: ExampleLabelSide::default(),
286 grow: false,
287 }
288 }
289
290 /// Set the example to grow to fill the available horizontal space.
291 pub fn grow(mut self) -> Self {
292 self.grow = true;
293 self
294 }
295}
296
297/// A group of component examples.
298#[derive(IntoElement)]
299pub struct ComponentExampleGroup {
300 pub title: Option<SharedString>,
301 pub examples: Vec<ComponentExample>,
302 pub grow: bool,
303 pub vertical: bool,
304}
305
306impl RenderOnce for ComponentExampleGroup {
307 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
308 div()
309 .flex_col()
310 .text_sm()
311 .text_color(cx.theme().colors().text_muted)
312 .when(self.grow, |this| this.w_full().flex_1())
313 .when_some(self.title, |this, title| {
314 this.gap_4().child(
315 div()
316 .flex()
317 .items_center()
318 .gap_3()
319 .pb_1()
320 .child(div().h_px().w_4().bg(cx.theme().colors().border))
321 .child(
322 div()
323 .flex_none()
324 .text_size(px(10.))
325 .child(title.to_uppercase()),
326 )
327 .child(
328 div()
329 .h_px()
330 .w_full()
331 .flex_1()
332 .bg(cx.theme().colors().border),
333 ),
334 )
335 })
336 .child(
337 div()
338 .flex()
339 .when(self.vertical, |this| this.flex_col())
340 .items_start()
341 .w_full()
342 .gap_6()
343 .children(self.examples)
344 .into_any_element(),
345 )
346 .into_any_element()
347 }
348}
349
350impl ComponentExampleGroup {
351 /// Create a new group of examples with the given title.
352 pub fn new(examples: Vec<ComponentExample>) -> Self {
353 Self {
354 title: None,
355 examples,
356 grow: false,
357 vertical: false,
358 }
359 }
360
361 /// Create a new group of examples with the given title.
362 pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
363 Self {
364 title: Some(title.into()),
365 examples,
366 grow: false,
367 vertical: false,
368 }
369 }
370
371 /// Set the group to grow to fill the available horizontal space.
372 pub fn grow(mut self) -> Self {
373 self.grow = true;
374 self
375 }
376
377 /// Lay the group out vertically.
378 pub fn vertical(mut self) -> Self {
379 self.vertical = true;
380 self
381 }
382}
383
384/// Create a single example
385pub fn single_example(
386 variant_name: impl Into<SharedString>,
387 example: AnyElement,
388) -> ComponentExample {
389 ComponentExample::new(variant_name, example)
390}
391
392/// Create a group of examples without a title
393pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
394 ComponentExampleGroup::new(examples)
395}
396
397/// Create a group of examples with a title
398pub fn example_group_with_title(
399 title: impl Into<SharedString>,
400 examples: Vec<ComponentExample>,
401) -> ComponentExampleGroup {
402 ComponentExampleGroup::with_title(title, examples)
403}