component.rs

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