story.rs

  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}