Skip to main content

guise/data/
timeline.rs

1//! `Timeline` — a vertical sequence of events with bullets and connectors.
2
3use gpui::prelude::*;
4use gpui::{div, px, App, FontWeight, IntoElement, SharedString, Window};
5
6use crate::devtools::Probed;
7use crate::theme::{theme, ColorName, Size};
8
9struct Item {
10    title: SharedString,
11    description: Option<SharedString>,
12}
13
14/// A vertical timeline. Items up to and including `active` are highlighted.
15#[derive(IntoElement)]
16pub struct Timeline {
17    items: Vec<Item>,
18    active: usize,
19    color: ColorName,
20}
21
22impl Timeline {
23    pub fn new() -> Self {
24        Timeline {
25            items: Vec::new(),
26            active: 0,
27            color: ColorName::Blue,
28        }
29    }
30
31    pub fn item(mut self, title: impl Into<SharedString>) -> Self {
32        self.items.push(Item {
33            title: title.into(),
34            description: None,
35        });
36        self
37    }
38
39    pub fn item_desc(
40        mut self,
41        title: impl Into<SharedString>,
42        description: impl Into<SharedString>,
43    ) -> Self {
44        self.items.push(Item {
45            title: title.into(),
46            description: Some(description.into()),
47        });
48        self
49    }
50
51    /// Index of the last highlighted item.
52    pub fn active(mut self, active: usize) -> Self {
53        self.active = active;
54        self
55    }
56
57    pub fn color(mut self, color: ColorName) -> Self {
58        self.color = color;
59        self
60    }
61}
62
63impl Default for Timeline {
64    fn default() -> Self {
65        Timeline::new()
66    }
67}
68
69impl RenderOnce for Timeline {
70    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
71        let t = theme(cx);
72        let accent = t.color(self.color, t.primary_shade()).hsla();
73        let border = t.border().hsla();
74        let surface = t.surface().hsla();
75        let text = t.text().hsla();
76        let dimmed = t.dimmed().hsla();
77        let font = t.font_size(Size::Sm);
78        let last = self.items.len().saturating_sub(1);
79
80        let mut column = div().flex().flex_col();
81        for (i, item) in self.items.iter().enumerate() {
82            let reached = i <= self.active;
83            let connector_done = i < self.active;
84
85            let mut rail = div().flex().flex_col().items_center().w(px(20.0));
86            let mut dot = div().w(px(14.0)).h(px(14.0)).rounded(px(7.0));
87            dot = if reached {
88                dot.bg(accent)
89            } else {
90                dot.bg(surface).border_1().border_color(border)
91            };
92            rail = rail.child(dot);
93            if i != last {
94                rail = rail.child(
95                    div()
96                        .w(px(2.0))
97                        .flex_grow()
98                        .min_h(px(12.0))
99                        .bg(if connector_done { accent } else { border }),
100                );
101            }
102
103            let mut content = div()
104                .flex()
105                .flex_col()
106                .gap(px(2.0))
107                .pb(px(if i == last { 0.0 } else { 18.0 }))
108                .child(
109                    div()
110                        .font_weight(FontWeight::SEMIBOLD)
111                        .text_size(px(font))
112                        .text_color(text)
113                        .child(item.title.clone()),
114                );
115            if let Some(description) = item.description.clone() {
116                content = content.child(
117                    div()
118                        .text_size(px(t.font_size(Size::Xs)))
119                        .text_color(dimmed)
120                        .child(description),
121                );
122            }
123
124            column = column.child(div().flex().gap(px(10.0)).child(rail).child(content));
125        }
126        column.probe("Timeline")
127    }
128}