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