Cargo.lock 🔗
@@ -14567,6 +14567,7 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"command_palette_hooks",
+ "component",
"db",
"editor",
"feature_flags",
Ben Kunkle created
wip
Cargo.lock | 1
crates/settings_ui/Cargo.toml | 1
crates/settings_ui/src/keybindings.rs | 130 --------
crates/settings_ui/src/settings_ui.rs | 1
crates/settings_ui/src/ui_components/mod.rs | 1
crates/settings_ui/src/ui_components/table.rs | 314 +++++++++++++++++++++
crates/ui/src/components.rs | 2
crates/ui/src/components/table.rs | 271 ------------------
crates/workspace/src/theme_preview.rs | 4
9 files changed, 326 insertions(+), 399 deletions(-)
@@ -14567,6 +14567,7 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"command_palette_hooks",
+ "component",
"db",
"editor",
"feature_flags",
@@ -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
@@ -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<const COLS: usize> {
- striped: bool,
- width: Length,
- row_count: usize,
-}
-
-impl<const COLS: usize> Table<COLS> {
- /// 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<Length>) -> 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<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 SerializableItem for KeymapEditor {
fn serialized_item_kind() -> &'static str {
"KeymapEditor"
@@ -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;
@@ -0,0 +1 @@
+pub mod table;
@@ -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<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<AnyElement>>,
+ element_id: ElementId,
+ row_count: usize,
+}
+
+enum TableContents<const COLS: usize> {
+ Vec(Vec<[AnyElement; COLS]>),
+ UniformList(UniformListData),
+}
+
+impl<const COLS: usize> TableContents<COLS> {
+ 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<const COLS: usize = 3> {
+ striped: bool,
+ width: Length,
+ headers: Option<[AnyElement; COLS]>,
+ rows: TableContents<COLS>,
+}
+
+impl<const COLS: usize> Table<COLS> {
+ pub fn uniform_list(
+ id: impl Into<ElementId>,
+ row_count: usize,
+ render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<AnyElement> + '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<Length>) -> 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<const COLS: usize>(
+ 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<const COLS: usize>(
+ 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<const COLS: usize> RenderOnce for Table<COLS> {
+ 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<AnyElement> {
+ 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(),
+ )
+ }
+}
@@ -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::*;
@@ -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<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 {
- 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| {
- 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<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 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<AnyElement> {
- 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(),
- )
- }
-}
@@ -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};