1use std::ops::Range;
2
3use db::smol::stream::iter;
4use gpui::{Entity, FontWeight, Length, uniform_list};
5use ui::{
6 ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
7 ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, IntoElement,
8 ParentElement, RegisterComponent, RenderOnce, Styled, StyledTypography, Window, div,
9 example_group_with_title, px, single_example, v_flex,
10};
11
12struct UniformListData {
13 render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<AnyElement>>,
14 element_id: ElementId,
15 row_count: usize,
16}
17
18enum TableContents<const COLS: usize> {
19 Vec(Vec<[AnyElement; COLS]>),
20 UniformList(UniformListData),
21}
22
23impl<const COLS: usize> TableContents<COLS> {
24 fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
25 match self {
26 TableContents::Vec(rows) => Some(rows),
27 TableContents::UniformList(_) => None,
28 }
29 }
30
31 fn len(&self) -> usize {
32 match self {
33 TableContents::Vec(rows) => rows.len(),
34 TableContents::UniformList(data) => data.row_count,
35 }
36 }
37}
38
39/// A table component
40#[derive(RegisterComponent, IntoElement)]
41pub struct Table<const COLS: usize = 3> {
42 striped: bool,
43 width: Length,
44 headers: Option<[AnyElement; COLS]>,
45 rows: TableContents<COLS>,
46}
47
48impl<const COLS: usize> Table<COLS> {
49 pub fn uniform_list(
50 id: impl Into<ElementId>,
51 row_count: usize,
52 render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<AnyElement> + 'static,
53 ) -> Self {
54 Table {
55 striped: false,
56 width: Length::Auto,
57 headers: None,
58 rows: TableContents::UniformList(UniformListData {
59 element_id: id.into(),
60 row_count: row_count,
61 render_item_fn: Box::new(render_item_fn),
62 }),
63 }
64 }
65
66 /// Create a new table with a column count equal to the
67 /// number of headers provided.
68 pub fn new() -> Self {
69 Table {
70 striped: false,
71 width: Length::Auto,
72 headers: None,
73 rows: TableContents::Vec(Vec::new()),
74 }
75 }
76
77 /// Enables row striping.
78 pub fn striped(mut self) -> Self {
79 self.striped = true;
80 self
81 }
82
83 /// Sets the width of the table.
84 pub fn width(mut self, width: impl Into<Length>) -> Self {
85 self.width = width.into();
86 self
87 }
88
89 pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
90 self.headers = Some(headers.map(IntoElement::into_any_element));
91 self
92 }
93
94 pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
95 if let Some(rows) = self.rows.rows_mut() {
96 rows.push(items.map(IntoElement::into_any_element));
97 }
98 self
99 }
100
101 pub fn render_row(&self, items: [impl IntoElement; COLS], cx: &mut App) -> AnyElement {
102 return render_row(0, items, self.rows.len(), self.striped, cx);
103 }
104
105 pub fn render_header(
106 &self,
107 headers: [impl IntoElement; COLS],
108 cx: &mut App,
109 ) -> impl IntoElement {
110 render_header(headers, cx)
111 }
112}
113
114fn base_cell_style(cx: &App) -> Div {
115 div()
116 .px_1p5()
117 .flex_1()
118 .justify_start()
119 .text_ui(cx)
120 .whitespace_nowrap()
121 .text_ellipsis()
122 .overflow_hidden()
123}
124
125pub fn render_row<const COLS: usize>(
126 row_index: usize,
127 items: [impl IntoElement; COLS],
128 row_count: usize,
129 striped: bool,
130 cx: &App,
131) -> AnyElement {
132 let is_last = row_index == row_count - 1;
133 let bg = if row_index % 2 == 1 && striped {
134 Some(cx.theme().colors().text.opacity(0.05))
135 } else {
136 None
137 };
138 div()
139 .w_full()
140 .flex()
141 .flex_row()
142 .items_center()
143 .justify_between()
144 .px_1p5()
145 .py_1()
146 .when_some(bg, |row, bg| row.bg(bg))
147 .when(!is_last, |row| {
148 row.border_b_1().border_color(cx.theme().colors().border)
149 })
150 .children(
151 items
152 .map(IntoElement::into_any_element)
153 .map(|cell| base_cell_style(cx).child(cell)),
154 )
155 .into_any_element()
156}
157
158pub fn render_header<const COLS: usize>(
159 headers: [impl IntoElement; COLS],
160 cx: &mut App,
161) -> impl IntoElement {
162 div()
163 .flex()
164 .flex_row()
165 .items_center()
166 .justify_between()
167 .w_full()
168 .p_2()
169 .border_b_1()
170 .border_color(cx.theme().colors().border)
171 .children(headers.into_iter().map(|h| {
172 base_cell_style(cx)
173 .font_weight(FontWeight::SEMIBOLD)
174 .child(h)
175 }))
176}
177
178impl<const COLS: usize> RenderOnce for Table<COLS> {
179 fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
180 // match self.ro
181 let row_count = self.rows.len();
182 div()
183 .w(self.width)
184 .overflow_hidden()
185 .when_some(self.headers.take(), |this, headers| {
186 this.child(render_header(headers, cx))
187 })
188 .map(|div| match self.rows {
189 TableContents::Vec(items) => div.children(
190 items
191 .into_iter()
192 .enumerate()
193 .map(|(index, row)| render_row(index, row, row_count, self.striped, cx)),
194 ),
195 TableContents::UniformList(uniform_list_data) => div.child(uniform_list(
196 uniform_list_data.element_id,
197 uniform_list_data.row_count,
198 uniform_list_data.render_item_fn,
199 )),
200 })
201 }
202}
203
204impl Component for Table<3> {
205 fn scope() -> ComponentScope {
206 ComponentScope::Layout
207 }
208
209 fn description() -> Option<&'static str> {
210 Some("A table component for displaying data in rows and columns with optional styling.")
211 }
212
213 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
214 Some(
215 v_flex()
216 .gap_6()
217 .children(vec![
218 example_group_with_title(
219 "Basic Tables",
220 vec![
221 single_example(
222 "Simple Table",
223 Table::new()
224 .width(px(400.))
225 .header(["Name", "Age", "City"])
226 .row(["Alice", "28", "New York"])
227 .row(["Bob", "32", "San Francisco"])
228 .row(["Charlie", "25", "London"])
229 .into_any_element(),
230 ),
231 single_example(
232 "Two Column Table",
233 Table::new()
234 .header(["Category", "Value"])
235 .width(px(300.))
236 .row(["Revenue", "$100,000"])
237 .row(["Expenses", "$75,000"])
238 .row(["Profit", "$25,000"])
239 .into_any_element(),
240 ),
241 ],
242 ),
243 example_group_with_title(
244 "Styled Tables",
245 vec![
246 single_example(
247 "Default",
248 Table::new()
249 .width(px(400.))
250 .header(["Product", "Price", "Stock"])
251 .row(["Laptop", "$999", "In Stock"])
252 .row(["Phone", "$599", "Low Stock"])
253 .row(["Tablet", "$399", "Out of Stock"])
254 .into_any_element(),
255 ),
256 single_example(
257 "Striped",
258 Table::new()
259 .width(px(400.))
260 .striped()
261 .header(["Product", "Price", "Stock"])
262 .row(["Laptop", "$999", "In Stock"])
263 .row(["Phone", "$599", "Low Stock"])
264 .row(["Tablet", "$399", "Out of Stock"])
265 .row(["Headphones", "$199", "In Stock"])
266 .into_any_element(),
267 ),
268 ],
269 ),
270 example_group_with_title(
271 "Mixed Content Table",
272 vec![single_example(
273 "Table with Elements",
274 Table::new()
275 .width(px(840.))
276 .header(["Status", "Name", "Priority", "Deadline", "Action"])
277 .row([
278 Indicator::dot().color(Color::Success).into_any_element(),
279 "Project A".into_any_element(),
280 "High".into_any_element(),
281 "2023-12-31".into_any_element(),
282 Button::new("view_a", "View")
283 .style(ButtonStyle::Filled)
284 .full_width()
285 .into_any_element(),
286 ])
287 .row([
288 Indicator::dot().color(Color::Warning).into_any_element(),
289 "Project B".into_any_element(),
290 "Medium".into_any_element(),
291 "2024-03-15".into_any_element(),
292 Button::new("view_b", "View")
293 .style(ButtonStyle::Filled)
294 .full_width()
295 .into_any_element(),
296 ])
297 .row([
298 Indicator::dot().color(Color::Error).into_any_element(),
299 "Project C".into_any_element(),
300 "Low".into_any_element(),
301 "2024-06-30".into_any_element(),
302 Button::new("view_c", "View")
303 .style(ButtonStyle::Filled)
304 .full_width()
305 .into_any_element(),
306 ])
307 .into_any_element(),
308 )],
309 ),
310 ])
311 .into_any_element(),
312 )
313 }
314}