table.rs

  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}