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, 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}