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