From 05efa59fc667f439c072bbe331d533fe8f631a09 Mon Sep 17 00:00:00 2001 From: Oleksandr Kholiavko <43780952+HalavicH@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:04:38 +0100 Subject: [PATCH] Add variable row height mode to table UI element (#46190) ## 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) --- crates/gpui/src/styled.rs | 7 +++ crates/ui/src/components/data_table.rs | 76 ++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index c5eef0d4496edea4d30c665c82dc0a9f00bb83be..3d0b86a9523f5ac05e51941c826e32379368c464 100644 --- a/crates/gpui/src/styled.rs +++ b/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 { diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs index 9cd2a5cb7a0d802d170fcfbe6a812027c779d942..6b852f31a503a1dd79d28b90b82eb76638157a27 100644 --- a/crates/ui/src/components/data_table.rs +++ b/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 { - render_item_fn: Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, + render_list_of_rows_fn: + Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, element_id: ElementId, row_count: usize, } +struct VariableRowHeightListData { + /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height + render_row_fn: Box [AnyElement; COLS]>, + list_state: ListState, + row_count: usize, +} + enum TableContents { Vec(Vec<[AnyElement; COLS]>), UniformList(UniformListData), + VariableRowHeightList(VariableRowHeightListData), } impl TableContents { @@ -37,6 +46,7 @@ impl TableContents { match self { TableContents::Vec(rows) => Some(rows), TableContents::UniformList(_) => None, + TableContents::VariableRowHeightList(_) => None, } } @@ -44,6 +54,7 @@ impl TableContents { match self { TableContents::Vec(rows) => rows.len(), TableContents::UniformList(data) => data.row_count, + TableContents::VariableRowHeightList(data) => data.row_count, } } @@ -519,7 +530,30 @@ impl Table { 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( .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 RenderOnce for Table { 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, window, cx| { let elements = render_item_fn(range.clone(), window, cx); elements @@ -891,6 +929,24 @@ impl RenderOnce for Table { }, ), ), + 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 RenderOnce for Table { .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),