1use gpui::{
2 div, hsla, prelude::*, px, rems, AnyElement, Div, ElementId, Hsla, SharedString, Stateful,
3 WindowContext,
4};
5use itertools::Itertools;
6use smallvec::SmallVec;
7
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12static COUNTER: AtomicUsize = AtomicUsize::new(0);
13
14pub fn reasonably_unique_id() -> String {
15 let now = SystemTime::now();
16 let timestamp = now.duration_since(UNIX_EPOCH).unwrap();
17
18 let cnt = COUNTER.fetch_add(1, Ordering::Relaxed);
19
20 let id = format!("{}_{}", timestamp.as_nanos(), cnt);
21
22 id
23}
24
25pub struct StoryColor {
26 pub primary: Hsla,
27 pub secondary: Hsla,
28 pub border: Hsla,
29 pub background: Hsla,
30 pub card_background: Hsla,
31 pub divider: Hsla,
32 pub link: Hsla,
33}
34
35impl StoryColor {
36 pub fn new() -> Self {
37 Self {
38 primary: hsla(216. / 360., 11. / 100., 0. / 100., 1.),
39 secondary: hsla(216. / 360., 11. / 100., 16. / 100., 1.),
40 border: hsla(216. / 360., 11. / 100., 91. / 100., 1.),
41 background: hsla(0. / 360., 0. / 100., 100. / 100., 1.),
42 card_background: hsla(0. / 360., 0. / 100., 96. / 100., 1.),
43 divider: hsla(216. / 360., 11. / 100., 86. / 100., 1.),
44 link: hsla(206. / 360., 100. / 100., 50. / 100., 1.),
45 }
46 }
47}
48
49pub fn story_color() -> StoryColor {
50 StoryColor::new()
51}
52
53#[derive(IntoElement)]
54pub struct StoryContainer {
55 title: SharedString,
56 relative_path: &'static str,
57 children: SmallVec<[AnyElement; 2]>,
58}
59
60impl StoryContainer {
61 pub fn new(title: impl Into<SharedString>, relative_path: &'static str) -> Self {
62 Self {
63 title: title.into(),
64 relative_path,
65 children: SmallVec::new(),
66 }
67 }
68}
69
70impl ParentElement for StoryContainer {
71 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
72 &mut self.children
73 }
74}
75
76impl RenderOnce for StoryContainer {
77 type Rendered = Stateful<Div>;
78
79 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
80 div()
81 .size_full()
82 .flex()
83 .flex_col()
84 .id("story_container")
85 .bg(story_color().background)
86 .child(
87 div()
88 .flex()
89 .flex_none()
90 .w_full()
91 .justify_between()
92 .p_2()
93 .bg(story_color().background)
94 .border_b()
95 .border_color(story_color().border)
96 .child(Story::title(self.title))
97 .child(
98 div()
99 .text_xs()
100 .text_color(story_color().primary)
101 .child(Story::open_story_link(self.relative_path)),
102 ),
103 )
104 .child(
105 div()
106 .w_full()
107 .h_px()
108 .flex_1()
109 .id("story_body")
110 .overflow_hidden_x()
111 .overflow_y_scroll()
112 .flex()
113 .flex_col()
114 .pb_4()
115 .children(self.children),
116 )
117 }
118}
119
120pub struct Story {}
121
122impl Story {
123 pub fn container() -> Div {
124 div().size_full().overflow_hidden().child(
125 div()
126 .id("story_container")
127 .overflow_y_scroll()
128 .w_full()
129 .min_h_full()
130 .flex()
131 .flex_col()
132 .bg(story_color().background),
133 )
134 }
135
136 // TODO: Move all stories to container2, then rename
137 pub fn container2<T>(relative_path: &'static str) -> Div {
138 div().size_full().child(
139 div()
140 .size_full()
141 .id("story_container")
142 .overflow_y_scroll()
143 .flex()
144 .flex_col()
145 .flex_none()
146 .child(
147 div()
148 .flex()
149 .justify_between()
150 .p_2()
151 .border_b()
152 .border_color(story_color().border)
153 .child(Story::title_for::<T>())
154 .child(
155 div()
156 .text_xs()
157 .text_color(story_color().primary)
158 .child(Story::open_story_link(relative_path)),
159 ),
160 )
161 .child(
162 div()
163 .w_full()
164 .min_h_full()
165 .flex()
166 .flex_col()
167 .bg(story_color().background),
168 ),
169 )
170 }
171
172 pub fn open_story_link(relative_path: &'static str) -> impl Element {
173 let path = PathBuf::from_iter([relative_path]);
174
175 div()
176 .flex()
177 .gap_2()
178 .text_xs()
179 .text_color(story_color().primary)
180 .id(SharedString::from(format!("id_{}", relative_path)))
181 .on_click({
182 let path = path.clone();
183
184 move |_event, _cx| {
185 let path = format!("{}:0:0", path.to_string_lossy());
186
187 std::process::Command::new("zed").arg(path).spawn().ok();
188 }
189 })
190 .children(vec![div().child(Story::link("Open in Zed →"))])
191 }
192
193 pub fn title(title: impl Into<SharedString>) -> impl Element {
194 div()
195 .text_xs()
196 .text_color(story_color().primary)
197 .child(title.into())
198 }
199
200 pub fn title_for<T>() -> impl Element {
201 Self::title(std::any::type_name::<T>())
202 }
203
204 pub fn section() -> Div {
205 div()
206 .p_4()
207 .m_4()
208 .border()
209 .border_color(story_color().border)
210 }
211
212 pub fn section_title() -> Div {
213 div().text_lg().text_color(story_color().primary)
214 }
215
216 pub fn group() -> Div {
217 div().my_2().bg(story_color().background)
218 }
219
220 pub fn code_block(code: impl Into<SharedString>) -> Div {
221 div()
222 .size_full()
223 .p_2()
224 .max_w(rems(36.))
225 .bg(gpui::black())
226 .rounded_md()
227 .text_sm()
228 .text_color(gpui::white())
229 .overflow_hidden()
230 .child(code.into())
231 }
232
233 pub fn divider() -> Div {
234 div().my_2().h(px(1.)).bg(story_color().divider)
235 }
236
237 pub fn link(link: impl Into<SharedString>) -> impl Element {
238 div()
239 .id(ElementId::from(SharedString::from(reasonably_unique_id())))
240 .text_xs()
241 .text_color(story_color().link)
242 .cursor(gpui::CursorStyle::PointingHand)
243 .child(link.into())
244 }
245
246 pub fn description(description: impl Into<SharedString>) -> impl Element {
247 div()
248 .text_sm()
249 .text_color(story_color().secondary)
250 .min_w_96()
251 .child(description.into())
252 }
253
254 pub fn label(label: impl Into<SharedString>) -> impl Element {
255 div()
256 .text_xs()
257 .text_color(story_color().primary)
258 .child(label.into())
259 }
260
261 /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate.
262 pub fn v_stack() -> Div {
263 div().flex().flex_col().gap_1()
264 }
265}
266
267#[derive(IntoElement)]
268pub struct StoryItem {
269 label: SharedString,
270 item: AnyElement,
271 description: Option<SharedString>,
272 usage: Option<SharedString>,
273}
274
275impl StoryItem {
276 pub fn new(label: impl Into<SharedString>, item: impl IntoElement) -> Self {
277 Self {
278 label: label.into(),
279 item: item.into_any_element(),
280 description: None,
281 usage: None,
282 }
283 }
284
285 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
286 self.description = Some(description.into());
287 self
288 }
289
290 pub fn usage(mut self, code: impl Into<SharedString>) -> Self {
291 self.usage = Some(code.into());
292 self
293 }
294}
295
296impl RenderOnce for StoryItem {
297 type Rendered = Div;
298
299 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
300 div()
301 .my_2()
302 .flex()
303 .gap_4()
304 .w_full()
305 .child(
306 Story::v_stack()
307 .px_2()
308 .w_1_2()
309 .min_h_px()
310 .child(Story::label(self.label))
311 .child(
312 div()
313 .rounded_md()
314 .bg(story_color().card_background)
315 .border()
316 .border_color(story_color().border)
317 .py_1()
318 .px_2()
319 .overflow_hidden()
320 .child(self.item),
321 )
322 .when_some(self.description, |this, description| {
323 this.child(Story::description(description))
324 }),
325 )
326 .child(
327 Story::v_stack()
328 .px_2()
329 .flex_none()
330 .w_1_2()
331 .min_h_px()
332 .when_some(self.usage, |this, usage| {
333 this.child(Story::label("Example Usage"))
334 .child(Story::code_block(usage))
335 }),
336 )
337 }
338}
339
340#[derive(IntoElement)]
341pub struct StorySection {
342 description: Option<SharedString>,
343 children: SmallVec<[AnyElement; 2]>,
344}
345
346impl StorySection {
347 pub fn new() -> Self {
348 Self {
349 description: None,
350 children: SmallVec::new(),
351 }
352 }
353
354 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
355 self.description = Some(description.into());
356 self
357 }
358}
359
360impl RenderOnce for StorySection {
361 type Rendered = Div;
362
363 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
364 let children: SmallVec<[AnyElement; 2]> = SmallVec::from_iter(Itertools::intersperse_with(
365 self.children.into_iter(),
366 || Story::divider().into_any_element(),
367 ));
368
369 Story::section()
370 // Section title
371 .py_2()
372 // Section description
373 .when_some(self.description.clone(), |section, description| {
374 section.child(Story::description(description))
375 })
376 .child(div().flex().flex_col().gap_2().children(children))
377 .child(Story::divider())
378 }
379}
380
381impl ParentElement for StorySection {
382 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
383 &mut self.children
384 }
385}