1use 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#[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 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}