component.rs

  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}