Add ui::ComponentPreview
(#20319)
The `ComponentPreview` trait enables rendering storybook-like previews of components inside of Zed.  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:
parent
a409123342
commit
f6fbf662b4
6 changed files with 249 additions and 26 deletions
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod clickable;
|
||||
pub mod component_preview;
|
||||
pub mod disableable;
|
||||
pub mod fixed;
|
||||
pub mod selectable;
|
||||
|
|
131
crates/ui/src/traits/component_preview.rs
Normal file
131
crates/ui/src/traits/component_preview.rs
Normal 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)
|
||||
}
|
|
@ -4,8 +4,8 @@ use strum::IntoEnumIterator;
|
|||
use theme::all_theme_colors;
|
||||
use ui::{
|
||||
prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar,
|
||||
AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, ElevationIndex, Facepile,
|
||||
TintColor, Tooltip,
|
||||
AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox, ElevationIndex,
|
||||
Facepile, TintColor, Tooltip,
|
||||
};
|
||||
|
||||
use crate::{Item, Workspace};
|
||||
|
@ -26,6 +26,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
enum ThemePreviewPage {
|
||||
Overview,
|
||||
Typography,
|
||||
Components,
|
||||
}
|
||||
|
||||
impl ThemePreviewPage {
|
||||
|
@ -33,6 +34,7 @@ impl ThemePreviewPage {
|
|||
match self {
|
||||
Self::Overview => "Overview",
|
||||
Self::Typography => "Typography",
|
||||
Self::Components => "Components",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +60,7 @@ impl ThemePreview {
|
|||
match page {
|
||||
ThemePreviewPage::Overview => self.render_overview_page(cx).into_any_element(),
|
||||
ThemePreviewPage::Typography => self.render_typography_page(cx).into_any_element(),
|
||||
ThemePreviewPage::Components => self.render_components_page(cx).into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -456,8 +459,6 @@ impl ThemePreview {
|
|||
.text_color(cx.theme().colors().text)
|
||||
.gap_2()
|
||||
.child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium))
|
||||
.child(self.render_avatars(cx))
|
||||
.child(self.render_buttons(layer, cx))
|
||||
.child(self.render_text(layer, cx))
|
||||
.child(self.render_colors(layer, cx))
|
||||
}
|
||||
|
@ -499,39 +500,53 @@ impl ThemePreview {
|
|||
.child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
|
||||
)
|
||||
}
|
||||
|
||||
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
let layer = ElevationIndex::Surface;
|
||||
|
||||
v_flex()
|
||||
.id("theme-preview-components")
|
||||
.overflow_scroll()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Checkbox::render_component_previews(cx))
|
||||
.child(Icon::render_component_previews(cx))
|
||||
.child(self.render_avatars(cx))
|
||||
.child(self.render_buttons(layer, cx))
|
||||
}
|
||||
|
||||
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("theme-preview-nav")
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.py_2()
|
||||
.bg(Self::preview_bg(cx))
|
||||
.children(ThemePreviewPage::iter().map(|p| {
|
||||
Button::new(ElementId::Name(p.name().into()), p.name())
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.current_page = p;
|
||||
cx.notify();
|
||||
}))
|
||||
.selected(p == self.current_page)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThemePreview {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
|
||||
h_flex()
|
||||
v_flex()
|
||||
.id("theme-preview")
|
||||
.key_context("ThemePreview")
|
||||
.items_start()
|
||||
.overflow_hidden()
|
||||
.size_full()
|
||||
.max_h_full()
|
||||
.p_4()
|
||||
.track_focus(&self.focus_handle)
|
||||
.px_2()
|
||||
.bg(Self::preview_bg(cx))
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.items_start()
|
||||
.gap_1()
|
||||
.w(px(240.))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_px()
|
||||
.children(ThemePreviewPage::iter().map(|p| {
|
||||
Button::new(ElementId::Name(p.name().into()), p.name())
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.current_page = p;
|
||||
cx.notify();
|
||||
}))
|
||||
.selected(p == self.current_page)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(self.render_page_nav(cx))
|
||||
.child(self.view(self.current_page, cx))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue