@@ -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<T>`, 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<T>(Vec<T>);
+
+ impl<T> TableRow<T> {
+ /// Constructs a `TableRow` from a `Vec<T>`, 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<T>, expected_length: usize) -> Self {
+ Self::try_from_vec(data, expected_length).unwrap_or_else(|e| {
+ let name = type_name::<Vec<T>>();
+ panic!("Expected {name} to be created successfully: {e}");
+ })
+ }
+
+ /// Attempts to construct a `TableRow` from a `Vec<T>`, 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<T>, expected_len: usize) -> Result<Self, String> {
+ 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::<T>()
+ )
+ })
+ }
+
+ 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<T> {
+ 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<F, U>(&self, f: F) -> TableRow<U>
+ 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<F, U>(self, f: F) -> TableRow<U>
+ 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<F, U>(&self, f: F) -> TableRow<U>
+ 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<T> {
+ fn into_table_row(self, expected_length: usize) -> TableRow<T>;
+ }
+ impl<T> IntoTableRow<T> for Vec<T> {
+ fn into_table_row(self, expected_length: usize) -> TableRow<T> {
+ TableRow::from_vec(self, expected_length)
+ }
+ }
+
+ // Index implementations for convenient access
+ impl<T> Index<usize> for TableRow<T> {
+ type Output = T;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.0[index]
+ }
+ }
+
+ impl<T> IndexMut<usize> for TableRow<T> {
+ fn index_mut(&mut self, index: usize) -> &mut Self::Output {
+ &mut self.0[index]
+ }
+ }
+
+ // Range indexing implementations for slice operations
+ impl<T> Index<Range<usize>> for TableRow<T> {
+ type Output = [T];
+
+ fn index(&self, index: Range<usize>) -> &Self::Output {
+ <Vec<T> as Index<Range<usize>>>::index(&self.0, index)
+ }
+ }
+
+ impl<T> Index<RangeFrom<usize>> for TableRow<T> {
+ type Output = [T];
+
+ fn index(&self, index: RangeFrom<usize>) -> &Self::Output {
+ <Vec<T> as Index<RangeFrom<usize>>>::index(&self.0, index)
+ }
+ }
+
+ impl<T> Index<RangeTo<usize>> for TableRow<T> {
+ type Output = [T];
+
+ fn index(&self, index: RangeTo<usize>) -> &Self::Output {
+ <Vec<T> as Index<RangeTo<usize>>>::index(&self.0, index)
+ }
+ }
+
+ impl<T> Index<RangeToInclusive<usize>> for TableRow<T> {
+ type Output = [T];
+
+ fn index(&self, index: RangeToInclusive<usize>) -> &Self::Output {
+ <Vec<T> as Index<RangeToInclusive<usize>>>::index(&self.0, index)
+ }
+ }
+
+ impl<T> Index<RangeFull> for TableRow<T> {
+ type Output = [T];
+
+ fn index(&self, index: RangeFull) -> &Self::Output {
+ <Vec<T> as Index<RangeFull>>::index(&self.0, index)
+ }
+ }
+
+ impl<T> Index<RangeInclusive<usize>> for TableRow<T> {
+ type Output = [T];
+
+ fn index(&self, index: RangeInclusive<usize>) -> &Self::Output {
+ <Vec<T> as Index<RangeInclusive<usize>>>::index(&self.0, index)
+ }
+ }
+
+ impl<T> IndexMut<RangeInclusive<usize>> for TableRow<T> {
+ fn index_mut(&mut self, index: RangeInclusive<usize>) -> &mut Self::Output {
+ <Vec<T> as IndexMut<RangeInclusive<usize>>>::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<T>` internally
+pub type UncheckedTableRow<T> = Vec<T>;
+
#[derive(Debug)]
struct DraggedColumn(usize);
-struct UniformListData<const COLS: usize> {
+struct UniformListData {
render_list_of_rows_fn:
- Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
+ Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
element_id: ElementId,
row_count: usize,
}
-struct VariableRowHeightListData<const COLS: usize> {
+struct VariableRowHeightListData {
/// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
- render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> [AnyElement; COLS]>,
+ render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
list_state: ListState,
row_count: usize,
}
-enum TableContents<const COLS: usize> {
- Vec(Vec<[AnyElement; COLS]>),
- UniformList(UniformListData<COLS>),
- VariableRowHeightList(VariableRowHeightListData<COLS>),
+enum TableContents {
+ Vec(Vec<TableRow<AnyElement>>),
+ UniformList(UniformListData),
+ VariableRowHeightList(VariableRowHeightListData),
}
-impl<const COLS: usize> TableContents<COLS> {
- fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
+impl TableContents {
+ fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
match self {
TableContents::Vec(rows) => Some(rows),
TableContents::UniformList(_) => None,
@@ -101,24 +310,41 @@ impl TableInteractionState {
}
}
- fn render_resize_handles<const COLS: usize>(
+ /// 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<Entity<TableColumnWidths<COLS>>>,
+ column_widths: &TableRow<Length>,
+ resizable_columns: &TableRow<TableResizeBehavior>,
+ initial_sizes: &TableRow<DefiniteLength>,
+ columns: Option<Entity<TableColumnWidths>>,
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<const COLS: usize> {
- widths: [DefiniteLength; COLS],
- visible_widths: [DefiniteLength; COLS],
+pub struct TableColumnWidths {
+ widths: TableRow<DefiniteLength>,
+ visible_widths: TableRow<DefiniteLength>,
cached_bounds_width: Pixels,
initialized: bool,
}
-impl<const COLS: usize> TableColumnWidths<COLS> {
- 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<const COLS: usize> TableColumnWidths<COLS> {
fn on_double_click(
&mut self,
double_click_position: usize,
- initial_sizes: &[DefiniteLength; COLS],
- resize_behavior: &[TableResizeBehavior; COLS],
+ initial_sizes: &TableRow<DefiniteLength>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
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<const COLS: usize> TableColumnWidths<COLS> {
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<f32>,
+ initial_sizes: TableRow<f32>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
+ ) -> TableRow<f32> {
// RESET:
// Part 1:
// Figure out if we should shrink/grow the selected column
@@ -348,7 +578,7 @@ impl<const COLS: usize> TableColumnWidths<COLS> {
fn on_drag_move(
&mut self,
drag_event: &DragMoveEvent<DraggedColumn>,
- resize_behavior: &[TableResizeBehavior; COLS],
+ resize_behavior: &TableRow<TableResizeBehavior>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -368,7 +598,7 @@ impl<const COLS: usize> TableColumnWidths<COLS> {
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<const COLS: usize> TableColumnWidths<COLS> {
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<const COLS: usize> TableColumnWidths<COLS> {
fn drag_column_handle(
diff: f32,
col_idx: usize,
- widths: &mut [f32; COLS],
- resize_behavior: &[TableResizeBehavior; COLS],
+ widths: &mut TableRow<f32>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
) {
// if diff > 0.0 then go right
if diff > 0.0 {
@@ -406,8 +637,8 @@ impl<const COLS: usize> TableColumnWidths<COLS> {
fn propagate_resize_diff(
diff: f32,
col_idx: usize,
- widths: &mut [f32; COLS],
- resize_behavior: &[TableResizeBehavior; COLS],
+ widths: &mut TableRow<f32>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
direction: i8,
) -> f32 {
let mut diff_remaining = diff;
@@ -429,7 +660,7 @@ impl<const COLS: usize> TableColumnWidths<COLS> {
}
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<const COLS: usize> TableColumnWidths<COLS> {
}
}
-pub struct TableWidths<const COLS: usize> {
- initial: [DefiniteLength; COLS],
- current: Option<Entity<TableColumnWidths<COLS>>>,
- resizable: [TableResizeBehavior; COLS],
+pub struct TableWidths {
+ initial: TableRow<DefiniteLength>,
+ current: Option<Entity<TableColumnWidths>>,
+ resizable: TableRow<TableResizeBehavior>,
}
-impl<const COLS: usize> TableWidths<COLS> {
- pub fn new(widths: [impl Into<DefiniteLength>; COLS]) -> Self {
+impl TableWidths {
+ pub fn new(widths: TableRow<impl Into<DefiniteLength>>) -> 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<Length> {
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<const COLS: usize = 3> {
+pub struct Table {
striped: bool,
width: Option<Length>,
- headers: Option<[AnyElement; COLS]>,
- rows: TableContents<COLS>,
+ headers: Option<TableRow<AnyElement>>,
+ rows: TableContents,
interaction_state: Option<WeakEntity<TableInteractionState>>,
- col_widths: Option<TableWidths<COLS>>,
+ col_widths: Option<TableWidths>,
map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
use_ui_font: bool,
empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
+ /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
+ cols: usize,
}
-impl<const COLS: usize> Table<COLS> {
- /// 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<const COLS: usize> Table<COLS> {
mut self,
id: impl Into<ElementId>,
row_count: usize,
- render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
+ render_item_fn: impl Fn(
+ Range<usize>,
+ &mut Window,
+ &mut App,
+ ) -> Vec<UncheckedTableRow<AnyElement>>
+ 'static,
) -> Self {
self.rows = TableContents::UniformList(UniformListData {
@@ -548,7 +788,7 @@ impl<const COLS: usize> Table<COLS> {
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<AnyElement> + 'static,
) -> Self {
self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
render_row_fn: Box::new(render_row_fn),
@@ -558,7 +798,7 @@ impl<const COLS: usize> Table<COLS> {
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<const COLS: usize> Table<COLS> {
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<impl IntoElement>) -> 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<impl IntoElement>) -> 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<DefiniteLength>; COLS]) -> Self {
+ pub fn column_widths(mut self, widths: UncheckedTableRow<impl Into<DefiniteLength>>) -> 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<TableColumnWidths<COLS>>,
+ resizable: UncheckedTableRow<TableResizeBehavior>,
+ column_widths: &Entity<TableColumnWidths>,
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<const COLS: usize> Table<COLS> {
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<Length>, 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<const COLS: usize>(
+pub fn render_table_row(
row_index: usize,
- items: [impl IntoElement; COLS],
- table_context: TableRenderContext<COLS>,
+ items: TableRow<impl IntoElement>,
+ table_context: TableRenderContext,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
@@ -677,9 +925,12 @@ pub fn render_table_row<const COLS: usize>(
} 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<const COLS: usize>(
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<const COLS: usize>(
div().size_full().child(row).into_any_element()
}
-pub fn render_table_header<const COLS: usize>(
- headers: [impl IntoElement; COLS],
- table_context: TableRenderContext<COLS>,
+pub fn render_table_header(
+ headers: TableRow<impl IntoElement>,
+ table_context: TableRenderContext,
columns_widths: Option<(
- WeakEntity<TableColumnWidths<COLS>>,
- [TableResizeBehavior; COLS],
- [DefiniteLength; COLS],
+ WeakEntity<TableColumnWidths>,
+ TableRow<TableResizeBehavior>,
+ TableRow<DefiniteLength>,
)>,
entity_id: Option<EntityId>,
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<const COLS: usize>(
.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<const COLS: usize> {
+pub struct TableRenderContext {
pub striped: bool,
pub total_row_count: usize,
- pub column_widths: Option<[Length; COLS]>,
+ pub column_widths: Option<TableRow<Length>>,
pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
pub use_ui_font: bool,
}
-impl<const COLS: usize> TableRenderContext<COLS> {
- fn new(table: &Table<COLS>, 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<const COLS: usize> TableRenderContext<COLS> {
}
}
-impl<const COLS: usize> RenderOnce for Table<COLS> {
+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<const COLS: usize> RenderOnce for Table<COLS> {
.on_drop::<DraggedColumn>(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<const COLS: usize> RenderOnce for Table<COLS> {
{
let render_item_fn = uniform_list_data.render_list_of_rows_fn;
move |range: Range<usize>, 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::<Vec<_>>();
elements
.into_iter()
.zip(range)
@@ -933,7 +1202,8 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
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<const COLS: usize> RenderOnce for Table<COLS> {
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<const COLS: usize> RenderOnce for Table<COLS> {
}
}
-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<const COLS: usize>(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::<Vec<String>>()
.join("|")
}
- fn parse_resize_behavior<const COLS: usize>(
+ 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<TableResizeBehavior> {
+ 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
}