table.rs

  1use crate::{prelude::*, Indicator};
  2use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
  3
  4/// A table component
  5#[derive(IntoElement)]
  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 ComponentPreview for Table {
155    fn description() -> impl Into<Option<&'static str>> {
156        "Used for showing tabular data. Tables may show both text and elements in their cells."
157    }
158
159    fn example_label_side() -> ExampleLabelSide {
160        ExampleLabelSide::Top
161    }
162
163    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
164        vec![
165            example_group(vec![
166                single_example(
167                    "Simple Table",
168                    Table::new(vec!["Name", "Age", "City"])
169                        .width(px(400.))
170                        .row(vec!["Alice", "28", "New York"])
171                        .row(vec!["Bob", "32", "San Francisco"])
172                        .row(vec!["Charlie", "25", "London"]),
173                ),
174                single_example(
175                    "Two Column Table",
176                    Table::new(vec!["Category", "Value"])
177                        .width(px(300.))
178                        .row(vec!["Revenue", "$100,000"])
179                        .row(vec!["Expenses", "$75,000"])
180                        .row(vec!["Profit", "$25,000"]),
181                ),
182            ]),
183            example_group(vec![single_example(
184                "Striped Table",
185                Table::new(vec!["Product", "Price", "Stock"])
186                    .width(px(600.))
187                    .striped()
188                    .row(vec!["Laptop", "$999", "In Stock"])
189                    .row(vec!["Phone", "$599", "Low Stock"])
190                    .row(vec!["Tablet", "$399", "Out of Stock"])
191                    .row(vec!["Headphones", "$199", "In Stock"]),
192            )]),
193            example_group_with_title(
194                "Mixed Content Table",
195                vec![single_example(
196                    "Table with Elements",
197                    Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
198                        .width(px(840.))
199                        .row(vec![
200                            element_cell(Indicator::dot().color(Color::Success).into_any_element()),
201                            string_cell("Project A"),
202                            string_cell("High"),
203                            string_cell("2023-12-31"),
204                            element_cell(
205                                Button::new("view_a", "View")
206                                    .style(ButtonStyle::Filled)
207                                    .full_width()
208                                    .into_any_element(),
209                            ),
210                        ])
211                        .row(vec![
212                            element_cell(Indicator::dot().color(Color::Warning).into_any_element()),
213                            string_cell("Project B"),
214                            string_cell("Medium"),
215                            string_cell("2024-03-15"),
216                            element_cell(
217                                Button::new("view_b", "View")
218                                    .style(ButtonStyle::Filled)
219                                    .full_width()
220                                    .into_any_element(),
221                            ),
222                        ])
223                        .row(vec![
224                            element_cell(Indicator::dot().color(Color::Error).into_any_element()),
225                            string_cell("Project C"),
226                            string_cell("Low"),
227                            string_cell("2024-06-30"),
228                            element_cell(
229                                Button::new("view_c", "View")
230                                    .style(ButtonStyle::Filled)
231                                    .full_width()
232                                    .into_any_element(),
233                            ),
234                        ]),
235                )],
236            ),
237        ]
238    }
239}