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