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