1use crate::{Indicator, prelude::*};
2use gpui::{AnyElement, FontWeight, IntoElement, Length, div};
3
4/// A table component
5#[derive(IntoElement, RegisterComponent)]
6pub struct Table {
7 column_headers: Vec<SharedString>,
8 rows: Vec<Vec<TableCell>>,
9 column_count: usize,
10 striped: bool,
11 width: Length,
12}
13
14impl Table {
15 /// Create a new table with a column count equal to the
16 /// number of headers provided.
17 pub fn new(headers: Vec<impl Into<SharedString>>) -> Self {
18 let column_count = headers.len();
19
20 Table {
21 column_headers: headers.into_iter().map(Into::into).collect(),
22 column_count,
23 rows: Vec::new(),
24 striped: false,
25 width: Length::Auto,
26 }
27 }
28
29 /// Adds a row to the table.
30 ///
31 /// The row must have the same number of columns as the table.
32 pub fn row(mut self, items: Vec<impl Into<TableCell>>) -> Self {
33 if items.len() == self.column_count {
34 self.rows.push(items.into_iter().map(Into::into).collect());
35 } else {
36 // TODO: Log error: Row length mismatch
37 }
38 self
39 }
40
41 /// Adds multiple rows to the table.
42 ///
43 /// Each row must have the same number of columns as the table.
44 /// Rows that don't match the column count are ignored.
45 pub fn rows(mut self, rows: Vec<Vec<impl Into<TableCell>>>) -> Self {
46 for row in rows {
47 self = self.row(row);
48 }
49 self
50 }
51
52 fn base_cell_style(cx: &mut App) -> Div {
53 div()
54 .px_1p5()
55 .flex_1()
56 .justify_start()
57 .text_ui(cx)
58 .whitespace_nowrap()
59 .text_ellipsis()
60 .overflow_hidden()
61 }
62
63 /// Enables row striping.
64 pub fn striped(mut self) -> Self {
65 self.striped = true;
66 self
67 }
68
69 /// Sets the width of the table.
70 pub fn width(mut self, width: impl Into<Length>) -> Self {
71 self.width = width.into();
72 self
73 }
74}
75
76impl RenderOnce for Table {
77 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
78 let header = div()
79 .flex()
80 .flex_row()
81 .items_center()
82 .justify_between()
83 .w_full()
84 .p_2()
85 .border_b_1()
86 .border_color(cx.theme().colors().border)
87 .children(self.column_headers.into_iter().map(|h| {
88 Self::base_cell_style(cx)
89 .font_weight(FontWeight::SEMIBOLD)
90 .child(h)
91 }));
92
93 let row_count = self.rows.len();
94 let rows = self.rows.into_iter().enumerate().map(|(ix, row)| {
95 let is_last = ix == row_count - 1;
96 let bg = if ix % 2 == 1 && self.striped {
97 Some(cx.theme().colors().text.opacity(0.05))
98 } else {
99 None
100 };
101 div()
102 .w_full()
103 .flex()
104 .flex_row()
105 .items_center()
106 .justify_between()
107 .px_1p5()
108 .py_1()
109 .when_some(bg, |row, bg| row.bg(bg))
110 .when(!is_last, |row| {
111 row.border_b_1().border_color(cx.theme().colors().border)
112 })
113 .children(row.into_iter().map(|cell| match cell {
114 TableCell::String(s) => Self::base_cell_style(cx).child(s),
115 TableCell::Element(e) => Self::base_cell_style(cx).child(e),
116 }))
117 });
118
119 div()
120 .w(self.width)
121 .overflow_hidden()
122 .child(header)
123 .children(rows)
124 }
125}
126
127/// Represents a cell in a table.
128pub enum TableCell {
129 /// A cell containing a string value.
130 String(SharedString),
131 /// A cell containing a UI element.
132 Element(AnyElement),
133}
134
135/// Creates a `TableCell` containing a string value.
136pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
137 TableCell::String(s.into())
138}
139
140/// Creates a `TableCell` containing an element.
141pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
142 TableCell::Element(e.into())
143}
144
145impl<E> From<E> for TableCell
146where
147 E: Into<SharedString>,
148{
149 fn from(e: E) -> Self {
150 TableCell::String(e.into())
151 }
152}
153
154impl Component for Table {
155 fn scope() -> ComponentScope {
156 ComponentScope::Layout
157 }
158
159 fn description() -> Option<&'static str> {
160 Some("A table component for displaying data in rows and columns with optional styling.")
161 }
162
163 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
164 Some(
165 v_flex()
166 .gap_6()
167 .children(vec![
168 example_group_with_title(
169 "Basic Tables",
170 vec![
171 single_example(
172 "Simple Table",
173 Table::new(vec!["Name", "Age", "City"])
174 .width(px(400.))
175 .row(vec!["Alice", "28", "New York"])
176 .row(vec!["Bob", "32", "San Francisco"])
177 .row(vec!["Charlie", "25", "London"])
178 .into_any_element(),
179 ),
180 single_example(
181 "Two Column Table",
182 Table::new(vec!["Category", "Value"])
183 .width(px(300.))
184 .row(vec!["Revenue", "$100,000"])
185 .row(vec!["Expenses", "$75,000"])
186 .row(vec!["Profit", "$25,000"])
187 .into_any_element(),
188 ),
189 ],
190 ),
191 example_group_with_title(
192 "Styled Tables",
193 vec![
194 single_example(
195 "Default",
196 Table::new(vec!["Product", "Price", "Stock"])
197 .width(px(400.))
198 .row(vec!["Laptop", "$999", "In Stock"])
199 .row(vec!["Phone", "$599", "Low Stock"])
200 .row(vec!["Tablet", "$399", "Out of Stock"])
201 .into_any_element(),
202 ),
203 single_example(
204 "Striped",
205 Table::new(vec!["Product", "Price", "Stock"])
206 .width(px(400.))
207 .striped()
208 .row(vec!["Laptop", "$999", "In Stock"])
209 .row(vec!["Phone", "$599", "Low Stock"])
210 .row(vec!["Tablet", "$399", "Out of Stock"])
211 .row(vec!["Headphones", "$199", "In Stock"])
212 .into_any_element(),
213 ),
214 ],
215 ),
216 example_group_with_title(
217 "Mixed Content Table",
218 vec![single_example(
219 "Table with Elements",
220 Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
221 .width(px(840.))
222 .row(vec![
223 element_cell(
224 Indicator::dot().color(Color::Success).into_any_element(),
225 ),
226 string_cell("Project A"),
227 string_cell("High"),
228 string_cell("2023-12-31"),
229 element_cell(
230 Button::new("view_a", "View")
231 .style(ButtonStyle::Filled)
232 .full_width()
233 .into_any_element(),
234 ),
235 ])
236 .row(vec![
237 element_cell(
238 Indicator::dot().color(Color::Warning).into_any_element(),
239 ),
240 string_cell("Project B"),
241 string_cell("Medium"),
242 string_cell("2024-03-15"),
243 element_cell(
244 Button::new("view_b", "View")
245 .style(ButtonStyle::Filled)
246 .full_width()
247 .into_any_element(),
248 ),
249 ])
250 .row(vec![
251 element_cell(
252 Indicator::dot().color(Color::Error).into_any_element(),
253 ),
254 string_cell("Project C"),
255 string_cell("Low"),
256 string_cell("2024-06-30"),
257 element_cell(
258 Button::new("view_c", "View")
259 .style(ButtonStyle::Filled)
260 .full_width()
261 .into_any_element(),
262 ),
263 ])
264 .into_any_element(),
265 )],
266 ),
267 ])
268 .into_any_element(),
269 )
270 }
271}