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"
Anthony and Ben Kunkle created
Co-authored-by: Ben Kunkle <ben@zed.dev>
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(-)
@@ -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"
@@ -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(),
)
}
@@ -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();
+ });
+}
@@ -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)
}
}
@@ -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(),