story.rs

  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}