This PR formalizes design components with the Component and ComponentPreview traits. You can open the preview UI with `workspace: open component preview`. Component previews no longer need to return `Self` allowing for more complex previews, and previews of components like `ui::Tooltip` that supplement other components rather than are rendered by default. `cargo-machete` incorrectly identifies `linkme` as an unused dep on crates that have components deriving `IntoComponent`, so you may need to add this to that crate's `Cargo.toml`: ```toml # cargo-machete doesn't understand that linkme is used in the component macro [package.metadata.cargo-machete] ignored = ["linkme"] ``` Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
262 lines
9.4 KiB
Rust
262 lines
9.4 KiB
Rust
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())
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|