1#![allow(missing_docs)]
2use crate::{prelude::*, KeyBinding};
3use gpui::{AnyElement, SharedString};
4
5/// Which side of the preview to show labels on
6#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ExampleLabelSide {
8 /// Left side
9 Left,
10 /// Right side
11 Right,
12 #[default]
13 /// Top side
14 Top,
15 /// Bottom side
16 Bottom,
17}
18
19/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
20pub trait ComponentPreview: IntoElement {
21 fn title() -> &'static str {
22 std::any::type_name::<Self>()
23 }
24
25 fn description() -> impl Into<Option<&'static str>> {
26 None
27 }
28
29 fn example_label_side() -> ExampleLabelSide {
30 ExampleLabelSide::default()
31 }
32
33 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>>;
34
35 fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
36 None::<AnyElement>
37 }
38
39 fn component_previews(cx: &mut WindowContext) -> Vec<AnyElement> {
40 Self::examples(cx)
41 .into_iter()
42 .map(|example| Self::render_example_group(example))
43 .collect()
44 }
45
46 fn render_component_previews(cx: &mut WindowContext) -> AnyElement {
47 let title = Self::title();
48 let (source, title) = title
49 .rsplit_once("::")
50 .map_or((None, title), |(s, t)| (Some(s), t));
51 let description = Self::description().into();
52
53 v_flex()
54 .w_full()
55 .gap_6()
56 .p_4()
57 .border_1()
58 .border_color(cx.theme().colors().border)
59 .rounded_md()
60 .child(
61 v_flex()
62 .gap_1()
63 .child(
64 h_flex()
65 .gap_1()
66 .child(Headline::new(title).size(HeadlineSize::Small))
67 .when_some(source, |this, source| {
68 this.child(Label::new(format!("({})", source)).color(Color::Muted))
69 }),
70 )
71 .when_some(description, |this, description| {
72 this.child(
73 div()
74 .text_ui_sm(cx)
75 .text_color(cx.theme().colors().text_muted)
76 .max_w(px(600.0))
77 .child(description),
78 )
79 }),
80 )
81 .when_some(Self::custom_example(cx).into(), |this, custom_example| {
82 this.child(custom_example)
83 })
84 .children(Self::component_previews(cx))
85 .into_any_element()
86 }
87
88 fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
89 v_flex()
90 .gap_6()
91 .when(group.grow, |this| this.w_full().flex_1())
92 .when_some(group.title, |this, title| {
93 this.child(Label::new(title).size(LabelSize::Small))
94 })
95 .child(
96 h_flex()
97 .w_full()
98 .gap_6()
99 .children(group.examples.into_iter().map(Self::render_example))
100 .into_any_element(),
101 )
102 .into_any_element()
103 }
104
105 fn render_example(example: ComponentExample<Self>) -> AnyElement {
106 let base = div().flex();
107
108 let base = match Self::example_label_side() {
109 ExampleLabelSide::Right => base.flex_row(),
110 ExampleLabelSide::Left => base.flex_row_reverse(),
111 ExampleLabelSide::Bottom => base.flex_col(),
112 ExampleLabelSide::Top => base.flex_col_reverse(),
113 };
114
115 base.gap_1()
116 .when(example.grow, |this| this.flex_1())
117 .child(example.element)
118 .child(
119 Label::new(example.variant_name)
120 .size(LabelSize::XSmall)
121 .color(Color::Muted),
122 )
123 .into_any_element()
124 }
125}
126
127/// A single example of a component.
128pub struct ComponentExample<T> {
129 variant_name: SharedString,
130 element: T,
131 grow: bool,
132}
133
134impl<T> ComponentExample<T> {
135 /// Create a new example with the given variant name and example value.
136 pub fn new(variant_name: impl Into<SharedString>, example: T) -> Self {
137 Self {
138 variant_name: variant_name.into(),
139 element: example,
140 grow: false,
141 }
142 }
143
144 /// Set the example to grow to fill the available horizontal space.
145 pub fn grow(mut self) -> Self {
146 self.grow = true;
147 self
148 }
149}
150
151/// A group of component examples.
152pub struct ComponentExampleGroup<T> {
153 pub title: Option<SharedString>,
154 pub examples: Vec<ComponentExample<T>>,
155 pub grow: bool,
156}
157
158impl<T> ComponentExampleGroup<T> {
159 /// Create a new group of examples with the given title.
160 pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
161 Self {
162 title: None,
163 examples,
164 grow: false,
165 }
166 }
167
168 /// Create a new group of examples with the given title.
169 pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
170 Self {
171 title: Some(title.into()),
172 examples,
173 grow: false,
174 }
175 }
176
177 /// Set the group to grow to fill the available horizontal space.
178 pub fn grow(mut self) -> Self {
179 self.grow = true;
180 self
181 }
182}
183
184/// Create a single example
185pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> ComponentExample<T> {
186 ComponentExample::new(variant_name, example)
187}
188
189/// Create a group of examples without a title
190pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
191 ComponentExampleGroup::new(examples)
192}
193
194/// Create a group of examples with a title
195pub fn example_group_with_title<T>(
196 title: impl Into<SharedString>,
197 examples: Vec<ComponentExample<T>>,
198) -> ComponentExampleGroup<T> {
199 ComponentExampleGroup::with_title(title, examples)
200}
201
202pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding {
203 KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None))
204}