component.rs

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