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); } }; }