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