use crate::{prelude::*, Indicator};use gpui::{div, AnyElement, FontWeight, IntoElement, Length}; /// A table component#[derive(IntoElement, IntoComponent)]pub struct Table { column_headers: Vec<SharedString>, rows: Vec<Vec<TableCell>>, column_count: usize, striped: bool, width: Length,} impl Table { /// Create a new table with a column count equal to the /// number of headers provided. pub fn new(headers: Vec<impl Into<SharedString>>) -> Self { let column_count = headers.len(); Table { column_headers: headers.into_iter().map(Into::into).collect(), column_count, rows: Vec::new(), striped: false, width: Length::Auto, } } /// Adds a row to the table. /// /// The row must have the same number of columns as the table. pub fn row(mut self, items: Vec<impl Into<TableCell>>) -> Self { if items.len() == self.column_count { self.rows.push(items.into_iter().map(Into::into).collect()); } else { // TODO: Log error: Row length mismatch } self } /// Adds multiple rows to the table. /// /// Each row must have the same number of columns as the table. /// Rows that don't match the column count are ignored. pub fn rows(mut self, rows: Vec<Vec<impl Into<TableCell>>>) -> Self { for row in rows { self = self.row(row); } self } fn base_cell_style(cx: &mut App) -> Div { div() .px_1p5() .flex_1() .justify_start() .text_ui(cx) .whitespace_nowrap() .text_ellipsis() .overflow_hidden() } /// Enables row striping. pub fn striped(mut self) -> Self { self.striped = true; self } /// Sets the width of the table. pub fn width(mut self, width: impl Into<Length>) -> Self { self.width = width.into(); self }} impl RenderOnce for Table { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let header = div() .flex() .flex_row() .items_center() .justify_between() .w_full() .p_2() .border_b_1() .border_color(cx.theme().colors().border) .children(self.column_headers.into_iter().map(|h| { Self::base_cell_style(cx) .font_weight(FontWeight::SEMIBOLD) .child(h) })); let row_count = self.rows.len(); let rows = self.rows.into_iter().enumerate().map(|(ix, row)| { let is_last = ix == row_count - 1; let bg = if ix % 2 == 1 && self.striped { Some(cx.theme().colors().text.opacity(0.05)) } else { None }; div() .w_full() .flex() .flex_row() .items_center() .justify_between() .px_1p5() .py_1() .when_some(bg, |row, bg| row.bg(bg)) .when(!is_last, |row| { row.border_b_1().border_color(cx.theme().colors().border) }) .children(row.into_iter().map(|cell| match cell { TableCell::String(s) => Self::base_cell_style(cx).child(s), TableCell::Element(e) => Self::base_cell_style(cx).child(e), })) }); div() .w(self.width) .overflow_hidden() .child(header) .children(rows) }} /// Represents a cell in a table.pub enum TableCell { /// A cell containing a string value. String(SharedString), /// A cell containing a UI element. Element(AnyElement),} /// Creates a `TableCell` containing a string value.pub fn string_cell(s: impl Into<SharedString>) -> TableCell { TableCell::String(s.into())} /// Creates a `TableCell` containing an element.pub fn element_cell(e: impl Into<AnyElement>) -> TableCell { TableCell::Element(e.into())} impl<E> From<E> for TableCellwhere E: Into<SharedString>,{ fn from(e: E) -> Self { TableCell::String(e.into()) }} // View this component preview using `workspace: open component-preview`impl ComponentPreview for Table { fn preview(_window: &mut Window, _cx: &App) -> AnyElement { v_flex() .gap_6() .children(vec![ example_group_with_title( "Basic Tables", vec![ single_example( "Simple Table", Table::new(vec!["Name", "Age", "City"]) .width(px(400.)) .row(vec!["Alice", "28", "New York"]) .row(vec!["Bob", "32", "San Francisco"]) .row(vec!["Charlie", "25", "London"]) .into_any_element(), ), single_example( "Two Column Table", Table::new(vec!["Category", "Value"]) .width(px(300.)) .row(vec!["Revenue", "$100,000"]) .row(vec!["Expenses", "$75,000"]) .row(vec!["Profit", "$25,000"]) .into_any_element(), ), ], ), example_group_with_title( "Styled Tables", vec![ single_example( "Default", Table::new(vec!["Product", "Price", "Stock"]) .width(px(400.)) .row(vec!["Laptop", "$999", "In Stock"]) .row(vec!["Phone", "$599", "Low Stock"]) .row(vec!["Tablet", "$399", "Out of Stock"]) .into_any_element(), ), single_example( "Striped", Table::new(vec!["Product", "Price", "Stock"]) .width(px(400.)) .striped() .row(vec!["Laptop", "$999", "In Stock"]) .row(vec!["Phone", "$599", "Low Stock"]) .row(vec!["Tablet", "$399", "Out of Stock"]) .row(vec!["Headphones", "$199", "In Stock"]) .into_any_element(), ), ], ), example_group_with_title( "Mixed Content Table", vec![single_example( "Table with Elements", Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"]) .width(px(840.)) .row(vec![ element_cell( Indicator::dot().color(Color::Success).into_any_element(), ), string_cell("Project A"), string_cell("High"), string_cell("2023-12-31"), element_cell( Button::new("view_a", "View") .style(ButtonStyle::Filled) .full_width() .into_any_element(), ), ]) .row(vec![ element_cell( Indicator::dot().color(Color::Warning).into_any_element(), ), string_cell("Project B"), string_cell("Medium"), string_cell("2024-03-15"), element_cell( Button::new("view_b", "View") .style(ButtonStyle::Filled) .full_width() .into_any_element(), ), ]) .row(vec![ element_cell( Indicator::dot().color(Color::Error).into_any_element(), ), string_cell("Project C"), string_cell("Low"), string_cell("2024-06-30"), element_cell( Button::new("view_c", "View") .style(ButtonStyle::Filled) .full_width() .into_any_element(), ), ]) .into_any_element(), )], ), ]) .into_any_element() }}
use crate::{prelude::*, Indicator};
use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
/// A table component
#[derive(IntoElement, IntoComponent)]
pub struct Table {
column_headers: Vec<SharedString>,
rows: Vec<Vec<TableCell>>,
column_count: usize,
striped: bool,
width: Length,
}
impl Table {
/// Create a new table with a column count equal to the
/// number of headers provided.
pub fn new(headers: Vec<impl Into<SharedString>>) -> Self {
let column_count = headers.len();
Table {
column_headers: headers.into_iter().map(Into::into).collect(),
column_count,
rows: Vec::new(),
striped: false,
width: Length::Auto,
}
}
/// Adds a row to the table.
///
/// The row must have the same number of columns as the table.
pub fn row(mut self, items: Vec<impl Into<TableCell>>) -> Self {
if items.len() == self.column_count {
self.rows.push(items.into_iter().map(Into::into).collect());
} else {
// TODO: Log error: Row length mismatch
}
self
}
/// Adds multiple rows to the table.
///
/// Each row must have the same number of columns as the table.
/// Rows that don't match the column count are ignored.
pub fn rows(mut self, rows: Vec<Vec<impl Into<TableCell>>>) -> Self {
for row in rows {
self = self.row(row);
}
self
}
fn base_cell_style(cx: &mut App) -> Div {
div()
.px_1p5()
.flex_1()
.justify_start()
.text_ui(cx)
.whitespace_nowrap()
.text_ellipsis()
.overflow_hidden()
}
/// Enables row striping.
pub fn striped(mut self) -> Self {
self.striped = true;
self
}
/// Sets the width of the table.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
}
impl RenderOnce for Table {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let header = div()
.flex()
.flex_row()
.items_center()
.justify_between()
.w_full()
.p_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.children(self.column_headers.into_iter().map(|h| {
Self::base_cell_style(cx)
.font_weight(FontWeight::SEMIBOLD)
.child(h)
}));
let row_count = self.rows.len();
let rows = self.rows.into_iter().enumerate().map(|(ix, row)| {
let is_last = ix == row_count - 1;
let bg = if ix % 2 == 1 && self.striped {
Some(cx.theme().colors().text.opacity(0.05))
} else {
None
};
div()
.w_full()
.flex()
.flex_row()
.items_center()
.justify_between()
.px_1p5()
.py_1()
.when_some(bg, |row, bg| row.bg(bg))
.when(!is_last, |row| {
row.border_b_1().border_color(cx.theme().colors().border)
})
.children(row.into_iter().map(|cell| match cell {
TableCell::String(s) => Self::base_cell_style(cx).child(s),
TableCell::Element(e) => Self::base_cell_style(cx).child(e),
}))
});
div()
.w(self.width)
.overflow_hidden()
.child(header)
.children(rows)
}
}
/// Represents a cell in a table.
pub enum TableCell {
/// A cell containing a string value.
String(SharedString),
/// A cell containing a UI element.
Element(AnyElement),
}
/// Creates a `TableCell` containing a string value.
pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
TableCell::String(s.into())
}
/// Creates a `TableCell` containing an element.
pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
TableCell::Element(e.into())
}
impl<E> From<E> for TableCell
where
E: Into<SharedString>,
{
fn from(e: E) -> Self {
TableCell::String(e.into())
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Table {
fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Tables",
vec![
single_example(
"Simple Table",
Table::new(vec!["Name", "Age", "City"])
.width(px(400.))
.row(vec!["Alice", "28", "New York"])
.row(vec!["Bob", "32", "San Francisco"])
.row(vec!["Charlie", "25", "London"])
.into_any_element(),
),
single_example(
"Two Column Table",
Table::new(vec!["Category", "Value"])
.width(px(300.))
.row(vec!["Revenue", "$100,000"])
.row(vec!["Expenses", "$75,000"])
.row(vec!["Profit", "$25,000"])
.into_any_element(),
),
],
),
example_group_with_title(
"Styled Tables",
vec![
single_example(
"Default",
Table::new(vec!["Product", "Price", "Stock"])
.width(px(400.))
.row(vec!["Laptop", "$999", "In Stock"])
.row(vec!["Phone", "$599", "Low Stock"])
.row(vec!["Tablet", "$399", "Out of Stock"])
.into_any_element(),
),
single_example(
"Striped",
Table::new(vec!["Product", "Price", "Stock"])
.width(px(400.))
.striped()
.row(vec!["Laptop", "$999", "In Stock"])
.row(vec!["Phone", "$599", "Low Stock"])
.row(vec!["Tablet", "$399", "Out of Stock"])
.row(vec!["Headphones", "$199", "In Stock"])
.into_any_element(),
),
],
),
example_group_with_title(
"Mixed Content Table",
vec![single_example(
"Table with Elements",
Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
.width(px(840.))
.row(vec![
element_cell(
Indicator::dot().color(Color::Success).into_any_element(),
),
string_cell("Project A"),
string_cell("High"),
string_cell("2023-12-31"),
element_cell(
Button::new("view_a", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
),
])
.row(vec![
element_cell(
Indicator::dot().color(Color::Warning).into_any_element(),
),
string_cell("Project B"),
string_cell("Medium"),
string_cell("2024-03-15"),
element_cell(
Button::new("view_b", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
),
])
.row(vec![
element_cell(
Indicator::dot().color(Color::Error).into_any_element(),
),
string_cell("Project C"),
string_cell("Low"),
string_cell("2024-06-30"),
element_cell(
Button::new("view_c", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
),
])
.into_any_element(),
)],
),
])
.into_any_element()
}
}