table.rs

  1use crate::{Indicator, prelude::*};
  2use gpui::{AnyElement, FontWeight, IntoElement, Length, div};
  3
  4/// A table component
  5#[derive(IntoElement, IntoComponent)]
  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
154// View this component preview using `workspace: open component-preview`
155impl ComponentPreview for Table {
156    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
157        v_flex()
158            .gap_6()
159            .children(vec![
160                example_group_with_title(
161                    "Basic Tables",
162                    vec![
163                        single_example(
164                            "Simple Table",
165                            Table::new(vec!["Name", "Age", "City"])
166                                .width(px(400.))
167                                .row(vec!["Alice", "28", "New York"])
168                                .row(vec!["Bob", "32", "San Francisco"])
169                                .row(vec!["Charlie", "25", "London"])
170                                .into_any_element(),
171                        ),
172                        single_example(
173                            "Two Column Table",
174                            Table::new(vec!["Category", "Value"])
175                                .width(px(300.))
176                                .row(vec!["Revenue", "$100,000"])
177                                .row(vec!["Expenses", "$75,000"])
178                                .row(vec!["Profit", "$25,000"])
179                                .into_any_element(),
180                        ),
181                    ],
182                ),
183                example_group_with_title(
184                    "Styled Tables",
185                    vec![
186                        single_example(
187                            "Default",
188                            Table::new(vec!["Product", "Price", "Stock"])
189                                .width(px(400.))
190                                .row(vec!["Laptop", "$999", "In Stock"])
191                                .row(vec!["Phone", "$599", "Low Stock"])
192                                .row(vec!["Tablet", "$399", "Out of Stock"])
193                                .into_any_element(),
194                        ),
195                        single_example(
196                            "Striped",
197                            Table::new(vec!["Product", "Price", "Stock"])
198                                .width(px(400.))
199                                .striped()
200                                .row(vec!["Laptop", "$999", "In Stock"])
201                                .row(vec!["Phone", "$599", "Low Stock"])
202                                .row(vec!["Tablet", "$399", "Out of Stock"])
203                                .row(vec!["Headphones", "$199", "In Stock"])
204                                .into_any_element(),
205                        ),
206                    ],
207                ),
208                example_group_with_title(
209                    "Mixed Content Table",
210                    vec![single_example(
211                        "Table with Elements",
212                        Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
213                            .width(px(840.))
214                            .row(vec![
215                                element_cell(
216                                    Indicator::dot().color(Color::Success).into_any_element(),
217                                ),
218                                string_cell("Project A"),
219                                string_cell("High"),
220                                string_cell("2023-12-31"),
221                                element_cell(
222                                    Button::new("view_a", "View")
223                                        .style(ButtonStyle::Filled)
224                                        .full_width()
225                                        .into_any_element(),
226                                ),
227                            ])
228                            .row(vec![
229                                element_cell(
230                                    Indicator::dot().color(Color::Warning).into_any_element(),
231                                ),
232                                string_cell("Project B"),
233                                string_cell("Medium"),
234                                string_cell("2024-03-15"),
235                                element_cell(
236                                    Button::new("view_b", "View")
237                                        .style(ButtonStyle::Filled)
238                                        .full_width()
239                                        .into_any_element(),
240                                ),
241                            ])
242                            .row(vec![
243                                element_cell(
244                                    Indicator::dot().color(Color::Error).into_any_element(),
245                                ),
246                                string_cell("Project C"),
247                                string_cell("Low"),
248                                string_cell("2024-06-30"),
249                                element_cell(
250                                    Button::new("view_c", "View")
251                                        .style(ButtonStyle::Filled)
252                                        .full_width()
253                                        .into_any_element(),
254                                ),
255                            ])
256                            .into_any_element(),
257                    )],
258                ),
259            ])
260            .into_any_element()
261    }
262}