table.rs

  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}