1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! 时间线。
//!
//! 纵向事件流:时间点 + 圆点标记 + 内容卡, commits/动态/日志类场景。
use crate::{prelude::*, *};
/// 时间线条目。
#[derive(Clone)]
pub struct TimelineItem {
/// 时间标签(如 "10:24")。
pub time: SharedString,
/// 标题。
pub title: SharedString,
/// 内容(可选)。
pub content: Option<SharedString>,
/// 圆点颜色(默认 accent)。
pub dot: Option<Hsla>,
}
impl TimelineItem {
/// 创建条目。
pub fn new(time: impl Into<SharedString>, title: impl Into<SharedString>) -> Self {
Self {
time: time.into(),
title: title.into(),
content: None,
dot: None,
}
}
/// 设置内容。
pub fn content(mut self, content: impl Into<SharedString>) -> Self {
self.content = Some(content.into());
self
}
/// 设置圆点颜色。
pub fn dot(mut self, color: Hsla) -> Self {
self.dot = Some(color);
self
}
}
/// 时间线。
#[derive(IntoElement)]
pub struct Timeline {
/// 条目列表。
items: Vec<TimelineItem>,
/// 用户样式。
style: StyleRefinement,
}
impl Timeline {
/// 创建时间线。
pub fn new(items: Vec<TimelineItem>) -> Self {
Self {
items,
style: StyleRefinement::default(),
}
}
}
impl Styled for Timeline {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Timeline {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let border = theme.tokens.border;
let accent = theme.tokens.accent.color;
let muted_foreground = theme.tokens.muted_foreground.color;
let user_style = self.style;
let total = self.items.len();
div()
.flex()
.flex_col()
.w_full()
.children(self.items.into_iter().enumerate().map(|(ix, item)| {
let dot = item.dot.unwrap_or(accent);
div()
.flex()
.flex_row()
.gap(px(10.0))
.child(
div()
.flex()
.flex_col()
.items_center()
.child(
div()
.w(px(10.0))
.h(px(10.0))
.mt(px(4.0))
.rounded_full()
.bg(dot),
)
.when(ix + 1 < total, |this| {
this.child(div().w(px(1.0)).flex_1().bg(border))
}),
)
.child(
div()
.flex()
.flex_col()
.flex_1()
.gap(px(2.0))
.pb(px(12.0))
.child(
div()
.flex()
.flex_row()
.items_center()
.gap(px(8.0))
.child(
div()
.text_sm()
.text_color(accent)
.child(item.title.clone()),
)
.child(
div()
.text_xs()
.text_color(muted_foreground)
.child(item.time.clone()),
),
)
.when_some(item.content, |this, content| {
this.child(
div().text_sm().text_color(muted_foreground).child(content),
)
}),
)
.into_any_element()
}))
.map(|mut this| {
this.style().refine(&user_style);
this
})
}
}