Add ui::table (#20447)

This PR adds the `ui::Table` component.

It has a rather simple API, but cells can contain either strings or
elements, allowing for some complex uses.

Example usage:

```rust
Table::new(vec!["Product", "Price", "Stock"])
    .width(px(600.))
    .striped()
    .row(vec!["Laptop", "$999", "In Stock"])
    .row(vec!["Phone", "$599", "Low Stock"])
    .row(vec!["Tablet", "$399", "Out of Stock"])
```

For more complex use cases, the table supports mixed content:

```rust
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()),
    ])
    // ... more rows
```

Preview:

![CleanShot 2024-11-08 at 20 53
04@2x](https://github.com/user-attachments/assets/b39122f0-a29b-423b-8e24-86ab4c42bac2)

This component is pretty basic, improvements are welcome!

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2024-11-08 21:10:15 -05:00 committed by GitHub
parent 1f974d074e
commit 31a6ee0229
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 306 additions and 24 deletions

View file

@ -26,6 +26,7 @@ mod settings_group;
mod stack;
mod tab;
mod tab_bar;
mod table;
mod tool_strip;
mod tooltip;
@ -60,6 +61,7 @@ pub use settings_group::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
pub use table::*;
pub use tool_strip::*;
pub use tooltip::*;

View file

@ -445,7 +445,7 @@ impl ComponentPreview for Button {
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(
example_group_with_title(
"Styles",
vec![
single_example("Default", Button::new("default", "Default")),
@ -463,7 +463,7 @@ impl ComponentPreview for Button {
),
],
),
example_group(
example_group_with_title(
"Tinted",
vec![
single_example(
@ -488,7 +488,7 @@ impl ComponentPreview for Button {
),
],
),
example_group(
example_group_with_title(
"States",
vec![
single_example("Default", Button::new("default_state", "Default")),
@ -502,7 +502,7 @@ impl ComponentPreview for Button {
),
],
),
example_group(
example_group_with_title(
"With Icons",
vec![
single_example(

View file

@ -123,7 +123,7 @@ impl ComponentPreview for Checkbox {
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(
example_group_with_title(
"Default",
vec![
single_example(
@ -140,7 +140,7 @@ impl ComponentPreview for Checkbox {
),
],
),
example_group(
example_group_with_title(
"Disabled",
vec![
single_example(

View file

@ -83,7 +83,7 @@ impl ComponentPreview for Facepile {
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
];
vec![example_group(
vec![example_group_with_title(
"Examples",
vec![
single_example(

View file

@ -6,7 +6,7 @@ use ui_macros::DerivePathStr;
use crate::{
prelude::*,
traits::component_preview::{example_group, ComponentExample, ComponentPreview},
traits::component_preview::{ComponentExample, ComponentPreview},
Indicator,
};
@ -510,7 +510,7 @@ impl ComponentPreview for Icon {
IconName::ArrowCircle,
];
vec![example_group(
vec![example_group_with_title(
"Arrow Icons",
arrow_icons
.into_iter()

View file

@ -91,7 +91,7 @@ impl ComponentPreview for Indicator {
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(
example_group_with_title(
"Types",
vec![
single_example("Dot", Indicator::dot().color(Color::Info)),
@ -102,7 +102,7 @@ impl ComponentPreview for Indicator {
),
],
),
example_group(
example_group_with_title(
"Examples",
vec![
single_example("Info", Indicator::dot().color(Color::Info)),

View file

@ -0,0 +1,239 @@
use crate::{prelude::*, Indicator};
use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
/// A table component
#[derive(IntoElement)]
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: &WindowContext) -> 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, cx: &mut WindowContext) -> 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 description() -> impl Into<Option<&'static str>> {
"Used for showing tabular data. Tables may show both text and elements in their cells."
}
fn example_label_side() -> ExampleLabelSide {
ExampleLabelSide::Top
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(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"]),
),
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"]),
),
]),
example_group(vec![single_example(
"Striped Table",
Table::new(vec!["Product", "Price", "Stock"])
.width(px(600.))
.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"]),
)]),
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(),
),
]),
)],
),
]
}
}

View file

@ -2,6 +2,20 @@
use crate::prelude::*;
use gpui::{AnyElement, SharedString};
/// Which side of the preview to show labels on
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExampleLabelSide {
/// Left side
Left,
/// Right side
Right,
/// Top side
Top,
#[default]
/// Bottom side
Bottom,
}
/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
pub trait ComponentPreview: IntoElement {
fn title() -> &'static str {
@ -12,6 +26,10 @@ pub trait ComponentPreview: IntoElement {
None
}
fn example_label_side() -> ExampleLabelSide {
ExampleLabelSide::default()
}
fn examples() -> Vec<ComponentExampleGroup<Self>>;
fn component_previews() -> Vec<AnyElement> {
@ -62,7 +80,9 @@ pub trait ComponentPreview: IntoElement {
fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
v_flex()
.gap_2()
.child(Label::new(group.title).size(LabelSize::Small))
.when_some(group.title, |this, title| {
this.child(Headline::new(title).size(HeadlineSize::Small))
})
.child(
h_flex()
.gap_6()
@ -73,8 +93,16 @@ pub trait ComponentPreview: IntoElement {
}
fn render_example(example: ComponentExample<Self>) -> AnyElement {
v_flex()
.gap_1()
let base = div().flex();
let base = match Self::example_label_side() {
ExampleLabelSide::Right => base.flex_row(),
ExampleLabelSide::Left => base.flex_row_reverse(),
ExampleLabelSide::Bottom => base.flex_col(),
ExampleLabelSide::Top => base.flex_col_reverse(),
};
base.gap_1()
.child(example.element)
.child(
Label::new(example.variant_name)
@ -103,15 +131,22 @@ impl<T> ComponentExample<T> {
/// A group of component examples.
pub struct ComponentExampleGroup<T> {
pub title: SharedString,
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample<T>>,
}
impl<T> ComponentExampleGroup<T> {
/// Create a new group of examples with the given title.
pub fn new(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
Self {
title: title.into(),
title: None,
examples,
}
}
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
Self {
title: Some(title.into()),
examples,
}
}
@ -122,10 +157,15 @@ pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> C
ComponentExample::new(variant_name, example)
}
/// Create a group of examples
pub fn example_group<T>(
/// Create a group of examples without a title
pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
ComponentExampleGroup::new(examples)
}
/// Create a group of examples with a title
pub fn example_group_with_title<T>(
title: impl Into<SharedString>,
examples: Vec<ComponentExample<T>>,
) -> ComponentExampleGroup<T> {
ComponentExampleGroup::new(title, examples)
ComponentExampleGroup::with_title(title, examples)
}