ui: Extract `table_row` & `tests` modules to separate files (#51059)

Oleksandr Kholiavko created

Extract data table modules into separate files

This PR extracts the `tests` and `table_row` modules from
`data_table.rs` into separate files to improve code organization. This
is preparatory work for the upcoming column width API rework (#2 in the
series), where separating mechanical changes from logical changes will
make the review easier.

The extraction was performed using rust-analyzer's "Extract module to
file" command.

**Context:**

This is part 1 of a 3-PR series improving data table column width
handling:
1. **This PR**: Extract modules into separate files (mechanical change)
2. [#51060](https://github.com/zed-industries/zed/pull/51060) -
Introduce width config enum for redistributable column widths (API
rework)
3. Implement independently resizable column widths (new feature)

The series builds on previously merged infrastructure:
- [#46341](https://github.com/zed-industries/zed/pull/46341) - Data
table dynamic column support
- [#46190](https://github.com/zed-industries/zed/pull/46190) - Variable
row height mode for data tables

Primary beneficiary: CSV preview feature
([#48207](https://github.com/zed-industries/zed/pull/48207))

Release Notes:

- N/A

Change summary

crates/ui/src/components/data_table.rs           | 540 -----------------
crates/ui/src/components/data_table/table_row.rs | 208 ++++++
crates/ui/src/components/data_table/tests.rs     | 318 ++++++++++
3 files changed, 529 insertions(+), 537 deletions(-)

Detailed changes

crates/ui/src/components/data_table.rs 🔗

@@ -18,216 +18,9 @@ use crate::{
 };
 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> {
-        pub fn from_element(element: T, length: usize) -> Self
-        where
-            T: Clone,
-        {
-            Self::from_vec(vec![element; length], length)
-        }
-
-        /// 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: impl Into<usize>) -> &T {
-            let col = col.into();
-            self.0.get(col).unwrap_or_else(|| {
-                panic!(
-                    "Expected table row of `{}` to have {col:?}",
-                    type_name::<T>()
-                )
-            })
-        }
-
-        pub fn get(&self, col: impl Into<usize>) -> Option<&T> {
-            self.0.get(col.into())
-        }
-
-        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)
-        }
-    }
-}
+pub mod table_row;
+#[cfg(test)]
+mod tests;
 
 const RESIZE_COLUMN_WIDTH: f32 = 8.0;
 
@@ -1445,330 +1238,3 @@ impl Component for Table {
         )
     }
 }
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
-        a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
-    }
-
-    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(
-        input: &str,
-        total_size: f32,
-        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.push(TableResizeBehavior::None);
-            } else if col.starts_with('*') {
-                resize_behavior.push(TableResizeBehavior::MinSize(col.len() as f32 / total_size));
-            } else {
-                panic!("invalid test input: unrecognized resize behavior: {}", col);
-            }
-        }
-
-        if resize_behavior.len() != expected_cols {
-            panic!(
-                "invalid test input: expected {} columns, got {}",
-                expected_cols,
-                resize_behavior.len()
-            );
-        }
-        resize_behavior
-    }
-
-    mod reset_column_size {
-        use super::*;
-
-        fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
-            let mut widths = Vec::new();
-            let mut column_index = None;
-            for (index, col) in input.split('|').enumerate() {
-                widths.push(col.len() as f32);
-                if col.starts_with('X') {
-                    column_index = Some(index);
-                }
-            }
-
-            for w in &widths {
-                assert!(w.is_finite(), "incorrect number of columns");
-            }
-            let total = widths.iter().sum::<f32>();
-            for width in &mut widths {
-                *width /= total;
-            }
-            (widths, total, column_index)
-        }
-
-        #[track_caller]
-        fn check_reset_size(
-            initial_sizes: &str,
-            widths: &str,
-            expected: &str,
-            resize_behavior: &str,
-        ) {
-            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 {
-                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 {
-                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 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,
-                TableRow::from_vec(widths, cols),
-                TableRow::from_vec(initial_sizes, cols),
-                &resize_behavior,
-            );
-            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_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_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($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($initial, $current, $expected, $resizing);
-                }
-            };
-        }
-
-        check_reset_size!(
-            basic_right,
-            columns: 5,
-            starting: "**|**|**|**|**",
-            snapshot: "**|**|X|***|**",
-            expected: "**|**|**|**|**",
-            minimums: "X|*|*|*|*",
-        );
-
-        check_reset_size!(
-            basic_left,
-            columns: 5,
-            starting: "**|**|**|**|**",
-            snapshot: "**|**|***|X|**",
-            expected: "**|**|**|**|**",
-            minimums: "X|*|*|*|**",
-        );
-
-        check_reset_size!(
-            squashed_left_reset_col2,
-            columns: 6,
-            starting: "*|***|**|**|****|*",
-            snapshot: "*|*|X|*|*|********",
-            expected: "*|*|**|*|*|*******",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            grow_cascading_right,
-            columns: 6,
-            starting: "*|***|****|**|***|*",
-            snapshot: "*|***|X|**|**|*****",
-            expected: "*|***|****|*|*|****",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-           squashed_right_reset_col4,
-           columns: 6,
-           starting: "*|***|**|**|****|*",
-           snapshot: "*|********|*|*|X|*",
-           expected: "*|*****|*|*|****|*",
-           minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            reset_col6_right,
-            columns: 6,
-            starting: "*|***|**|***|***|**",
-            snapshot: "*|***|**|***|**|XXX",
-            expected: "*|***|**|***|***|**",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            reset_col6_left,
-            columns: 6,
-            starting: "*|***|**|***|***|**",
-            snapshot: "*|***|**|***|****|X",
-            expected: "*|***|**|***|***|**",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            last_column_grow_cascading,
-            columns: 6,
-            starting: "*|***|**|**|**|***",
-            snapshot: "*|*******|*|**|*|X",
-            expected: "*|******|*|*|*|***",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            goes_left_when_left_has_extreme_diff,
-            columns: 6,
-            starting: "*|***|****|**|**|***",
-            snapshot: "*|********|X|*|**|**",
-            expected: "*|*****|****|*|**|**",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            basic_shrink_right,
-            columns: 6,
-            starting: "**|**|**|**|**|**",
-            snapshot: "**|**|XXX|*|**|**",
-            expected: "**|**|**|**|**|**",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            shrink_should_go_left,
-            columns: 6,
-            starting: "*|***|**|*|*|*",
-            snapshot: "*|*|XXX|**|*|*",
-            expected: "*|**|**|**|*|*",
-            minimums: "X|*|*|*|*|*",
-        );
-
-        check_reset_size!(
-            shrink_should_go_right,
-            columns: 6,
-            starting: "*|***|**|**|**|*",
-            snapshot: "*|****|XXX|*|*|*",
-            expected: "*|****|**|**|*|*",
-            minimums: "X|*|*|*|*|*",
-        );
-    }
-
-    mod drag_handle {
-        use super::*;
-
-        fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
-            let mut widths = Vec::new();
-            let column_index = input.replace("*", "").find("I");
-            for col in input.replace("I", "|").split('|') {
-                widths.push(col.len() as f32);
-            }
-
-            for w in &widths {
-                assert!(w.is_finite(), "incorrect number of columns");
-            }
-            let total = widths.iter().sum::<f32>();
-            for width in &mut widths {
-                *width /= total;
-            }
-            (widths, total, column_index)
-        }
-
-        #[track_caller]
-        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 {
-                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 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 mut widths_table_row = TableRow::from_vec(widths, cols);
-            TableColumnWidths::drag_column_handle(
-                distance,
-                column_index,
-                &mut widths_table_row,
-                &resize_behavior,
-            );
-
-            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(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_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($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($dist, $current, $expected, $resizing);
-                }
-            };
-        }
-
-        check!(
-            basic_right_drag,
-            columns: 3,
-            distance: 1,
-            snapshot: "**|**I**",
-            expected: "**|***|*",
-            minimums: "X|*|*",
-        );
-
-        check!(
-            drag_left_against_mins,
-            columns: 5,
-            distance: -1,
-            snapshot: "*|*|*|*I*******",
-            expected: "*|*|*|*|*******",
-            minimums: "X|*|*|*|*",
-        );
-
-        check!(
-            drag_left,
-            columns: 5,
-            distance: -2,
-            snapshot: "*|*|*|*****I***",
-            expected: "*|*|*|***|*****",
-            minimums: "X|*|*|*|*",
-        );
-    }
-}

crates/ui/src/components/data_table/table_row.rs 🔗

@@ -0,0 +1,208 @@
+//! 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> {
+    pub fn from_element(element: T, length: usize) -> Self
+    where
+        T: Clone,
+    {
+        Self::from_vec(vec![element; length], length)
+    }
+
+    /// 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: impl Into<usize>) -> &T {
+        let col = col.into();
+        self.0.get(col).unwrap_or_else(|| {
+            panic!(
+                "Expected table row of `{}` to have {col:?}",
+                type_name::<T>()
+            )
+        })
+    }
+
+    pub fn get(&self, col: impl Into<usize>) -> Option<&T> {
+        self.0.get(col.into())
+    }
+
+    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)
+    }
+}

crates/ui/src/components/data_table/tests.rs 🔗

@@ -0,0 +1,318 @@
+use super::*;
+
+fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
+    a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
+}
+
+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(
+    input: &str,
+    total_size: f32,
+    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.push(TableResizeBehavior::None);
+        } else if col.starts_with('*') {
+            resize_behavior.push(TableResizeBehavior::MinSize(col.len() as f32 / total_size));
+        } else {
+            panic!("invalid test input: unrecognized resize behavior: {}", col);
+        }
+    }
+
+    if resize_behavior.len() != expected_cols {
+        panic!(
+            "invalid test input: expected {} columns, got {}",
+            expected_cols,
+            resize_behavior.len()
+        );
+    }
+    resize_behavior
+}
+
+mod reset_column_size {
+    use super::*;
+
+    fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
+        let mut widths = Vec::new();
+        let mut column_index = None;
+        for (index, col) in input.split('|').enumerate() {
+            widths.push(col.len() as f32);
+            if col.starts_with('X') {
+                column_index = Some(index);
+            }
+        }
+
+        for w in &widths {
+            assert!(w.is_finite(), "incorrect number of columns");
+        }
+        let total = widths.iter().sum::<f32>();
+        for width in &mut widths {
+            *width /= total;
+        }
+        (widths, total, column_index)
+    }
+
+    #[track_caller]
+    fn check_reset_size(initial_sizes: &str, widths: &str, expected: &str, resize_behavior: &str) {
+        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 {
+            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 {
+            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 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,
+            TableRow::from_vec(widths, cols),
+            TableRow::from_vec(initial_sizes, cols),
+            &resize_behavior,
+        );
+        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_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_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($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($initial, $current, $expected, $resizing);
+            }
+        };
+    }
+
+    check_reset_size!(
+        basic_right,
+        columns: 5,
+        starting: "**|**|**|**|**",
+        snapshot: "**|**|X|***|**",
+        expected: "**|**|**|**|**",
+        minimums: "X|*|*|*|*",
+    );
+
+    check_reset_size!(
+        basic_left,
+        columns: 5,
+        starting: "**|**|**|**|**",
+        snapshot: "**|**|***|X|**",
+        expected: "**|**|**|**|**",
+        minimums: "X|*|*|*|**",
+    );
+
+    check_reset_size!(
+        squashed_left_reset_col2,
+        columns: 6,
+        starting: "*|***|**|**|****|*",
+        snapshot: "*|*|X|*|*|********",
+        expected: "*|*|**|*|*|*******",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        grow_cascading_right,
+        columns: 6,
+        starting: "*|***|****|**|***|*",
+        snapshot: "*|***|X|**|**|*****",
+        expected: "*|***|****|*|*|****",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+       squashed_right_reset_col4,
+       columns: 6,
+       starting: "*|***|**|**|****|*",
+       snapshot: "*|********|*|*|X|*",
+       expected: "*|*****|*|*|****|*",
+       minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        reset_col6_right,
+        columns: 6,
+        starting: "*|***|**|***|***|**",
+        snapshot: "*|***|**|***|**|XXX",
+        expected: "*|***|**|***|***|**",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        reset_col6_left,
+        columns: 6,
+        starting: "*|***|**|***|***|**",
+        snapshot: "*|***|**|***|****|X",
+        expected: "*|***|**|***|***|**",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        last_column_grow_cascading,
+        columns: 6,
+        starting: "*|***|**|**|**|***",
+        snapshot: "*|*******|*|**|*|X",
+        expected: "*|******|*|*|*|***",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        goes_left_when_left_has_extreme_diff,
+        columns: 6,
+        starting: "*|***|****|**|**|***",
+        snapshot: "*|********|X|*|**|**",
+        expected: "*|*****|****|*|**|**",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        basic_shrink_right,
+        columns: 6,
+        starting: "**|**|**|**|**|**",
+        snapshot: "**|**|XXX|*|**|**",
+        expected: "**|**|**|**|**|**",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        shrink_should_go_left,
+        columns: 6,
+        starting: "*|***|**|*|*|*",
+        snapshot: "*|*|XXX|**|*|*",
+        expected: "*|**|**|**|*|*",
+        minimums: "X|*|*|*|*|*",
+    );
+
+    check_reset_size!(
+        shrink_should_go_right,
+        columns: 6,
+        starting: "*|***|**|**|**|*",
+        snapshot: "*|****|XXX|*|*|*",
+        expected: "*|****|**|**|*|*",
+        minimums: "X|*|*|*|*|*",
+    );
+}
+
+mod drag_handle {
+    use super::*;
+
+    fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
+        let mut widths = Vec::new();
+        let column_index = input.replace("*", "").find("I");
+        for col in input.replace("I", "|").split('|') {
+            widths.push(col.len() as f32);
+        }
+
+        for w in &widths {
+            assert!(w.is_finite(), "incorrect number of columns");
+        }
+        let total = widths.iter().sum::<f32>();
+        for width in &mut widths {
+            *width /= total;
+        }
+        (widths, total, column_index)
+    }
+
+    #[track_caller]
+    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 {
+            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 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 mut widths_table_row = TableRow::from_vec(widths, cols);
+        TableColumnWidths::drag_column_handle(
+            distance,
+            column_index,
+            &mut widths_table_row,
+            &resize_behavior,
+        );
+
+        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(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_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($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($dist, $current, $expected, $resizing);
+            }
+        };
+    }
+
+    check!(
+        basic_right_drag,
+        columns: 3,
+        distance: 1,
+        snapshot: "**|**I**",
+        expected: "**|***|*",
+        minimums: "X|*|*",
+    );
+
+    check!(
+        drag_left_against_mins,
+        columns: 5,
+        distance: -1,
+        snapshot: "*|*|*|*I*******",
+        expected: "*|*|*|*|*******",
+        minimums: "X|*|*|*|*",
+    );
+
+    check!(
+        drag_left,
+        columns: 5,
+        distance: -2,
+        snapshot: "*|*|*|*****I***",
+        expected: "*|*|*|***|*****",
+        minimums: "X|*|*|*|*",
+    );
+}