diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 08ebe1a771..5b2c7d6172 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -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::*; diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index ea4d48de14..b584231018 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -445,7 +445,7 @@ impl ComponentPreview for Button { fn examples() -> Vec> { 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( diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs index 64fb709fc3..4245c0dba2 100644 --- a/crates/ui/src/components/checkbox.rs +++ b/crates/ui/src/components/checkbox.rs @@ -123,7 +123,7 @@ impl ComponentPreview for Checkbox { fn examples() -> Vec> { 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( diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index 99d5232c79..5d406f67c7 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -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( diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index fbd723cd27..5875c8891c 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -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() diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs index d910542bb3..8ce075d228 100644 --- a/crates/ui/src/components/indicator.rs +++ b/crates/ui/src/components/indicator.rs @@ -91,7 +91,7 @@ impl ComponentPreview for Indicator { fn examples() -> Vec> { 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)), diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs new file mode 100644 index 0000000000..59273cce12 --- /dev/null +++ b/crates/ui/src/components/table.rs @@ -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, + rows: Vec>, + 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>) -> 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>) -> 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>>) -> 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) -> 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) -> TableCell { + TableCell::String(s.into()) +} + +/// Creates a `TableCell` containing an element. +pub fn element_cell(e: impl Into) -> TableCell { + TableCell::Element(e.into()) +} + +impl From for TableCell +where + E: Into, +{ + fn from(e: E) -> Self { + TableCell::String(e.into()) + } +} + +impl ComponentPreview for Table { + fn description() -> impl Into> { + "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> { + 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(), + ), + ]), + )], + ), + ] + } +} diff --git a/crates/ui/src/traits/component_preview.rs b/crates/ui/src/traits/component_preview.rs index d767b734b1..12408ff49e 100644 --- a/crates/ui/src/traits/component_preview.rs +++ b/crates/ui/src/traits/component_preview.rs @@ -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>; fn component_previews() -> Vec { @@ -62,7 +80,9 @@ pub trait ComponentPreview: IntoElement { fn render_example_group(group: ComponentExampleGroup) -> 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) -> 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 ComponentExample { /// A group of component examples. pub struct ComponentExampleGroup { - pub title: SharedString, + pub title: Option, pub examples: Vec>, } impl ComponentExampleGroup { /// Create a new group of examples with the given title. - pub fn new(title: impl Into, examples: Vec>) -> Self { + pub fn new(examples: Vec>) -> Self { Self { - title: title.into(), + title: None, + examples, + } + } + + pub fn with_title(title: impl Into, examples: Vec>) -> Self { + Self { + title: Some(title.into()), examples, } } @@ -122,10 +157,15 @@ pub fn single_example(variant_name: impl Into, example: T) -> C ComponentExample::new(variant_name, example) } -/// Create a group of examples -pub fn example_group( +/// Create a group of examples without a title +pub fn example_group(examples: Vec>) -> ComponentExampleGroup { + ComponentExampleGroup::new(examples) +} + +/// Create a group of examples with a title +pub fn example_group_with_title( title: impl Into, examples: Vec>, ) -> ComponentExampleGroup { - ComponentExampleGroup::new(title, examples) + ComponentExampleGroup::with_title(title, examples) } diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 7361bb3adc..f7f3c9d34e 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -1,11 +1,11 @@ #![allow(unused, dead_code)] -use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla}; +use gpui::{actions, hsla, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla}; use strum::IntoEnumIterator; use theme::all_theme_colors; use ui::{ - prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar, - AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox, ElevationIndex, - Facepile, Indicator, TintColor, Tooltip, + element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus, + Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, + Checkbox, ElevationIndex, Facepile, Indicator, Table, TintColor, Tooltip, }; use crate::{Item, Workspace}; @@ -514,6 +514,7 @@ impl ThemePreview { .child(Button::render_component_previews(cx)) .child(Indicator::render_component_previews(cx)) .child(Icon::render_component_previews(cx)) + .child(Table::render_component_previews(cx)) .child(self.render_avatars(cx)) .child(self.render_buttons(layer, cx)) }