Detailed changes
@@ -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::*;
@@ -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(
@@ -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(
@@ -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(
@@ -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()
@@ -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)),
@@ -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(),
+ ),
+ ]),
+ )],
+ ),
+ ]
+ }
+}
@@ -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)
}
@@ -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))
}