Add ui::ComponentPreview (#20319)

The `ComponentPreview` trait enables rendering storybook-like previews
of components inside of Zed.


![CleanShot 2024-11-06 at 16 32
25@2x](https://github.com/user-attachments/assets/6894663f-1bbc-4a40-b420-33882e9e239a)


This initial version will work for any component that doesn't return a
view.

Example impl:

```rust
impl ComponentPreview for Checkbox {
    fn description() -> impl Into<Option<&'static str>> {
        "A checkbox lets people choose between opposing..."
    }

    fn examples() -> Vec<ComponentExampleGroup<Self>> {
        vec![
            example_group(
                "Default",
                vec![
                    single_example(
                        "Unselected",
                        Checkbox::new("checkbox_unselected", Selection::Unselected),
                    ),
                    // ... more examples
                ],
            ),
            // ... more examples
        ]
    }
}
```

Example usage:

```rust
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(Checkbox::render_component_previews(cx))
            .child(Icon::render_component_previews(cx))
    }
}
```

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2024-11-06 16:54:18 -05:00 committed by GitHub
parent a409123342
commit f6fbf662b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 249 additions and 26 deletions

View file

@ -115,3 +115,51 @@ impl RenderOnce for Checkbox {
)
}
}
impl ComponentPreview for Checkbox {
fn description() -> impl Into<Option<&'static str>> {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(
"Default",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_unselected", Selection::Unselected),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_indeterminate", Selection::Indeterminate),
),
single_example(
"Selected",
Checkbox::new("checkbox_selected", Selection::Selected),
),
],
),
example_group(
"Disabled",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_disabled_unselected", Selection::Unselected)
.disabled(true),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_disabled_indeterminate", Selection::Indeterminate)
.disabled(true),
),
single_example(
"Selected",
Checkbox::new("checkbox_disabled_selected", Selection::Selected)
.disabled(true),
),
],
),
]
}
}

View file

@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoStaticStr};
use ui_macros::DerivePathStr;
use crate::{prelude::*, Indicator};
use crate::{
prelude::*,
traits::component_preview::{example_group, ComponentExample, ComponentPreview},
Indicator,
};
#[derive(IntoElement)]
pub enum AnyIcon {
@ -494,3 +498,26 @@ impl RenderOnce for IconWithIndicator {
})
}
}
impl ComponentPreview for Icon {
fn examples() -> Vec<ComponentExampleGroup<Icon>> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,
IconName::ArrowRight,
IconName::ArrowUp,
IconName::ArrowCircle,
];
vec![example_group(
"Arrow Icons",
arrow_icons
.into_iter()
.map(|icon| {
let name = format!("{:?}", icon).to_string();
ComponentExample::new(name, Icon::new(icon))
})
.collect(),
)]
}
}

View file

@ -9,6 +9,7 @@ pub use gpui::{
pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize};
pub use crate::traits::clickable::*;
pub use crate::traits::component_preview::*;
pub use crate::traits::disableable::*;
pub use crate::traits::fixed::*;
pub use crate::traits::selectable::*;

View file

@ -1,4 +1,5 @@
pub mod clickable;
pub mod component_preview;
pub mod disableable;
pub mod fixed;
pub mod selectable;

View file

@ -0,0 +1,131 @@
#![allow(missing_docs)]
use crate::prelude::*;
use gpui::{AnyElement, SharedString};
/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
pub trait ComponentPreview: IntoElement {
fn title() -> &'static str {
std::any::type_name::<Self>()
}
fn description() -> impl Into<Option<&'static str>> {
None
}
fn examples() -> Vec<ComponentExampleGroup<Self>>;
fn component_previews() -> Vec<AnyElement> {
Self::examples()
.into_iter()
.map(|example| Self::render_example_group(example))
.collect()
}
fn render_component_previews(cx: &WindowContext) -> AnyElement {
let title = Self::title();
let (source, title) = title
.rsplit_once("::")
.map_or((None, title), |(s, t)| (Some(s), t));
let description = Self::description().into();
v_flex()
.gap_3()
.p_4()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.child(
v_flex()
.gap_1()
.child(
h_flex()
.gap_1()
.child(Headline::new(title).size(HeadlineSize::Small))
.when_some(source, |this, source| {
this.child(Label::new(format!("({})", source)).color(Color::Muted))
}),
)
.when_some(description, |this, description| {
this.child(
div()
.text_ui_sm(cx)
.text_color(cx.theme().colors().text_muted)
.max_w(px(600.0))
.child(description),
)
}),
)
.children(Self::component_previews())
.into_any_element()
}
fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
v_flex()
.gap_2()
.child(Label::new(group.title).size(LabelSize::Small))
.child(
h_flex()
.gap_6()
.children(group.examples.into_iter().map(Self::render_example))
.into_any_element(),
)
.into_any_element()
}
fn render_example(example: ComponentExample<Self>) -> AnyElement {
v_flex()
.gap_1()
.child(example.element)
.child(
Label::new(example.variant_name)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element()
}
}
/// A single example of a component.
pub struct ComponentExample<T> {
variant_name: SharedString,
element: T,
}
impl<T> ComponentExample<T> {
/// Create a new example with the given variant name and example value.
pub fn new(variant_name: impl Into<SharedString>, example: T) -> Self {
Self {
variant_name: variant_name.into(),
element: example,
}
}
}
/// A group of component examples.
pub struct ComponentExampleGroup<T> {
pub title: SharedString,
pub examples: Vec<ComponentExample<T>>,
}
impl<T> ComponentExampleGroup<T> {
/// Create a new group of examples with the given title.
pub fn new(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
Self {
title: title.into(),
examples,
}
}
}
/// Create a single example
pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> ComponentExample<T> {
ComponentExample::new(variant_name, example)
}
/// Create a group of examples
pub fn example_group<T>(
title: impl Into<SharedString>,
examples: Vec<ComponentExample<T>>,
) -> ComponentExampleGroup<T> {
ComponentExampleGroup::new(title, examples)
}