From fa08e503462547820e251c1ef89ba445fbbe873d Mon Sep 17 00:00:00 2001 From: Oleksandr Kholiavko <43780952+HalavicH@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:32:38 +0100 Subject: [PATCH] Remove const generics from data table (runtime column count support) (#46341) This PR removes the const generics limitation from the data table component, enabling tables with a dynamic number of columns determined at runtime. **Context:** This is the next infrastructure step split out from the [original CSV preview draft PR](https://github.com/zed-industries/zed/pull/44344). The draft PR remains open as a reference and will continue to be decomposed into smaller, reviewable pieces. **Details:** - Introduces a `TableRow` newtype to enforce column count invariants at runtime, replacing the previous const generic approach. It's api surface is larger than is currently used, as it's planned to be used in CSV feature itself. - Refactors the data table and all usages (including the keymap editor) to work with runtime column counts. - This change is foundational for supporting CSV preview and other features that require flexible, dynamic tables. - Performance impact has not been formally measured, but there is no noticeable slowdown in practice. --- Release Notes: - N/A (internal infrastructure change, no user impact) --------- Co-authored-by: Anthony Eid --- .../src/edit_prediction_context_view.rs | 9 +- crates/keymap_editor/src/keymap_editor.rs | 15 +- crates/ui/src/components/data_table.rs | 696 ++++++++++++------ 3 files changed, 500 insertions(+), 220 deletions(-) diff --git a/crates/edit_prediction_ui/src/edit_prediction_context_view.rs b/crates/edit_prediction_ui/src/edit_prediction_context_view.rs index a148f08c11f25c0cf419a9e2fe5dd741eeb01105..64343c94baf6f2f129fb81a86c179b18dfc4c2cb 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_context_view.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_context_view.rs @@ -243,11 +243,14 @@ impl EditPredictionContextView { .gap_2() .child(v_flex().h_full().flex_1().child({ let t0 = run.started_at; - let mut table = ui::Table::<2>::new().width(ui::px(300.)).no_ui_font(); + let mut table = ui::Table::new(2).width(ui::px(300.)).no_ui_font(); for (key, value) in &run.metadata { - table = table.row([key.into_any_element(), value.clone().into_any_element()]) + table = table.row(vec![ + key.into_any_element(), + value.clone().into_any_element(), + ]) } - table = table.row([ + table = table.row(vec![ "Total Time".into_any_element(), format!("{} ms", (run.finished_at.unwrap_or(t0) - t0).as_millis()) .into_any_element(), diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 141cae7986ffb4436162cc2e7d62f4853cb97f83..6149f01cefb15aa3f02fa07df88da1bfe36bb73f 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -54,6 +54,7 @@ use crate::{ }; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static(""); +const COLS: usize = 6; actions!( keymap_editor, @@ -428,7 +429,7 @@ struct KeymapEditor { context_menu: Option<(Entity, Point, Subscription)>, previous_edit: Option, humanized_action_names: HumanizedActionNameCache, - current_widths: Entity>, + current_widths: Entity, show_hover_menus: bool, actions_with_schemas: HashSet<&'static str>, /// In order for the JSON LSP to run in the actions arguments editor, we @@ -560,7 +561,7 @@ impl KeymapEditor { actions_with_schemas: HashSet::default(), action_args_temp_dir: None, action_args_temp_dir_worktree: None, - current_widths: cx.new(|cx| TableColumnWidths::new(cx)), + current_widths: cx.new(|cx| TableColumnWidths::new(COLS, cx)), }; this.on_keymap_changed(window, cx); @@ -1914,14 +1915,14 @@ impl Render for KeymapEditor { ), ) .child( - Table::new() + Table::new(COLS) .interactable(&self.table_interaction_state) .striped() .empty_table_callback({ let this = cx.entity(); move |window, cx| this.read(cx).render_no_matches_hint(window, cx) }) - .column_widths([ + .column_widths(vec![ DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))), DefiniteLength::Fraction(0.25), DefiniteLength::Fraction(0.20), @@ -1930,7 +1931,7 @@ impl Render for KeymapEditor { DefiniteLength::Fraction(0.08), ]) .resizable_columns( - [ + vec![ TableResizeBehavior::None, TableResizeBehavior::Resizable, TableResizeBehavior::Resizable, @@ -1941,7 +1942,7 @@ impl Render for KeymapEditor { &self.current_widths, cx, ) - .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) + .header(vec!["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .uniform_list( "keymap-editor-table", row_count, @@ -2051,7 +2052,7 @@ impl Render for KeymapEditor { .unwrap_or_default() .into_any_element(); - Some([ + Some(vec![ icon.into_any_element(), action, action_arguments, diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs index 6b852f31a503a1dd79d28b90b82eb76638157a27..8362ada27d67c056c76266c3f683e6aac7f3f183 100644 --- a/crates/ui/src/components/data_table.rs +++ b/crates/ui/src/components/data_table.rs @@ -12,37 +12,246 @@ use crate::{ InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, - px, single_example, v_flex, + px, single_example, + table_row::{IntoTableRow as _, TableRow}, + v_flex, }; use itertools::intersperse_with; +pub mod table_row { + //! A newtype for a table row that enforces a fixed column count at runtime. + //! + //! This type ensures that all rows in a table have the same width, preventing accidental creation or mutation of rows with inconsistent lengths. + //! It is especially useful for CSV or tabular data where rectangular invariants must be maintained, but the number of columns is only known at runtime. + //! By using `TableRow`, we gain stronger guarantees and safer APIs compared to a bare `Vec`, without requiring const generics. + + use std::{ + any::type_name, + ops::{ + Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, + }, + }; + + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct TableRow(Vec); + + impl TableRow { + /// Constructs a `TableRow` from a `Vec`, panicking if the length does not match `expected_length`. + /// + /// Use this when you want to ensure at construction time that the row has the correct number of columns. + /// This enforces the rectangular invariant for table data, preventing accidental creation of malformed rows. + /// + /// # Panics + /// Panics if `data.len() != expected_length`. + pub fn from_vec(data: Vec, expected_length: usize) -> Self { + Self::try_from_vec(data, expected_length).unwrap_or_else(|e| { + let name = type_name::>(); + panic!("Expected {name} to be created successfully: {e}"); + }) + } + + /// Attempts to construct a `TableRow` from a `Vec`, returning an error if the length does not match `expected_len`. + /// + /// This is a fallible alternative to `from_vec`, allowing you to handle inconsistent row lengths gracefully. + /// Returns `Ok(TableRow)` if the length matches, or an `Err` with a descriptive message otherwise. + pub fn try_from_vec(data: Vec, expected_len: usize) -> Result { + if data.len() != expected_len { + Err(format!( + "Row length {} does not match expected {}", + data.len(), + expected_len + )) + } else { + Ok(Self(data)) + } + } + + /// Returns reference to element by column index. + /// + /// # Panics + /// Panics if `col` is out of bounds (i.e., `col >= self.cols()`). + pub fn expect_get(&self, col: usize) -> &T { + self.0.get(col).unwrap_or_else(|| { + panic!( + "Expected table row of `{}` to have {col:?}", + type_name::() + ) + }) + } + + pub fn get(&self, col: usize) -> Option<&T> { + self.0.get(col) + } + + pub fn as_slice(&self) -> &[T] { + &self.0 + } + + pub fn into_vec(self) -> Vec { + self.0 + } + + /// Like [`map`], but borrows the row and clones each element before mapping. + /// + /// This is useful when you want to map over a borrowed row without consuming it, + /// but your mapping function requires ownership of each element. + /// + /// # Difference + /// - `map_cloned` takes `&self`, clones each element, and applies `f(T) -> U`. + /// - [`map`] takes `self` by value and applies `f(T) -> U` directly, consuming the row. + /// - [`map_ref`] takes `&self` and applies `f(&T) -> U` to references of each element. + pub fn map_cloned(&self, f: F) -> TableRow + where + F: FnMut(T) -> U, + T: Clone, + { + self.clone().map(f) + } + + /// Consumes the row and transforms all elements within it in a length-safe way. + /// + /// # Difference + /// - `map` takes ownership of the row (`self`) and applies `f(T) -> U` to each element. + /// - Use this when you want to transform and consume the row in one step. + /// - See also [`map_cloned`] (for mapping over a borrowed row with cloning) and [`map_ref`] (for mapping over references). + pub fn map(self, f: F) -> TableRow + where + F: FnMut(T) -> U, + { + TableRow(self.0.into_iter().map(f).collect()) + } + + /// Borrows the row and transforms all elements by reference in a length-safe way. + /// + /// # Difference + /// - `map_ref` takes `&self` and applies `f(&T) -> U` to each element by reference. + /// - Use this when you want to map over a borrowed row without cloning or consuming it. + /// - See also [`map`] (for consuming the row) and [`map_cloned`] (for mapping with cloning). + pub fn map_ref(&self, f: F) -> TableRow + where + F: FnMut(&T) -> U, + { + TableRow(self.0.iter().map(f).collect()) + } + + /// Number of columns (alias to `len()` with more semantic meaning) + pub fn cols(&self) -> usize { + self.0.len() + } + } + + ///// Convenience traits ///// + pub trait IntoTableRow { + fn into_table_row(self, expected_length: usize) -> TableRow; + } + impl IntoTableRow for Vec { + fn into_table_row(self, expected_length: usize) -> TableRow { + TableRow::from_vec(self, expected_length) + } + } + + // Index implementations for convenient access + impl Index for TableRow { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } + } + + impl IndexMut for TableRow { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.0[index] + } + } + + // Range indexing implementations for slice operations + impl Index> for TableRow { + type Output = [T]; + + fn index(&self, index: Range) -> &Self::Output { + as Index>>::index(&self.0, index) + } + } + + impl Index> for TableRow { + type Output = [T]; + + fn index(&self, index: RangeFrom) -> &Self::Output { + as Index>>::index(&self.0, index) + } + } + + impl Index> for TableRow { + type Output = [T]; + + fn index(&self, index: RangeTo) -> &Self::Output { + as Index>>::index(&self.0, index) + } + } + + impl Index> for TableRow { + type Output = [T]; + + fn index(&self, index: RangeToInclusive) -> &Self::Output { + as Index>>::index(&self.0, index) + } + } + + impl Index for TableRow { + type Output = [T]; + + fn index(&self, index: RangeFull) -> &Self::Output { + as Index>::index(&self.0, index) + } + } + + impl Index> for TableRow { + type Output = [T]; + + fn index(&self, index: RangeInclusive) -> &Self::Output { + as Index>>::index(&self.0, index) + } + } + + impl IndexMut> for TableRow { + fn index_mut(&mut self, index: RangeInclusive) -> &mut Self::Output { + as IndexMut>>::index_mut(&mut self.0, index) + } + } +} + const RESIZE_COLUMN_WIDTH: f32 = 8.0; +/// Represents an unchecked table row, which is a vector of elements. +/// Will be converted into `TableRow` internally +pub type UncheckedTableRow = Vec; + #[derive(Debug)] struct DraggedColumn(usize); -struct UniformListData { +struct UniformListData { render_list_of_rows_fn: - Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, + Box, &mut Window, &mut App) -> Vec>>, element_id: ElementId, row_count: usize, } -struct VariableRowHeightListData { +struct VariableRowHeightListData { /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height - render_row_fn: Box [AnyElement; COLS]>, + render_row_fn: Box UncheckedTableRow>, list_state: ListState, row_count: usize, } -enum TableContents { - Vec(Vec<[AnyElement; COLS]>), - UniformList(UniformListData), - VariableRowHeightList(VariableRowHeightListData), +enum TableContents { + Vec(Vec>), + UniformList(UniformListData), + VariableRowHeightList(VariableRowHeightListData), } -impl TableContents { - fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> { +impl TableContents { + fn rows_mut(&mut self) -> Option<&mut Vec>> { match self { TableContents::Vec(rows) => Some(rows), TableContents::UniformList(_) => None, @@ -101,24 +310,41 @@ impl TableInteractionState { } } - fn render_resize_handles( + /// Renders invisible resize handles overlaid on top of table content. + /// + /// - Spacer: invisible element that matches the width of table column content + /// - Divider: contains the actual resize handle that users can drag to resize columns + /// + /// Structure: [spacer] [divider] [spacer] [divider] [spacer] + /// + /// Business logic: + /// 1. Creates spacers matching each column width + /// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns) + /// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize + /// 4. Returns an absolute-positioned overlay that sits on top of table content + fn render_resize_handles( &self, - column_widths: &[Length; COLS], - resizable_columns: &[TableResizeBehavior; COLS], - initial_sizes: [DefiniteLength; COLS], - columns: Option>>, + column_widths: &TableRow, + resizable_columns: &TableRow, + initial_sizes: &TableRow, + columns: Option>, window: &mut Window, cx: &mut App, ) -> AnyElement { let spacers = column_widths + .as_slice() .iter() .map(|width| base_cell_style(Some(*width)).into_any_element()); let mut column_ix = 0; - let resizable_columns_slice = *resizable_columns; - let mut resizable_columns = resizable_columns.iter(); + let resizable_columns_shared = Rc::new(resizable_columns.clone()); + let initial_sizes_shared = Rc::new(initial_sizes.clone()); + let mut resizable_columns_iter = resizable_columns.as_slice().iter(); + // Insert dividers between spacers (column content) let dividers = intersperse_with(spacers, || { + let resizable_columns = Rc::clone(&resizable_columns_shared); + let initial_sizes = Rc::clone(&initial_sizes_shared); window.with_id(column_ix, |window| { let mut resize_divider = div() // This is required because this is evaluated at a different time than the use_state call above @@ -136,7 +362,7 @@ impl TableInteractionState { .w(px(RESIZE_COLUMN_WIDTH)) .h_full(); - if resizable_columns + if resizable_columns_iter .next() .is_some_and(TableResizeBehavior::is_resizable) { @@ -156,7 +382,7 @@ impl TableInteractionState { columns.on_double_click( column_ix, &initial_sizes, - &resizable_columns_slice, + &resizable_columns, window, ); }) @@ -206,23 +432,27 @@ impl TableResizeBehavior { } } -pub struct TableColumnWidths { - widths: [DefiniteLength; COLS], - visible_widths: [DefiniteLength; COLS], +pub struct TableColumnWidths { + widths: TableRow, + visible_widths: TableRow, cached_bounds_width: Pixels, initialized: bool, } -impl TableColumnWidths { - pub fn new(_: &mut App) -> Self { +impl TableColumnWidths { + pub fn new(cols: usize, _: &mut App) -> Self { Self { - widths: [DefiniteLength::default(); COLS], - visible_widths: [DefiniteLength::default(); COLS], + widths: vec![DefiniteLength::default(); cols].into_table_row(cols), + visible_widths: vec![DefiniteLength::default(); cols].into_table_row(cols), cached_bounds_width: Default::default(), initialized: false, } } + pub fn cols(&self) -> usize { + self.widths.cols() + } + fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { match length { DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width, @@ -236,17 +466,17 @@ impl TableColumnWidths { fn on_double_click( &mut self, double_click_position: usize, - initial_sizes: &[DefiniteLength; COLS], - resize_behavior: &[TableResizeBehavior; COLS], + initial_sizes: &TableRow, + resize_behavior: &TableRow, window: &mut Window, ) { let bounds_width = self.cached_bounds_width; let rem_size = window.rem_size(); let initial_sizes = - initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size)); + initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let widths = self .widths - .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); + .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let updated_widths = Self::reset_to_initial_size( double_click_position, @@ -255,15 +485,15 @@ impl TableColumnWidths { resize_behavior, ); self.widths = updated_widths.map(DefiniteLength::Fraction); - self.visible_widths = self.widths; + self.visible_widths = self.widths.clone(); // previously was copy } fn reset_to_initial_size( col_idx: usize, - mut widths: [f32; COLS], - initial_sizes: [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - ) -> [f32; COLS] { + mut widths: TableRow, + initial_sizes: TableRow, + resize_behavior: &TableRow, + ) -> TableRow { // RESET: // Part 1: // Figure out if we should shrink/grow the selected column @@ -348,7 +578,7 @@ impl TableColumnWidths { fn on_drag_move( &mut self, drag_event: &DragMoveEvent, - resize_behavior: &[TableResizeBehavior; COLS], + resize_behavior: &TableRow, window: &mut Window, cx: &mut Context, ) { @@ -368,7 +598,7 @@ impl TableColumnWidths { let mut widths = self .widths - .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); + .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); for length in widths[0..=col_idx].iter() { col_position += length + column_handle_width; @@ -378,7 +608,8 @@ impl TableColumnWidths { for length in widths[col_idx + 1..].iter() { total_length_ratio += length; } - total_length_ratio += (COLS - 1 - col_idx) as f32 * column_handle_width; + let cols = resize_behavior.cols(); + total_length_ratio += (cols - 1 - col_idx) as f32 * column_handle_width; let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; let drag_fraction = drag_fraction * total_length_ratio; @@ -392,8 +623,8 @@ impl TableColumnWidths { fn drag_column_handle( diff: f32, col_idx: usize, - widths: &mut [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], + widths: &mut TableRow, + resize_behavior: &TableRow, ) { // if diff > 0.0 then go right if diff > 0.0 { @@ -406,8 +637,8 @@ impl TableColumnWidths { fn propagate_resize_diff( diff: f32, col_idx: usize, - widths: &mut [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], + widths: &mut TableRow, + resize_behavior: &TableRow, direction: i8, ) -> f32 { let mut diff_remaining = diff; @@ -429,7 +660,7 @@ impl TableColumnWidths { } let mut curr_column = col_idx + step_right - step_left; - while diff_remaining != 0.0 && curr_column < COLS { + while diff_remaining != 0.0 && curr_column < widths.cols() { let Some(min_size) = resize_behavior[curr_column].min_size() else { if curr_column == 0 { break; @@ -461,49 +692,54 @@ impl TableColumnWidths { } } -pub struct TableWidths { - initial: [DefiniteLength; COLS], - current: Option>>, - resizable: [TableResizeBehavior; COLS], +pub struct TableWidths { + initial: TableRow, + current: Option>, + resizable: TableRow, } -impl TableWidths { - pub fn new(widths: [impl Into; COLS]) -> Self { +impl TableWidths { + pub fn new(widths: TableRow>) -> Self { let widths = widths.map(Into::into); + let expected_length = widths.cols(); TableWidths { initial: widths, current: None, - resizable: [TableResizeBehavior::None; COLS], + resizable: vec![TableResizeBehavior::None; expected_length] + .into_table_row(expected_length), } } - fn lengths(&self, cx: &App) -> [Length; COLS] { + fn lengths(&self, cx: &App) -> TableRow { self.current .as_ref() - .map(|entity| entity.read(cx).visible_widths.map(Length::Definite)) - .unwrap_or(self.initial.map(Length::Definite)) + .map(|entity| entity.read(cx).visible_widths.map_cloned(Length::Definite)) + .unwrap_or_else(|| self.initial.map_cloned(Length::Definite)) } } /// A table component #[derive(RegisterComponent, IntoElement)] -pub struct Table { +pub struct Table { striped: bool, width: Option, - headers: Option<[AnyElement; COLS]>, - rows: TableContents, + headers: Option>, + rows: TableContents, interaction_state: Option>, - col_widths: Option>, + col_widths: Option, map_row: Option), &mut Window, &mut App) -> AnyElement>>, use_ui_font: bool, empty_table_callback: Option AnyElement>>, + /// The number of columns in the table. Used to assert column numbers in `TableRow` collections + cols: usize, } -impl Table { - /// number of headers provided. - pub fn new() -> Self { +impl Table { + /// Creates a new table with the specified number of columns. + pub fn new(cols: usize) -> Self { Self { + cols, striped: false, width: None, headers: None, @@ -524,7 +760,11 @@ impl Table { mut self, id: impl Into, row_count: usize, - render_item_fn: impl Fn(Range, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + render_item_fn: impl Fn( + Range, + &mut Window, + &mut App, + ) -> Vec> + 'static, ) -> Self { self.rows = TableContents::UniformList(UniformListData { @@ -548,7 +788,7 @@ impl Table { mut self, row_count: usize, list_state: ListState, - render_row_fn: impl Fn(usize, &mut Window, &mut App) -> [AnyElement; COLS] + 'static, + render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow + 'static, ) -> Self { self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData { render_row_fn: Box::new(render_row_fn), @@ -558,7 +798,7 @@ impl Table { self } - /// Enables row striping. + /// Enables row striping (alternating row colors) pub fn striped(mut self) -> Self { self.striped = true; self @@ -584,33 +824,41 @@ impl Table { self } - pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self { - self.headers = Some(headers.map(IntoElement::into_any_element)); + pub fn header(mut self, headers: UncheckedTableRow) -> Self { + self.headers = Some( + headers + .into_table_row(self.cols) + .map(IntoElement::into_any_element), + ); self } - pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self { + pub fn row(mut self, items: UncheckedTableRow) -> Self { if let Some(rows) = self.rows.rows_mut() { - rows.push(items.map(IntoElement::into_any_element)); + rows.push( + items + .into_table_row(self.cols) + .map(IntoElement::into_any_element), + ); } self } - pub fn column_widths(mut self, widths: [impl Into; COLS]) -> Self { + pub fn column_widths(mut self, widths: UncheckedTableRow>) -> Self { if self.col_widths.is_none() { - self.col_widths = Some(TableWidths::new(widths)); + self.col_widths = Some(TableWidths::new(widths.into_table_row(self.cols))); } self } pub fn resizable_columns( mut self, - resizable: [TableResizeBehavior; COLS], - column_widths: &Entity>, + resizable: UncheckedTableRow, + column_widths: &Entity, cx: &mut App, ) -> Self { if let Some(table_widths) = self.col_widths.as_mut() { - table_widths.resizable = resizable; + table_widths.resizable = resizable.into_table_row(self.cols); let column_widths = table_widths .current .get_or_insert_with(|| column_widths.clone()); @@ -618,8 +866,8 @@ impl Table { column_widths.update(cx, |widths, _| { if !widths.initialized { widths.initialized = true; - widths.widths = table_widths.initial; - widths.visible_widths = widths.widths; + widths.widths = table_widths.initial.clone(); + widths.visible_widths = widths.widths.clone(); } }) } @@ -663,10 +911,10 @@ fn base_cell_style_text(width: Option, use_ui_font: bool, cx: &App) -> D base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx)) } -pub fn render_table_row( +pub fn render_table_row( row_index: usize, - items: [impl IntoElement; COLS], - table_context: TableRenderContext, + items: TableRow, + table_context: TableRenderContext, window: &mut Window, cx: &mut App, ) -> AnyElement { @@ -677,9 +925,12 @@ pub fn render_table_row( } else { None }; + let cols = items.cols(); let column_widths = table_context .column_widths - .map_or([None; COLS], |widths| widths.map(Some)); + .map_or(vec![None; cols].into_table_row(cols), |widths| { + widths.map(Some) + }); let mut row = div() // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element. @@ -699,8 +950,9 @@ pub fn render_table_row( row = row.children( items .map(IntoElement::into_any_element) + .into_vec() .into_iter() - .zip(column_widths) + .zip(column_widths.into_vec()) .map(|(cell, width)| { base_cell_style_text(width, table_context.use_ui_font, cx) .px_1() @@ -718,20 +970,23 @@ pub fn render_table_row( div().size_full().child(row).into_any_element() } -pub fn render_table_header( - headers: [impl IntoElement; COLS], - table_context: TableRenderContext, +pub fn render_table_header( + headers: TableRow, + table_context: TableRenderContext, columns_widths: Option<( - WeakEntity>, - [TableResizeBehavior; COLS], - [DefiniteLength; COLS], + WeakEntity, + TableRow, + TableRow, )>, entity_id: Option, cx: &mut App, ) -> impl IntoElement { + let cols = headers.cols(); let column_widths = table_context .column_widths - .map_or([None; COLS], |widths| widths.map(Some)); + .map_or(vec![None; cols].into_table_row(cols), |widths| { + widths.map(Some) + }); let element_id = entity_id .map(|entity| entity.to_string()) @@ -748,52 +1003,57 @@ pub fn render_table_header( .p_2() .border_b_1() .border_color(cx.theme().colors().border) - .children(headers.into_iter().enumerate().zip(column_widths).map( - |((header_idx, h), width)| { - base_cell_style_text(width, table_context.use_ui_font, cx) - .child(h) - .id(ElementId::NamedInteger( - shared_element_id.clone(), - header_idx as u64, - )) - .when_some( - columns_widths.as_ref().cloned(), - |this, (column_widths, resizables, initial_sizes)| { - if resizables[header_idx].is_resizable() { - this.on_click(move |event, window, cx| { - if event.click_count() > 1 { - column_widths - .update(cx, |column, _| { - column.on_double_click( - header_idx, - &initial_sizes, - &resizables, - window, - ); - }) - .ok(); - } - }) - } else { - this - } - }, - ) - }, - )) + .children( + headers + .into_vec() + .into_iter() + .enumerate() + .zip(column_widths.into_vec()) + .map(|((header_idx, h), width)| { + base_cell_style_text(width, table_context.use_ui_font, cx) + .child(h) + .id(ElementId::NamedInteger( + shared_element_id.clone(), + header_idx as u64, + )) + .when_some( + columns_widths.as_ref().cloned(), + |this, (column_widths, resizables, initial_sizes)| { + if resizables[header_idx].is_resizable() { + this.on_click(move |event, window, cx| { + if event.click_count() > 1 { + column_widths + .update(cx, |column, _| { + column.on_double_click( + header_idx, + &initial_sizes, + &resizables, + window, + ); + }) + .ok(); + } + }) + } else { + this + } + }, + ) + }), + ) } #[derive(Clone)] -pub struct TableRenderContext { +pub struct TableRenderContext { pub striped: bool, pub total_row_count: usize, - pub column_widths: Option<[Length; COLS]>, + pub column_widths: Option>, pub map_row: Option), &mut Window, &mut App) -> AnyElement>>, pub use_ui_font: bool, } -impl TableRenderContext { - fn new(table: &Table, cx: &App) -> Self { +impl TableRenderContext { + fn new(table: &Table, cx: &App) -> Self { Self { striped: table.striped, total_row_count: table.rows.len(), @@ -804,20 +1064,26 @@ impl TableRenderContext { } } -impl RenderOnce for Table { +impl RenderOnce for Table { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { let table_context = TableRenderContext::new(&self, cx); let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); let current_widths = self .col_widths .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable))) + .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable.clone()))) .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); let current_widths_with_initial_sizes = self .col_widths .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) + .and_then(|widths| { + Some(( + widths.current.as_ref()?, + widths.resizable.clone(), + widths.initial.clone(), + )) + }) .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); let width = self.width; @@ -863,7 +1129,7 @@ impl RenderOnce for Table { .on_drop::(move |_, _, cx| { widths .update(cx, |widths, _| { - widths.widths = widths.visible_widths; + widths.widths = widths.visible_widths.clone(); }) .ok(); // Finish the resize operation @@ -895,7 +1161,10 @@ impl RenderOnce for Table { { let render_item_fn = uniform_list_data.render_list_of_rows_fn; move |range: Range, window, cx| { - let elements = render_item_fn(range.clone(), window, cx); + let elements = render_item_fn(range.clone(), window, cx) + .into_iter() + .map(|raw_row| raw_row.into_table_row(self.cols)) + .collect::>(); elements .into_iter() .zip(range) @@ -933,7 +1202,8 @@ impl RenderOnce for Table { list(variable_list_data.list_state.clone(), { let render_item_fn = variable_list_data.render_row_fn; move |row_index: usize, window: &mut Window, cx: &mut App| { - let row = render_item_fn(row_index, window, cx); + let row = render_item_fn(row_index, window, cx) + .into_table_row(self.cols); render_table_row( row_index, row, @@ -952,13 +1222,13 @@ impl RenderOnce for Table { self.col_widths.as_ref().zip(interaction_state.as_ref()), |parent, (table_widths, state)| { parent.child(state.update(cx, |state, cx| { - let resizable_columns = table_widths.resizable; + let resizable_columns = &table_widths.resizable; let column_widths = table_widths.lengths(cx); let columns = table_widths.current.clone(); - let initial_sizes = table_widths.initial; + let initial_sizes = &table_widths.initial; state.render_resize_handles( &column_widths, - &resizable_columns, + resizable_columns, initial_sizes, columns, window, @@ -1012,7 +1282,7 @@ impl RenderOnce for Table { } } -impl Component for Table<3> { +impl Component for Table { fn scope() -> ComponentScope { ComponentScope::Layout } @@ -1031,22 +1301,22 @@ impl Component for Table<3> { vec![ single_example( "Simple Table", - Table::new() + Table::new(3) .width(px(400.)) - .header(["Name", "Age", "City"]) - .row(["Alice", "28", "New York"]) - .row(["Bob", "32", "San Francisco"]) - .row(["Charlie", "25", "London"]) + .header(vec!["Name", "Age", "City"]) + .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() - .header(["Category", "Value"]) + Table::new(2) + .header(vec!["Category", "Value"]) .width(px(300.)) - .row(["Revenue", "$100,000"]) - .row(["Expenses", "$75,000"]) - .row(["Profit", "$25,000"]) + .row(vec!["Revenue", "$100,000"]) + .row(vec!["Expenses", "$75,000"]) + .row(vec!["Profit", "$25,000"]) .into_any_element(), ), ], @@ -1056,24 +1326,24 @@ impl Component for Table<3> { vec![ single_example( "Default", - Table::new() + Table::new(3) .width(px(400.)) - .header(["Product", "Price", "Stock"]) - .row(["Laptop", "$999", "In Stock"]) - .row(["Phone", "$599", "Low Stock"]) - .row(["Tablet", "$399", "Out of Stock"]) + .header(vec!["Product", "Price", "Stock"]) + .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() + Table::new(3) .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"]) + .header(vec!["Product", "Price", "Stock"]) + .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(), ), ], @@ -1082,10 +1352,10 @@ impl Component for Table<3> { "Mixed Content Table", vec![single_example( "Table with Elements", - Table::new() + Table::new(5) .width(px(840.)) - .header(["Status", "Name", "Priority", "Deadline", "Action"]) - .row([ + .header(vec!["Status", "Name", "Priority", "Deadline", "Action"]) + .row(vec![ Indicator::dot().color(Color::Success).into_any_element(), "Project A".into_any_element(), "High".into_any_element(), @@ -1095,7 +1365,7 @@ impl Component for Table<3> { .full_width() .into_any_element(), ]) - .row([ + .row(vec![ Indicator::dot().color(Color::Warning).into_any_element(), "Project B".into_any_element(), "Medium".into_any_element(), @@ -1105,7 +1375,7 @@ impl Component for Table<3> { .full_width() .into_any_element(), ]) - .row([ + .row(vec![ Indicator::dot().color(Color::Error).into_any_element(), "Project C".into_any_element(), "Low".into_any_element(), @@ -1132,31 +1402,35 @@ mod test { a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6) } - fn cols_to_str(cols: &[f32; COLS], total_size: f32) -> String { - cols.map(|f| "*".repeat(f32::round(f * total_size) as usize)) + fn cols_to_str(cols: &[f32], total_size: f32) -> String { + cols.iter() + .map(|f| "*".repeat(f32::round(f * total_size) as usize)) + .collect::>() .join("|") } - fn parse_resize_behavior( + fn parse_resize_behavior( input: &str, total_size: f32, - ) -> [TableResizeBehavior; COLS] { - let mut resize_behavior = [TableResizeBehavior::None; COLS]; - let mut max_index = 0; - for (index, col) in input.split('|').enumerate() { + expected_cols: usize, + ) -> Vec { + let mut resize_behavior = Vec::with_capacity(expected_cols); + for col in input.split('|') { if col.starts_with('X') || col.is_empty() { - resize_behavior[index] = TableResizeBehavior::None; + resize_behavior.push(TableResizeBehavior::None); } else if col.starts_with('*') { - resize_behavior[index] = - TableResizeBehavior::MinSize(col.len() as f32 / total_size); + resize_behavior.push(TableResizeBehavior::MinSize(col.len() as f32 / total_size)); } else { panic!("invalid test input: unrecognized resize behavior: {}", col); } - max_index = index; } - if max_index + 1 != COLS { - panic!("invalid test input: too many columns"); + if resize_behavior.len() != expected_cols { + panic!( + "invalid test input: expected {} columns, got {}", + expected_cols, + resize_behavior.len() + ); } resize_behavior } @@ -1164,17 +1438,17 @@ mod test { mod reset_column_size { use super::*; - fn parse(input: &str) -> ([f32; COLS], f32, Option) { - let mut widths = [f32::NAN; COLS]; + fn parse(input: &str) -> (Vec, f32, Option) { + let mut widths = Vec::new(); let mut column_index = None; for (index, col) in input.split('|').enumerate() { - widths[index] = col.len() as f32; + widths.push(col.len() as f32); if col.starts_with('X') { column_index = Some(index); } } - for w in widths { + for w in &widths { assert!(w.is_finite(), "incorrect number of columns"); } let total = widths.iter().sum::(); @@ -1185,54 +1459,57 @@ mod test { } #[track_caller] - fn check_reset_size( + fn check_reset_size( initial_sizes: &str, widths: &str, expected: &str, resize_behavior: &str, ) { - let (initial_sizes, total_1, None) = parse::(initial_sizes) else { + let (initial_sizes, total_1, None) = parse(initial_sizes) else { panic!("invalid test input: initial sizes should not be marked"); }; - let (widths, total_2, Some(column_index)) = parse::(widths) else { + let (widths, total_2, Some(column_index)) = parse(widths) else { panic!("invalid test input: widths should be marked"); }; assert_eq!( total_1, total_2, "invalid test input: total width not the same {total_1}, {total_2}" ); - let (expected, total_3, None) = parse::(expected) else { + let (expected, total_3, None) = parse(expected) else { panic!("invalid test input: expected should not be marked: {expected:?}"); }; assert_eq!( total_2, total_3, "invalid test input: total width not the same" ); - let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); + let cols = initial_sizes.len(); + let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols); + let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols); let result = TableColumnWidths::reset_to_initial_size( column_index, - widths, - initial_sizes, + TableRow::from_vec(widths, cols), + TableRow::from_vec(initial_sizes, cols), &resize_behavior, ); - let is_eq = is_almost_eq(&result, &expected); + let result_slice = result.as_slice(); + let is_eq = is_almost_eq(result_slice, &expected); if !is_eq { - let result_str = cols_to_str(&result, total_1); + let result_str = cols_to_str(result_slice, total_1); let expected_str = cols_to_str(&expected, total_1); panic!( - "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" + "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_slice:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" ); } } macro_rules! check_reset_size { (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { - check_reset_size::<$cols>($initial, $current, $expected, $resizing); + check_reset_size($initial, $current, $expected, $resizing); }; ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { #[test] fn $name() { - check_reset_size::<$cols>($initial, $current, $expected, $resizing); + check_reset_size($initial, $current, $expected, $resizing); } }; } @@ -1349,14 +1626,14 @@ mod test { mod drag_handle { use super::*; - fn parse(input: &str) -> ([f32; COLS], f32, Option) { - let mut widths = [f32::NAN; COLS]; + fn parse(input: &str) -> (Vec, f32, Option) { + let mut widths = Vec::new(); let column_index = input.replace("*", "").find("I"); - for (index, col) in input.replace("I", "|").split('|').enumerate() { - widths[index] = col.len() as f32; + for col in input.replace("I", "|").split('|') { + widths.push(col.len() as f32); } - for w in widths { + for w in &widths { assert!(w.is_finite(), "incorrect number of columns"); } let total = widths.iter().sum::(); @@ -1367,51 +1644,50 @@ mod test { } #[track_caller] - fn check( - distance: i32, - widths: &str, - expected: &str, - resize_behavior: &str, - ) { - let (mut widths, total_1, Some(column_index)) = parse::(widths) else { + fn check(distance: i32, widths: &str, expected: &str, resize_behavior: &str) { + let (widths, total_1, Some(column_index)) = parse(widths) else { panic!("invalid test input: widths should be marked"); }; - let (expected, total_2, None) = parse::(expected) else { + let (expected, total_2, None) = parse(expected) else { panic!("invalid test input: expected should not be marked: {expected:?}"); }; assert_eq!( total_1, total_2, "invalid test input: total width not the same" ); - let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); + let cols = widths.len(); + let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols); + let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols); let distance = distance as f32 / total_1; - let result = TableColumnWidths::drag_column_handle( + let mut widths_table_row = TableRow::from_vec(widths, cols); + TableColumnWidths::drag_column_handle( distance, column_index, - &mut widths, + &mut widths_table_row, &resize_behavior, ); - let is_eq = is_almost_eq(&widths, &expected); + let result_widths = widths_table_row.as_slice(); + let is_eq = is_almost_eq(result_widths, &expected); if !is_eq { - let result_str = cols_to_str(&widths, total_1); + let result_str = cols_to_str(result_widths, total_1); let expected_str = cols_to_str(&expected, total_1); panic!( - "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" + "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_widths:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" ); } } macro_rules! check { (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { - check!($cols, $dist, $snapshot, $expected, $resizing); + check($dist, $current, $expected, $resizing); }; ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { #[test] fn $name() { - check::<$cols>($dist, $current, $expected, $resizing); + check($dist, $current, $expected, $resizing); } }; }