ZIm/crates/ui/src/components/table.rs
Nate Butler 9d8a163f5b
git_ui: New panel design (#25821)
This PR updates the ui of the git panel. It removes the header from the
panel and unifies the repository, branch and commit controls in the
bottom section.

It also adds a secondary menu to the primary button giving access to a
variety of actions for managing local and remote changes:

![CleanShot 2025-02-28 at 12 18
15@2x](https://github.com/user-attachments/assets/0260c122-405f-46fc-8cc8-d6beac782b9d)

Known issues (will be fixed in a later pr)
- Spinner showing git operation progress was removed, will be re-added
- Clicking expand with the panel editor focused will commit (due to
shared action name. Already tracked)

Before | After

![CleanShot 2025-02-28 at 12 22
18@2x](https://github.com/user-attachments/assets/4c1e4ac9-b975-487f-bf4e-8815a8da4f4f)

(Also adds `component`, `linkme` to cargo-machete ignore as they are
used in the `IntoComponent` proc-macro and will always be incorrectly
flagged as unused)

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-02-28 20:00:39 +00:00

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())
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Table {
fn preview(_window: &mut Window, _cx: &mut 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()
}
}