diff --git a/Cargo.lock b/Cargo.lock index 5c0184192121b354d51ab5b702485fa1521448a9..bb03cba07c141de700f8a43e9d03effc9853657b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14567,6 +14567,7 @@ name = "settings_ui" version = "0.1.0" dependencies = [ "command_palette_hooks", + "component", "db", "editor", "feature_flags", diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 904bea5b5ce2afc70520744ea0e1e4367b8bc5b4..2af699938d00bbb878b12d6b585aed95f97f3072 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -13,6 +13,7 @@ path = "src/settings_ui.rs" [dependencies] command_palette_hooks.workspace = true +component.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index e0ce07234c09b322036e803e4d43c7a15ef1be15..5c7d79f5597edd27e1db4704df4fb780a3b6af4b 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -15,7 +15,7 @@ use ui::{ }; use workspace::{Item, SerializableItem, Workspace, register_serializable_item}; -use crate::keybindings::persistence::KEYBINDING_EDITORS; +use crate::{keybindings::persistence::KEYBINDING_EDITORS, ui_components::table::Table}; actions!(zed, [OpenKeymapEditor]); @@ -391,7 +391,8 @@ impl Render for KeymapEditor { px(0.) }; - let table = Table::new(self.processed_bindings.len()); + let row_count = self.processed_bindings.len(); + let table = Table::new(row_count); let theme = cx.theme(); let headers = ["Command", "Keystrokes", "Context"].map(Into::into); @@ -412,7 +413,6 @@ impl Render for KeymapEditor { })) .child( table - .render() .h_full() .v_flex() .child(table.render_header(headers, cx)) @@ -424,10 +424,9 @@ impl Render for KeymapEditor { .overflow_hidden() .child( uniform_list( - cx.entity(), "keybindings", - table.row_count, - move |this, range, _, cx| { + row_count, + cx.processor(move |this, range, _, cx| { range .map(|index| { let binding = &this.processed_bindings[index]; @@ -443,7 +442,7 @@ impl Render for KeymapEditor { table.render_row(index, row, cx) }) .collect() - }, + }), ) .size_full() .flex_grow() @@ -518,123 +517,6 @@ impl Render for KeymapEditor { } } -/// A table component -#[derive(Clone, Copy)] -pub struct Table { - striped: bool, - width: Length, - row_count: usize, -} - -impl Table { - /// Create a new table with a column count equal to the - /// number of headers provided. - pub fn new(row_count: usize) -> Self { - Table { - striped: false, - width: Length::Auto, - row_count, - } - } - - /// 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 - } - - fn base_cell_style(cx: &App) -> Div { - div() - .px_1p5() - .flex_1() - .justify_start() - .text_ui(cx) - .whitespace_nowrap() - .text_ellipsis() - .overflow_hidden() - } - - pub fn render_row(&self, row_index: usize, items: [TableCell; COLS], cx: &App) -> AnyElement { - let is_last = row_index == self.row_count - 1; - let bg = if row_index % 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(items.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), - })) - .into_any_element() - } - - fn render_header(&self, headers: [SharedString; COLS], cx: &mut App) -> impl IntoElement { - div() - .flex() - .flex_row() - .items_center() - .justify_between() - .w_full() - .p_2() - .border_b_1() - .border_color(cx.theme().colors().border) - .children(headers.into_iter().map(|h| { - Self::base_cell_style(cx) - .font_weight(FontWeight::SEMIBOLD) - .child(h.clone()) - })) - } - - fn render(&self) -> Div { - div().w(self.width).overflow_hidden() - } -} - -/// 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 SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 122f801d6941fe467f29798afd2704ee94dadef9..b3fb10c5e6848c80c55117dadc750db7b70a58ea 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -21,6 +21,7 @@ use workspace::{Workspace, with_active_or_new_workspace}; use crate::appearance_settings_controls::AppearanceSettingsControls; pub mod keybindings; +pub mod ui_components; pub struct SettingsUiFeatureFlag; diff --git a/crates/settings_ui/src/ui_components/mod.rs b/crates/settings_ui/src/ui_components/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..13971b0a5df8e3b188de1df94faab3df94aa86da --- /dev/null +++ b/crates/settings_ui/src/ui_components/mod.rs @@ -0,0 +1 @@ +pub mod table; diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs new file mode 100644 index 0000000000000000000000000000000000000000..96a8aa41a7fdda1ec956d1bda254bfea54e33434 --- /dev/null +++ b/crates/settings_ui/src/ui_components/table.rs @@ -0,0 +1,314 @@ +use std::ops::Range; + +use db::smol::stream::iter; +use gpui::{Entity, FontWeight, Length, uniform_list}; +use ui::{ + ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, + ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, IntoElement, + ParentElement, RegisterComponent, RenderOnce, Styled, StyledTypography, Window, div, + example_group_with_title, px, single_example, v_flex, +}; + +struct UniformListData { + render_item_fn: Box, &mut Window, &mut App) -> Vec>, + element_id: ElementId, + row_count: usize, +} + +enum TableContents { + Vec(Vec<[AnyElement; COLS]>), + UniformList(UniformListData), +} + +impl TableContents { + fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> { + match self { + TableContents::Vec(rows) => Some(rows), + TableContents::UniformList(_) => None, + } + } + + fn len(&self) -> usize { + match self { + TableContents::Vec(rows) => rows.len(), + TableContents::UniformList(data) => data.row_count, + } + } +} + +/// A table component +#[derive(RegisterComponent, IntoElement)] +pub struct Table { + striped: bool, + width: Length, + headers: Option<[AnyElement; COLS]>, + rows: TableContents, +} + +impl Table { + pub fn uniform_list( + id: impl Into, + row_count: usize, + render_item_fn: impl Fn(Range, &mut Window, &mut App) -> Vec + 'static, + ) -> Self { + Table { + striped: false, + width: Length::Auto, + headers: None, + rows: TableContents::UniformList(UniformListData { + element_id: id.into(), + row_count: row_count, + render_item_fn: Box::new(render_item_fn), + }), + } + } + + /// Create a new table with a column count equal to the + /// number of headers provided. + pub fn new() -> Self { + Table { + striped: false, + width: Length::Auto, + headers: None, + rows: TableContents::Vec(Vec::new()), + } + } + + /// 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 + } + + pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self { + self.headers = Some(headers.map(IntoElement::into_any_element)); + self + } + + pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self { + if let Some(rows) = self.rows.rows_mut() { + rows.push(items.map(IntoElement::into_any_element)); + } + self + } + + pub fn render_row(&self, items: [impl IntoElement; COLS], cx: &mut App) -> AnyElement { + return render_row(0, items, self.rows.len(), self.striped, cx); + } + + pub fn render_header( + &self, + headers: [impl IntoElement; COLS], + cx: &mut App, + ) -> impl IntoElement { + render_header(headers, cx) + } +} + +fn base_cell_style(cx: &App) -> Div { + div() + .px_1p5() + .flex_1() + .justify_start() + .text_ui(cx) + .whitespace_nowrap() + .text_ellipsis() + .overflow_hidden() +} + +pub fn render_row( + row_index: usize, + items: [impl IntoElement; COLS], + row_count: usize, + striped: bool, + cx: &App, +) -> AnyElement { + let is_last = row_index == row_count - 1; + let bg = if row_index % 2 == 1 && 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( + items + .map(IntoElement::into_any_element) + .map(|cell| base_cell_style(cx).child(cell)), + ) + .into_any_element() +} + +pub fn render_header( + headers: [impl IntoElement; COLS], + cx: &mut App, +) -> impl IntoElement { + div() + .flex() + .flex_row() + .items_center() + .justify_between() + .w_full() + .p_2() + .border_b_1() + .border_color(cx.theme().colors().border) + .children(headers.into_iter().map(|h| { + base_cell_style(cx) + .font_weight(FontWeight::SEMIBOLD) + .child(h) + })) +} + +impl RenderOnce for Table { + fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + // match self.ro + let row_count = self.rows.len(); + div() + .w(self.width) + .overflow_hidden() + .when_some(self.headers.take(), |this, headers| { + this.child(render_header(headers, cx)) + }) + .map(|div| match self.rows { + TableContents::Vec(items) => div.children( + items + .into_iter() + .enumerate() + .map(|(index, row)| render_row(index, row, row_count, self.striped, cx)), + ), + TableContents::UniformList(uniform_list_data) => div.child(uniform_list( + uniform_list_data.element_id, + uniform_list_data.row_count, + uniform_list_data.render_item_fn, + )), + }) + } +} + +impl Component for Table<3> { + fn scope() -> ComponentScope { + ComponentScope::Layout + } + + fn description() -> Option<&'static str> { + Some("A table component for displaying data in rows and columns with optional styling.") + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + Some( + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Basic Tables", + vec![ + single_example( + "Simple Table", + Table::new() + .width(px(400.)) + .header(["Name", "Age", "City"]) + .row(["Alice", "28", "New York"]) + .row(["Bob", "32", "San Francisco"]) + .row(["Charlie", "25", "London"]) + .into_any_element(), + ), + single_example( + "Two Column Table", + Table::new() + .header(["Category", "Value"]) + .width(px(300.)) + .row(["Revenue", "$100,000"]) + .row(["Expenses", "$75,000"]) + .row(["Profit", "$25,000"]) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Styled Tables", + vec![ + single_example( + "Default", + Table::new() + .width(px(400.)) + .header(["Product", "Price", "Stock"]) + .row(["Laptop", "$999", "In Stock"]) + .row(["Phone", "$599", "Low Stock"]) + .row(["Tablet", "$399", "Out of Stock"]) + .into_any_element(), + ), + single_example( + "Striped", + Table::new() + .width(px(400.)) + .striped() + .header(["Product", "Price", "Stock"]) + .row(["Laptop", "$999", "In Stock"]) + .row(["Phone", "$599", "Low Stock"]) + .row(["Tablet", "$399", "Out of Stock"]) + .row(["Headphones", "$199", "In Stock"]) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Mixed Content Table", + vec![single_example( + "Table with Elements", + Table::new() + .width(px(840.)) + .header(["Status", "Name", "Priority", "Deadline", "Action"]) + .row([ + Indicator::dot().color(Color::Success).into_any_element(), + "Project A".into_any_element(), + "High".into_any_element(), + "2023-12-31".into_any_element(), + Button::new("view_a", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ]) + .row([ + Indicator::dot().color(Color::Warning).into_any_element(), + "Project B".into_any_element(), + "Medium".into_any_element(), + "2024-03-15".into_any_element(), + Button::new("view_b", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ]) + .row([ + Indicator::dot().color(Color::Error).into_any_element(), + "Project C".into_any_element(), + "Low".into_any_element(), + "2024-06-30".into_any_element(), + Button::new("view_c", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ]) + .into_any_element(), + )], + ), + ]) + .into_any_element(), + ) + } +} diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 6e3c7c78aec2732aeb6cea67b6da5da6686b0f30..237403d4ba053646108e88546242df1f07cdc8ab 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -32,7 +32,6 @@ mod settings_group; mod stack; mod tab; mod tab_bar; -mod table; mod toggle; mod tooltip; @@ -73,7 +72,6 @@ pub use settings_group::*; pub use stack::*; pub use tab::*; pub use tab_bar::*; -pub use table::*; pub use toggle::*; pub use tooltip::*; diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs deleted file mode 100644 index bd8fc140fb21c1656c5708eee061d5c6bb421219..0000000000000000000000000000000000000000 --- a/crates/ui/src/components/table.rs +++ /dev/null @@ -1,271 +0,0 @@ -use crate::{Indicator, prelude::*}; -use gpui::{AnyElement, FontWeight, IntoElement, Length, div}; - -/// A table component -#[derive(IntoElement, RegisterComponent)] -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 { - 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: &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) -> 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| { - Table::base_cell_style(cx) - .font_weight(FontWeight::SEMIBOLD) - .child(h.clone()) - })); - - 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 Component for Table { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some("A table component for displaying data in rows and columns with optional styling.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - 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(), - ) - } -} diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index ded1a08437fcfc7c8d8d47a1fd08072975dfeedd..f9aee26cddca7013bfa2fd1c6fb7c27dcb20e17d 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -5,8 +5,8 @@ use theme::all_theme_colors; use ui::{ AudioStatus, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox, CheckboxWithLabel, CollaboratorAvailability, ContentGroup, DecoratedIcon, - ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, Table, TintColor, - Tooltip, element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, + ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, TintColor, + Tooltip, prelude::*, utils::calculate_contrast_ratio, }; use crate::{Item, Workspace};