Add variable row height mode to table UI element (#46190)

Oleksandr Kholiavko created

## Add variable row height mode to data table infrastructure

This PR introduces support for variable row heights in the data table
component, laying the groundwork for more flexible tabular data
rendering in Zed.

**Context:**  
This is the first in a series of infrastructure-focused PRs split out
from the [original CSV preview draft
PR](https://github.com/zed-industries/zed/pull/44344). The draft PR
remains open as a reference and will be incrementally decomposed into
smaller, reviewable pieces like this one.

**Details:**  
- Adds a variable row height mode to the data table, enabling future
features that require rows of differing heights (such as CSV preview
and, eventually, database table views).
- No user-facing changes; this is an internal refactor to support
upcoming functionality.

**Thanks:**  
Big thanks to @Anthony-Eid for pairing sessions and guidance on how to
best structure and land these changes incrementally.

---

Release Notes:

- N/A (internal infrastructure change, no user impact)

Change summary

crates/gpui/src/styled.rs              |  7 ++
crates/ui/src/components/data_table.rs | 76 ++++++++++++++++++++++++---
2 files changed, 73 insertions(+), 10 deletions(-)

Detailed changes

crates/gpui/src/styled.rs 🔗

@@ -271,6 +271,13 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the element to stretch flex items to fill the available space along the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-items#stretch)
+    fn items_stretch(mut self) -> Self {
+        self.style().align_items = Some(AlignItems::Stretch);
+        self
+    }
+
     /// Sets the element to justify flex items against the start of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#start)
     fn justify_start(mut self) -> Self {

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

@@ -2,17 +2,17 @@ use std::{ops::Range, rc::Rc};
 
 use gpui::{
     AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
-    FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful,
-    UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
+    FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point,
+    Stateful, UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,
 };
 
 use crate::{
     ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
     ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
     InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
-    ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
-    StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px,
-    single_example, v_flex,
+    ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled,
+    StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex,
+    px, single_example, v_flex,
 };
 use itertools::intersperse_with;
 
@@ -22,14 +22,23 @@ const RESIZE_COLUMN_WIDTH: f32 = 8.0;
 struct DraggedColumn(usize);
 
 struct UniformListData<const COLS: usize> {
-    render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
+    render_list_of_rows_fn:
+        Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
     element_id: ElementId,
     row_count: usize,
 }
 
+struct VariableRowHeightListData<const COLS: usize> {
+    /// 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]>,
+    list_state: ListState,
+    row_count: usize,
+}
+
 enum TableContents<const COLS: usize> {
     Vec(Vec<[AnyElement; COLS]>),
     UniformList(UniformListData<COLS>),
+    VariableRowHeightList(VariableRowHeightListData<COLS>),
 }
 
 impl<const COLS: usize> TableContents<COLS> {
@@ -37,6 +46,7 @@ impl<const COLS: usize> TableContents<COLS> {
         match self {
             TableContents::Vec(rows) => Some(rows),
             TableContents::UniformList(_) => None,
+            TableContents::VariableRowHeightList(_) => None,
         }
     }
 
@@ -44,6 +54,7 @@ impl<const COLS: usize> TableContents<COLS> {
         match self {
             TableContents::Vec(rows) => rows.len(),
             TableContents::UniformList(data) => data.row_count,
+            TableContents::VariableRowHeightList(data) => data.row_count,
         }
     }
 
@@ -519,7 +530,30 @@ impl<const COLS: usize> Table<COLS> {
         self.rows = TableContents::UniformList(UniformListData {
             element_id: id.into(),
             row_count,
-            render_item_fn: Box::new(render_item_fn),
+            render_list_of_rows_fn: Box::new(render_item_fn),
+        });
+        self
+    }
+
+    /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
+    ///
+    /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
+    /// It is generally slower than [`Table::uniform_list`] due to the need to measure each row individually, but it provides correct layout for non-uniform or multiline content.
+    ///
+    /// # Parameters
+    /// - `row_count`: The total number of rows in the table.
+    /// - `list_state`: The [`ListState`] used for managing scroll position and virtualization. This must be initialized and managed by the caller, and should be kept in sync with the number of rows.
+    /// - `render_row_fn`: A closure that renders a single row, given the row index, a mutable reference to [`Window`], and a mutable reference to [`App`]. It should return an array of [`AnyElement`]s, one for each column.
+    pub fn variable_row_height_list(
+        mut self,
+        row_count: usize,
+        list_state: ListState,
+        render_row_fn: impl Fn(usize, &mut Window, &mut App) -> [AnyElement; COLS] + 'static,
+    ) -> Self {
+        self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
+            render_row_fn: Box::new(render_row_fn),
+            list_state,
+            row_count,
         });
         self
     }
@@ -647,7 +681,11 @@ pub fn render_table_row<const COLS: usize>(
         .column_widths
         .map_or([None; COLS], |widths| widths.map(Some));
 
-    let mut row = h_flex()
+    let mut row = div()
+        // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
+        // Applying `.flex().flex_row()` manually to overcome that
+        .flex()
+        .flex_row()
         .id(("table_row", row_index))
         .size_full()
         .when_some(bg, |row, bg| row.bg(bg))
@@ -855,7 +893,7 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
                                 uniform_list_data.element_id,
                                 uniform_list_data.row_count,
                                 {
-                                    let render_item_fn = uniform_list_data.render_item_fn;
+                                    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);
                                         elements
@@ -891,6 +929,24 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
                                 },
                             ),
                         ),
+                        TableContents::VariableRowHeightList(variable_list_data) => parent.child(
+                            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);
+                                    render_table_row(
+                                        row_index,
+                                        row,
+                                        table_context.clone(),
+                                        window,
+                                        cx,
+                                    )
+                                }
+                            })
+                            .size_full()
+                            .flex_grow()
+                            .with_sizing_behavior(ListSizingBehavior::Auto),
+                        ),
                     })
                     .when_some(
                         self.col_widths.as_ref().zip(interaction_state.as_ref()),
@@ -917,7 +973,7 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
                         .read(cx)
                         .custom_scrollbar
                         .clone()
-                        .unwrap_or_else(|| Scrollbars::new(super::ScrollAxes::Both));
+                        .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
                     content
                         .custom_scrollbars(
                             scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),