WIP Uniform table work and created an example for it

Anthony and Ben Kunkle created

Co-authored-by: Ben Kunkle <ben@zed.dev>

Change summary

crates/gpui/Cargo.toml                    |   4 
crates/gpui/examples/uniform_list.rs      |   5 
crates/gpui/examples/uniform_table.rs     |  50 +
crates/gpui/src/elements/uniform_table.rs | 644 +++++++++++++++---------
crates/ui/src/components/uniform_table.rs |  18 
5 files changed, 463 insertions(+), 258 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -298,3 +298,7 @@ path = "examples/uniform_list.rs"
 [[example]]
 name = "window_shadow"
 path = "examples/window_shadow.rs"
+
+[[example]]
+name = "uniform_table"
+path = "examples/uniform_table.rs"

crates/gpui/examples/uniform_list.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
-    rgb, size, uniform_list,
+    App, Application, Bounds, Context, ListSizingBehavior, Window, WindowBounds, WindowOptions,
+    div, prelude::*, px, rgb, size, uniform_list,
 };
 
 struct UniformListExample {}
@@ -30,6 +30,7 @@ impl Render for UniformListExample {
                     items
                 }),
             )
+            .with_sizing_behavior(ListSizingBehavior::Infer)
             .h_full(),
         )
     }

crates/gpui/examples/uniform_table.rs 🔗

@@ -0,0 +1,50 @@
+use gpui::{
+    App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
+    rgb, size,
+};
+
+struct UniformTableExample {}
+
+impl Render for UniformTableExample {
+    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+        const COLS: usize = 24;
+        const ROWS: usize = 100;
+        let mut headers = [0; COLS];
+
+        for column in 0..COLS {
+            headers[column] = column;
+        }
+
+        div().bg(rgb(0xffffff)).child(
+            gpui::uniform_table("simple table", ROWS, move |range, _, _| {
+                dbg!(&range);
+                range
+                    .map(|row_index| {
+                        let mut row = [0; COLS];
+                        for col in 0..COLS {
+                            row[col] = (row_index + 1) * (col + 1);
+                        }
+                        row.map(|cell| ToString::to_string(&cell))
+                            .map(|cell| div().flex().flex_row().child(cell))
+                            .map(IntoElement::into_any_element)
+                    })
+                    .collect()
+            })
+            .with_width_from_item(Some(ROWS - 1)),
+        )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| UniformTableExample {}),
+        )
+        .unwrap();
+    });
+}

crates/gpui/src/elements/uniform_table.rs 🔗

@@ -1,27 +1,37 @@
-use std::ops::Range;
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
+
+use smallvec::SmallVec;
 
 use crate::{
-    AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId,
-    Interactivity, IntoElement, LayoutId, Overflow, Pixels, Size, StyleRefinement, Window, point,
+    AnyElement, App, AvailableSpace, Bounds, ContentMask, Div, Element, ElementId, GlobalElementId,
+    Hitbox, InspectorElementId, Interactivity, IntoElement, IsZero as _, LayoutId, Length,
+    Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled as _, Window, div, point, px,
+    size,
 };
 
 /// todo!
 pub struct UniformTable<const COLS: usize> {
     id: ElementId,
     row_count: usize,
-    striped: bool,
-    headers: Option<[AnyElement; COLS]>,
     render_rows:
-        Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
+        Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
     interactivity: Interactivity,
-    // flexes: Arc<Mutex<[f32; COL]>>,
+    source_location: &'static std::panic::Location<'static>,
+    item_to_measure_index: usize,
+    scroll_handle: Option<UniformTableScrollHandle>, // todo! we either want to make our own or make a shared scroll handle between list and table
+    sizings: [Length; COLS],
 }
 
-/// todo!
-pub fn uniform_table<const COLS: usize>(
+/// TODO
+#[track_caller]
+pub fn uniform_table<const COLS: usize, F>(
     id: impl Into<ElementId>,
     row_count: usize,
-) -> UniformTable<COLS> {
+    render_rows: F,
+) -> UniformTable<COLS>
+where
+    F: 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>,
+{
     let mut base_style = StyleRefinement::default();
     base_style.overflow.y = Some(Overflow::Scroll);
     let id = id.into();
@@ -32,53 +42,25 @@ pub fn uniform_table<const COLS: usize>(
     UniformTable {
         id: id.clone(),
         row_count,
-        striped: false,
-        headers: None,
-        render_rows: Box::new(UniformTable::default_render_rows), // flexes: Arc::new(Mutex::new([0.0; COLS])),
+        render_rows: Rc::new(render_rows),
         interactivity: Interactivity {
             element_id: Some(id),
             base_style: Box::new(base_style),
             ..Interactivity::new()
         },
+        source_location: core::panic::Location::caller(),
+        item_to_measure_index: 0,
+        scroll_handle: None,
+        sizings: [Length::Auto; COLS],
     }
 }
 
 impl<const COLS: usize> UniformTable<COLS> {
     /// todo!
-    pub fn striped(mut self, striped: bool) -> Self {
-        self.striped = striped;
+    pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
+        self.item_to_measure_index = item_index.unwrap_or(0);
         self
     }
-
-    /// todo!
-    pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
-        self.headers = Some(headers.map(IntoElement::into_any_element));
-        self
-    }
-
-    /// todo!
-    pub fn rows<F>(mut self, render_rows: F) -> Self
-    where
-        F: 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>,
-    {
-        // let render_rows = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
-        //     // FIXME: avoid the double copy from vec and collect
-        //     render_rows(range, window, cx)
-        //         .into_iter()
-        //         .map(|component| component.into_any_element())
-        //         .collect()
-        // };
-        self.render_rows = Box::new(render_rows);
-        self
-    }
-
-    fn default_render_rows(
-        range: Range<usize>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Vec<[AnyElement; COLS]> {
-        vec![]
-    }
 }
 
 impl<const COLS: usize> IntoElement for UniformTable<COLS> {
@@ -92,14 +74,14 @@ impl<const COLS: usize> IntoElement for UniformTable<COLS> {
 impl<const COLS: usize> Element for UniformTable<COLS> {
     type RequestLayoutState = ();
 
-    type PrepaintState = ();
+    type PrepaintState = (Option<Hitbox>, SmallVec<[AnyElement; 32]>);
 
     fn id(&self) -> Option<ElementId> {
         Some(self.id.clone())
     }
 
     fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
-        None
+        Some(self.source_location)
     }
 
     fn request_layout(
@@ -109,15 +91,37 @@ impl<const COLS: usize> Element for UniformTable<COLS> {
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
-        let max_items = self.row_count;
+        let measure_cx = MeasureContext::new(self);
+        let item_size = measure_cx.measure_item(AvailableSpace::MinContent, None, window, cx);
         let layout_id = self.interactivity.request_layout(
             global_id,
             inspector_id,
             window,
             cx,
-            |style, window, cx| {
+            |style, window, _cx| {
                 window.with_text_style(style.text_style().cloned(), |window| {
-                    window.request_layout(style, None, cx)
+                    window.request_measured_layout(
+                        style,
+                        move |known_dimensions, available_space, window, cx| {
+                            let desired_height = item_size.height * measure_cx.row_count;
+                            let width =
+                                known_dimensions
+                                    .width
+                                    .unwrap_or(match available_space.width {
+                                        AvailableSpace::Definite(x) => x,
+                                        AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                            item_size.width
+                                        }
+                                    });
+                            let height = match available_space.height {
+                                AvailableSpace::Definite(height) => desired_height.min(height),
+                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                    desired_height
+                                }
+                            };
+                            size(width, height)
+                        },
+                    )
                 })
             },
         );
@@ -130,7 +134,7 @@ impl<const COLS: usize> Element for UniformTable<COLS> {
         global_id: Option<&GlobalElementId>,
         inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
-        request_layout: &mut Self::RequestLayoutState,
+        _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
         cx: &mut App,
     ) -> Self::PrepaintState {
@@ -148,209 +152,357 @@ impl<const COLS: usize> Element for UniformTable<COLS> {
                 - point(border.right + padding.right, border.bottom + padding.bottom),
         );
 
-        // let can_scroll_horizontally = matches!(
-        //     self.horizontal_sizing_behavior,
-        //     ListHorizontalSizingBehavior::Unconstrained
-        // );
-
-        // let longest_item_size = self.measure_item(None, window, cx);
-        // let content_width = if can_scroll_horizontally {
-        //     padded_bounds.size.width.max(longest_item_size.width)
-        // } else {
-        //     padded_bounds.size.width
-        // };
-
-        // let content_size = Size {
-        //     width: content_width,
-        //     height: longest_item_size.height * self.row_count + padding.top + padding.bottom,
-        // };
-
-        // let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
-        // let item_height = longest_item_size.height;
-        // let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
-        //     let mut handle = handle.0.borrow_mut();
-        //     handle.last_item_size = Some(ItemSize {
-        //         item: padded_bounds.size,
-        //         contents: content_size,
-        //     });
-        //     handle.deferred_scroll_to_item.take()
-        // });
-
-        // self.interactivity.prepaint(
-        //     global_id,
-        //     inspector_id,
-        //     bounds,
-        //     content_size,
-        //     window,
-        //     cx,
-        //     |style, mut scroll_offset, hitbox, window, cx| {
-        //         let border = style.border_widths.to_pixels(window.rem_size());
-        //         let padding = style
-        //             .padding
-        //             .to_pixels(bounds.size.into(), window.rem_size());
-
-        //         let padded_bounds = Bounds::from_corners(
-        //             bounds.origin + point(border.left + padding.left, border.top),
-        //             bounds.bottom_right() - point(border.right + padding.right, border.bottom),
-        //         );
-
-        //         let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() {
-        //             let mut scroll_state = scroll_handle.0.borrow_mut();
-        //             scroll_state.base_handle.set_bounds(bounds);
-        //             scroll_state.y_flipped
-        //         } else {
-        //             false
-        //         };
-
-        //         if self.item_count > 0 {
-        //             let content_height =
-        //                 item_height * self.item_count + padding.top + padding.bottom;
-        //             let is_scrolled_vertically = !scroll_offset.y.is_zero();
-        //             let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
-        //             if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
-        //                 shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
-        //                 scroll_offset.y = min_vertical_scroll_offset;
-        //             }
-
-        //             let content_width = content_size.width + padding.left + padding.right;
-        //             let is_scrolled_horizontally =
-        //                 can_scroll_horizontally && !scroll_offset.x.is_zero();
-        //             if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
-        //                 shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
-        //                 scroll_offset.x = Pixels::ZERO;
-        //             }
-
-        //             if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item {
-        //                 if y_flipped {
-        //                     ix = self.item_count.saturating_sub(ix + 1);
-        //                 }
-        //                 let list_height = padded_bounds.size.height;
-        //                 let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
-        //                 let item_top = item_height * ix + padding.top;
-        //                 let item_bottom = item_top + item_height;
-        //                 let scroll_top = -updated_scroll_offset.y;
-        //                 let mut scrolled_to_top = false;
-        //                 if item_top < scroll_top + padding.top {
-        //                     scrolled_to_top = true;
-        //                     updated_scroll_offset.y = -(item_top) + padding.top;
-        //                 } else if item_bottom > scroll_top + list_height - padding.bottom {
-        //                     scrolled_to_top = true;
-        //                     updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
-        //                 }
-
-        //                 match scroll_strategy {
-        //                     ScrollStrategy::Top => {}
-        //                     ScrollStrategy::Center => {
-        //                         if scrolled_to_top {
-        //                             let item_center = item_top + item_height / 2.0;
-        //                             let target_scroll_top = item_center - list_height / 2.0;
-
-        //                             if item_top < scroll_top
-        //                                 || item_bottom > scroll_top + list_height
-        //                             {
-        //                                 updated_scroll_offset.y = -target_scroll_top
-        //                                     .max(Pixels::ZERO)
-        //                                     .min(content_height - list_height)
-        //                                     .max(Pixels::ZERO);
-        //                             }
-        //                         }
-        //                     }
-        //                 }
-        //                 scroll_offset = *updated_scroll_offset
-        //             }
-
-        //             let first_visible_element_ix =
-        //                 (-(scroll_offset.y + padding.top) / item_height).floor() as usize;
-        //             let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
-        //                 / item_height)
-        //                 .ceil() as usize;
-        //             let visible_range = first_visible_element_ix
-        //                 ..cmp::min(last_visible_element_ix, self.item_count);
-
-        //             let items = if y_flipped {
-        //                 let flipped_range = self.item_count.saturating_sub(visible_range.end)
-        //                     ..self.item_count.saturating_sub(visible_range.start);
-        //                 let mut items = (self.render_items)(flipped_range, window, cx);
-        //                 items.reverse();
-        //                 items
-        //             } else {
-        //                 (self.render_items)(visible_range.clone(), window, cx)
-        //             };
-
-        //             let content_mask = ContentMask { bounds };
-        //             window.with_content_mask(Some(content_mask), |window| {
-        //                 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
-        //                     let item_origin = padded_bounds.origin
-        //                         + point(
-        //                             if can_scroll_horizontally {
-        //                                 scroll_offset.x + padding.left
-        //                             } else {
-        //                                 scroll_offset.x
-        //                             },
-        //                             item_height * ix + scroll_offset.y + padding.top,
-        //                         );
-        //                     let available_width = if can_scroll_horizontally {
-        //                         padded_bounds.size.width + scroll_offset.x.abs()
-        //                     } else {
-        //                         padded_bounds.size.width
-        //                     };
-        //                     let available_space = size(
-        //                         AvailableSpace::Definite(available_width),
-        //                         AvailableSpace::Definite(item_height),
-        //                     );
-        //                     item.layout_as_root(available_space, window, cx);
-        //                     item.prepaint_at(item_origin, window, cx);
-        //                     frame_state.items.push(item);
-        //                 }
-
-        //                 let bounds = Bounds::new(
-        //                     padded_bounds.origin
-        //                         + point(
-        //                             if can_scroll_horizontally {
-        //                                 scroll_offset.x + padding.left
-        //                             } else {
-        //                                 scroll_offset.x
-        //                             },
-        //                             scroll_offset.y + padding.top,
-        //                         ),
-        //                     padded_bounds.size,
-        //                 );
-        //                 for decoration in &self.decorations {
-        //                     let mut decoration = decoration.as_ref().compute(
-        //                         visible_range.clone(),
-        //                         bounds,
-        //                         item_height,
-        //                         self.item_count,
-        //                         window,
-        //                         cx,
-        //                     );
-        //                     let available_space = size(
-        //                         AvailableSpace::Definite(bounds.size.width),
-        //                         AvailableSpace::Definite(bounds.size.height),
-        //                     );
-        //                     decoration.layout_as_root(available_space, window, cx);
-        //                     decoration.prepaint_at(bounds.origin, window, cx);
-        //                     frame_state.decorations.push(decoration);
-        //                 }
-        //             });
-        //         }
-
-        //         hitbox
-        //     },
-        // )
-        todo!()
+        let can_scroll_horizontally = true;
+
+        let mut column_widths = [Pixels::default(); COLS];
+        let longest_row_size = MeasureContext::new(self).measure_item(
+            AvailableSpace::Definite(bounds.size.width),
+            Some(&mut column_widths),
+            window,
+            cx,
+        );
+
+        // We need to run this for each column:
+        let content_width = padded_bounds.size.width.max(longest_row_size.width);
+
+        let content_size = Size {
+            width: content_width,
+            height: longest_row_size.height * self.row_count + padding.top + padding.bottom,
+        };
+
+        let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
+        let row_height = longest_row_size.height;
+        let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
+            let mut handle = handle.0.borrow_mut();
+            handle.last_row_size = Some(RowSize {
+                row: padded_bounds.size,
+                contents: content_size,
+            });
+            handle.deferred_scroll_to_item.take()
+        });
+
+        let mut rendered_rows = SmallVec::default();
+
+        let hitbox = self.interactivity.prepaint(
+            global_id,
+            inspector_id,
+            bounds,
+            content_size,
+            window,
+            cx,
+            |style, mut scroll_offset, hitbox, window, cx| {
+                let border = style.border_widths.to_pixels(window.rem_size());
+                let padding = style
+                    .padding
+                    .to_pixels(bounds.size.into(), window.rem_size());
+
+                let padded_bounds = Bounds::from_corners(
+                    bounds.origin + point(border.left + padding.left, border.top),
+                    bounds.bottom_right() - point(border.right + padding.right, border.bottom),
+                );
+
+                let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() {
+                    let mut scroll_state = scroll_handle.0.borrow_mut();
+                    scroll_state.base_handle.set_bounds(bounds);
+                    scroll_state.y_flipped
+                } else {
+                    false
+                };
+
+                if self.row_count > 0 {
+                    let content_height = row_height * self.row_count + padding.top + padding.bottom;
+                    let is_scrolled_vertically = !scroll_offset.y.is_zero();
+                    let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
+                    if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
+                        shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
+                        scroll_offset.y = min_vertical_scroll_offset;
+                    }
+
+                    let content_width = content_size.width + padding.left + padding.right;
+                    let is_scrolled_horizontally =
+                        can_scroll_horizontally && !scroll_offset.x.is_zero();
+                    if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
+                        shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
+                        scroll_offset.x = Pixels::ZERO;
+                    }
+
+                    if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item {
+                        if y_flipped {
+                            ix = self.row_count.saturating_sub(ix + 1);
+                        }
+                        let list_height = padded_bounds.size.height;
+                        let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
+                        let item_top = row_height * ix + padding.top;
+                        let item_bottom = item_top + row_height;
+                        let scroll_top = -updated_scroll_offset.y;
+                        let mut scrolled_to_top = false;
+                        if item_top < scroll_top + padding.top {
+                            scrolled_to_top = true;
+                            updated_scroll_offset.y = -(item_top) + padding.top;
+                        } else if item_bottom > scroll_top + list_height - padding.bottom {
+                            scrolled_to_top = true;
+                            updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
+                        }
+
+                        match scroll_strategy {
+                            ScrollStrategy::Top => {}
+                            ScrollStrategy::Center => {
+                                if scrolled_to_top {
+                                    let item_center = item_top + row_height / 2.0;
+                                    let target_scroll_top = item_center - list_height / 2.0;
+
+                                    if item_top < scroll_top
+                                        || item_bottom > scroll_top + list_height
+                                    {
+                                        updated_scroll_offset.y = -target_scroll_top
+                                            .max(Pixels::ZERO)
+                                            .min(content_height - list_height)
+                                            .max(Pixels::ZERO);
+                                    }
+                                }
+                            }
+                        }
+                        scroll_offset = *updated_scroll_offset
+                    }
+
+                    let first_visible_element_ix =
+                        (-(scroll_offset.y + padding.top) / row_height).floor() as usize;
+                    let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
+                        / row_height)
+                        .ceil() as usize;
+                    let visible_range =
+                        first_visible_element_ix..cmp::min(last_visible_element_ix, self.row_count);
+
+                    let rows = if y_flipped {
+                        let flipped_range = self.row_count.saturating_sub(visible_range.end)
+                            ..self.row_count.saturating_sub(visible_range.start);
+                        let mut items = (self.render_rows)(flipped_range, window, cx);
+                        items.reverse();
+                        items
+                    } else {
+                        (self.render_rows)(visible_range.clone(), window, cx)
+                    };
+
+                    let content_mask = ContentMask { bounds };
+                    window.with_content_mask(Some(content_mask), |window| {
+                        let available_width = if can_scroll_horizontally {
+                            padded_bounds.size.width + scroll_offset.x.abs()
+                        } else {
+                            padded_bounds.size.width
+                        };
+                        let available_space = size(
+                            AvailableSpace::Definite(available_width),
+                            AvailableSpace::Definite(row_height),
+                        );
+                        for (mut row, ix) in rows.into_iter().zip(visible_range.clone()) {
+                            let row_origin = padded_bounds.origin
+                                + point(
+                                    if can_scroll_horizontally {
+                                        scroll_offset.x + padding.left
+                                    } else {
+                                        scroll_offset.x
+                                    },
+                                    row_height * ix + scroll_offset.y + padding.top,
+                                );
+
+                            let mut item = render_row(row, column_widths, row_height).into_any();
+
+                            item.layout_as_root(available_space, window, cx);
+                            item.prepaint_at(row_origin, window, cx);
+                            rendered_rows.push(item);
+                        }
+                    });
+                }
+
+                hitbox
+            },
+        );
+        return (hitbox, rendered_rows);
     }
 
     fn paint(
         &mut self,
-        id: Option<&GlobalElementId>,
+        global_id: Option<&GlobalElementId>,
         inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
-        request_layout: &mut Self::RequestLayoutState,
-        prepaint: &mut Self::PrepaintState,
+        _: &mut Self::RequestLayoutState,
+        (hitbox, rendered_rows): &mut Self::PrepaintState,
         window: &mut Window,
         cx: &mut App,
     ) {
-        todo!()
+        self.interactivity.paint(
+            global_id,
+            inspector_id,
+            bounds,
+            hitbox.as_ref(),
+            window,
+            cx,
+            |_, window, cx| {
+                for item in rendered_rows {
+                    item.paint(window, cx);
+                }
+            },
+        )
+    }
+}
+
+const DIVIDER_PADDING_PX: Pixels = px(2.0);
+
+fn render_row<const COLS: usize>(
+    row: [AnyElement; COLS],
+    column_widths: [Pixels; COLS],
+    row_height: Pixels,
+) -> Div {
+    use crate::ParentElement;
+    let mut div = crate::div().flex().flex_row().gap(DIVIDER_PADDING_PX);
+
+    for (ix, cell) in row.into_iter().enumerate() {
+        div = div.child(
+            crate::div()
+                .w(column_widths[ix])
+                .h(row_height)
+                .overflow_hidden()
+                .child(cell),
+        )
+    }
+
+    div
+}
+
+struct MeasureContext<const COLS: usize> {
+    row_count: usize,
+    item_to_measure_index: usize,
+    render_rows:
+        Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
+    sizings: [Length; COLS],
+}
+
+impl<const COLS: usize> MeasureContext<COLS> {
+    fn new(table: &UniformTable<COLS>) -> Self {
+        Self {
+            row_count: table.row_count,
+            item_to_measure_index: table.item_to_measure_index,
+            render_rows: table.render_rows.clone(),
+            sizings: table.sizings,
+        }
+    }
+
+    fn measure_item(
+        &self,
+        table_width: AvailableSpace,
+        column_sizes: Option<&mut [Pixels; COLS]>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Size<Pixels> {
+        if self.row_count == 0 {
+            return Size::default();
+        }
+
+        let item_ix = cmp::min(self.item_to_measure_index, self.row_count - 1);
+        let mut items = (self.render_rows)(item_ix..item_ix + 1, window, cx);
+        let Some(mut item_to_measure) = items.pop() else {
+            return Size::default();
+        };
+        let mut default_column_sizes = [Pixels::default(); COLS];
+        let column_sizes = column_sizes.unwrap_or(&mut default_column_sizes);
+
+        let mut row_height = px(0.0);
+        for i in 0..COLS {
+            let column_available_width = match self.sizings[i] {
+                Length::Definite(definite_length) => match table_width {
+                    AvailableSpace::Definite(pixels) => AvailableSpace::Definite(
+                        definite_length.to_pixels(pixels.into(), window.rem_size()),
+                    ),
+                    AvailableSpace::MinContent => AvailableSpace::MinContent,
+                    AvailableSpace::MaxContent => AvailableSpace::MaxContent,
+                },
+                Length::Auto => AvailableSpace::MaxContent,
+            };
+
+            let column_available_space = size(column_available_width, AvailableSpace::MinContent);
+
+            // todo!: Adjust row sizing to account for inter-column spacing
+            let cell_size = item_to_measure[i].layout_as_root(column_available_space, window, cx);
+            column_sizes[i] = cell_size.width;
+            row_height = row_height.max(cell_size.height);
+        }
+
+        let mut width = Pixels::ZERO;
+
+        for size in *column_sizes {
+            width += size;
+        }
+
+        Size::new(width + (COLS - 1) * DIVIDER_PADDING_PX, row_height)
+    }
+}
+
+impl<const COLS: usize> UniformTable<COLS> {}
+
+/// A handle for controlling the scroll position of a uniform list.
+/// This should be stored in your view and passed to the uniform_list on each frame.
+#[derive(Clone, Debug, Default)]
+pub struct UniformTableScrollHandle(pub Rc<RefCell<UniformTableScrollState>>);
+
+/// Where to place the element scrolled to.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ScrollStrategy {
+    /// Place the element at the top of the list's viewport.
+    Top,
+    /// Attempt to place the element in the middle of the list's viewport.
+    /// May not be possible if there's not enough list items above the item scrolled to:
+    /// in this case, the element will be placed at the closest possible position.
+    Center,
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+/// The size of the item and its contents.
+pub struct RowSize {
+    /// The size of the item.
+    pub row: Size<Pixels>,
+    /// The size of the item's contents, which may be larger than the item itself,
+    /// if the item was bounded by a parent element.
+    pub contents: Size<Pixels>,
+}
+
+#[derive(Clone, Debug, Default)]
+#[allow(missing_docs)]
+pub struct UniformTableScrollState {
+    pub base_handle: ScrollHandle,
+    pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>,
+    /// Size of the item, captured during last layout.
+    pub last_row_size: Option<RowSize>,
+    /// Whether the list was vertically flipped during last layout.
+    pub y_flipped: bool,
+}
+
+impl UniformTableScrollHandle {
+    /// Create a new scroll handle to bind to a uniform list.
+    pub fn new() -> Self {
+        Self(Rc::new(RefCell::new(UniformTableScrollState {
+            base_handle: ScrollHandle::new(),
+            deferred_scroll_to_item: None,
+            last_row_size: None,
+            y_flipped: false,
+        })))
+    }
+
+    /// Scroll the list to the given item index.
+    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
+        self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy));
+    }
+
+    /// Check if the list is flipped vertically.
+    pub fn y_flipped(&self) -> bool {
+        self.0.borrow().y_flipped
+    }
+
+    /// Get the index of the topmost visible child.
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn logical_scroll_top_index(&self) -> usize {
+        let this = self.0.borrow();
+        this.deferred_scroll_to_item
+            .map(|(ix, _)| ix)
+            .unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
     }
 }

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

@@ -34,16 +34,14 @@ impl Component for Table {
                     "Basic",
                     vec![single_example(
                         "Simple Table",
-                        gpui::uniform_table("simple table", 4)
-                            .header(["Name", "Age", "City"])
-                            .rows(move |range, _, _| {
-                                data[range]
-                                    .iter()
-                                    .cloned()
-                                    .map(|arr| arr.map(IntoElement::into_any_element))
-                                    .collect()
-                            })
-                            .into_any_element(),
+                        gpui::uniform_table("simple table", 4, move |range, _, _| {
+                            data[range]
+                                .iter()
+                                .cloned()
+                                .map(|arr| arr.map(IntoElement::into_any_element))
+                                .collect()
+                        })
+                        .into_any_element(),
                     )],
                 )])
                 .into_any_element(),