diff --git a/crates/story/src/table_story.rs b/crates/story/src/table_story.rs index d1b98f78..b5c68b83 100644 --- a/crates/story/src/table_story.rs +++ b/crates/story/src/table_story.rs @@ -301,11 +301,13 @@ impl TableDelegate for StockTableDelegate { } } - fn col_width(&self, col_ix: usize, _: &AppContext) -> Option { - if let Some(_) = self.columns.get(col_ix) { - Some(120.0.into()) + fn col_width(&self, col_ix: usize, _: &AppContext) -> Pixels { + if col_ix < 10 { + 120.0.into() + } else if col_ix < 20 { + 80.0.into() } else { - None + 130.0.into() } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 668f2cfa..dff8d1f4 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -5,6 +5,7 @@ mod icon; mod root; mod styled; mod svg_img; +mod table_row; mod time; mod title_bar; diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs index bc03b7d8..d7bdb1f2 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table.rs @@ -5,6 +5,7 @@ use crate::{ h_flex, popup_menu::PopupMenu, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, + table_row::table_row, theme::ActiveTheme, v_flex, Icon, IconName, Sizable, Size, StyleSized as _, }; @@ -45,19 +46,19 @@ pub enum ColFixed { } #[derive(Debug, Clone, Copy)] -struct ColGroup { - width: Option, - bounds: Bounds, - sort: Option, - fixed: Option, - padding: Option>, +pub(crate) struct ColGroup { + pub(crate) width: Pixels, + pub(crate) bounds: Bounds, + pub(crate) sort: Option, + pub(crate) fixed: Option, + pub(crate) padding: Option>, } #[derive(Clone)] pub(crate) struct DragCol { pub(crate) entity_id: EntityId, pub(crate) name: SharedString, - pub(crate) width: Option, + pub(crate) width: Pixels, pub(crate) col_ix: usize, } @@ -82,7 +83,7 @@ impl Render for DragCol { .border_1() .border_color(cx.theme().border) .shadow_md() - .when_some(self.width, |this, width| this.w(width)) + .w(self.width) .min_w(px(100.)) .max_w(px(450.)) .child(self.name.clone()) @@ -102,7 +103,7 @@ enum SelectionState { pub enum TableEvent { SelectRow(usize), SelectCol(usize), - ColWidthsChanged(Vec>), + ColWidthsChanged(Vec), MoveCol(usize, usize), } @@ -169,7 +170,11 @@ pub trait TableDelegate: Sized + 'static { /// Return None, use auto width. /// /// This is only called when the table initializes. - fn col_width(&self, col_ix: usize, cx: &AppContext) -> Option; + /// + /// Default: 100px + fn col_width(&self, col_ix: usize, cx: &AppContext) -> Pixels { + px(100.) + } /// Return the sort state of the column at the given index. /// @@ -513,7 +518,7 @@ where } let size = size.floor(); - let old_width = self.col_groups[ix].width.unwrap_or_default(); + let old_width = self.col_groups[ix].width; let new_width = size; if new_width < MIN_WIDTH { return; @@ -523,7 +528,7 @@ where if changed_width > px(-1.0) && changed_width < px(1.0) { return; } - self.col_groups[ix].width = Some(new_width.min(MAX_WIDTH)); + self.col_groups[ix].width = new_width.min(MAX_WIDTH); // Resize next col, table not need to resize the right cols. // let next_width = self.col_groups[ix + 1].width.unwrap_or_default(); @@ -619,7 +624,7 @@ where let col_padding = self.col_groups[col_ix].padding; div() - .when_some(col_width, |this, width| this.w(width)) + .w(col_width) .h_full() .flex_shrink_0() .overflow_hidden() @@ -721,7 +726,7 @@ where // sync col widths into real widths for (_, col_group) in view.col_groups.iter_mut().enumerate() { - col_group.width = Some(col_group.bounds.size.width); + col_group.width = col_group.bounds.size.width; } let ix = *ix; @@ -970,12 +975,22 @@ where let is_stripe_row = self.stripe && row_ix % 2 != 0; let is_selected = self.selected_row == Some(row_ix); let view = cx.view().clone(); + let col_groups: Rc> = Rc::new( + self.col_groups + .iter() + .skip(left_cols_count) + .cloned() + .collect(), + ); if row_ix < rows_count { self.delegate .render_tr(row_ix, cx) - .context_menu(move |this, cx: &mut ViewContext| { - view.read(cx).delegate.context_menu(row_ix, this, cx) + .context_menu({ + let view = view.clone(); + move |this, cx: &mut ViewContext| { + view.read(cx).delegate.context_menu(row_ix, this, cx) + } }) .w_full() .h(self.size.table_row_height()) @@ -1014,17 +1029,26 @@ where .flex_1() .h_full() .overflow_hidden() - .children((left_cols_count..cols_count).map(|col_ix| { - self - // Make the row scroll sync with the - // horizontal_scroll_handle to support horizontal scrolling. - .render_col_wrap(col_ix, cx) - .left(horizontal_scroll_handle.offset().x) - .child( - self.render_cell(col_ix, cx) - .child(self.delegate.render_td(row_ix, col_ix, cx)), - ) - })) + .relative() + .child(table_row( + view, + row_ix, + col_groups, + self.horizontal_scroll_handle.clone(), + { + move |table, visible_range: Range, cx| { + visible_range + .map(|col_ix| { + table.render_col_wrap(col_ix, cx).child( + table.render_cell(col_ix, cx).child( + table.delegate.render_td(row_ix, col_ix, cx), + ), + ) + }) + .collect::>() + } + }, + )) .child(self.delegate.render_last_empty_col(cx)), ) // Row selected style diff --git a/crates/ui/src/table_row.rs b/crates/ui/src/table_row.rs new file mode 100644 index 00000000..98d9a2a9 --- /dev/null +++ b/crates/ui/src/table_row.rs @@ -0,0 +1,274 @@ +//! Table row component for render a large number of differently sized columns (Must ensure each column width). +//! +//! Only visible columns are rendered for performance reasons. +//! +//! Inspired by uniform_list to rolate vertically to horizontally. +//! +//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs +use std::{cmp, ops::Range, rc::Rc}; + +use gpui::{ + div, point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Div, Element, ElementId, + Hitbox, InteractiveElement, IntoElement, IsZero as _, Pixels, Render, ScrollHandle, + SharedString, Size, Stateful, StyleRefinement, Styled, View, ViewContext, WindowContext, +}; +use smallvec::SmallVec; + +use crate::table::ColGroup; + +pub(crate) fn table_row( + view: View, + row_ix: usize, + col_groups: Rc>, + scroll_handle: ScrollHandle, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, +) -> TableRow +where + R: IntoElement, + V: Render, +{ + let id = ElementId::NamedInteger(SharedString::from("table-row"), row_ix); + + let render_range = move |range, cx: &mut WindowContext| { + view.update(cx, |this, cx| { + f(this, range, cx) + .into_iter() + .map(|component| component.into_any_element()) + .collect() + }) + }; + + TableRow { + id: id.clone(), + base: div().id(id).size_full(), + scroll_handle, + cols_count: col_groups.len(), + col_groups, + render_cols: Box::new(render_range), + } +} + +pub struct TableRow { + id: ElementId, + base: Stateful
, + scroll_handle: ScrollHandle, + // scroll_handle: ScrollHandle, + cols_count: usize, + col_groups: Rc>, + render_cols: + Box Fn(Range, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>, +} + +impl Styled for TableRow { + fn style(&mut self) -> &mut StyleRefinement { + self.base.style() + } +} + +/// Frame state used by the [TableRow]. +pub struct TableRowFrameState { + cols: SmallVec<[AnyElement; 32]>, + // decorations: SmallVec<[AnyElement; 1]>, +} + +impl TableRow { + #[allow(dead_code)] + fn measure_col(&self, cx: &mut WindowContext) -> Size { + if self.cols_count == 0 { + return Size::default(); + } + + let col_ix = self.cols_count - 1; + let mut items = (self.render_cols)(col_ix..col_ix + 1, cx); + let Some(mut item_to_measure) = items.pop() else { + return Size::default(); + }; + + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + item_to_measure.layout_as_root(available_space, cx) + } +} + +impl IntoElement for TableRow { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for TableRow { + type RequestLayoutState = TableRowFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn request_layout( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + let (layout_id, _) = self.base.request_layout(global_id, cx); + + ( + layout_id, + TableRowFrameState { + cols: SmallVec::new(), + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + bounds: gpui::Bounds, + frame_state: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Self::PrepaintState { + let style = self.base.interactivity().compute_style(global_id, None, cx); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.lower_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + // This is important to get the width of each column to measure the visible columns. + // + // So the col must have a width. + let col_widths = self + .col_groups + .iter() + .map(|col| col.width.0) + .collect::>(); + + let content_height = padded_bounds.size.height; + let content_width = px(col_widths.iter().sum::()); + let content_size = Size { + width: content_width, + height: content_height, + }; + + self.base.interactivity().prepaint( + global_id, + bounds, + content_size, + cx, + |style, _, hitbox, cx| { + let mut scroll_offset = self.scroll_handle.offset(); + // dbg!(&scroll_offset); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top), + bounds.lower_right() - point(border.right + padding.right, border.bottom), + ); + + if self.cols_count > 0 { + let is_scrolled_horizontally = !scroll_offset.x.is_zero(); + let min_horizontal_scroll_offset = padded_bounds.size.width - content_width; + if is_scrolled_horizontally && scroll_offset.x < min_horizontal_scroll_offset { + scroll_offset.x = min_horizontal_scroll_offset; + } + scroll_offset.y = Pixels::ZERO; + + // Calculate the first and last visible element indices. + let mut cumulative_width = 0.0; + let mut first_visible_element_ix = 0; + for (i, &width) in col_widths.iter().enumerate() { + cumulative_width += width; + if cumulative_width > -(scroll_offset.x + padding.left).0 { + first_visible_element_ix = i; + break; + } + } + + cumulative_width = 0.0; + let mut last_visible_element_ix = 0; + for (i, &width) in col_widths.iter().enumerate() { + cumulative_width += width; + if cumulative_width > (-scroll_offset.x + padded_bounds.size.width).0 { + last_visible_element_ix = i + 1; + break; + } + } + if last_visible_element_ix == 0 { + last_visible_element_ix = self.cols_count; + } else { + last_visible_element_ix += 1; + } + let visible_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.cols_count); + + let items = (self.render_cols)(visible_range.clone(), cx); + + let content_mask = ContentMask { bounds }; + cx.with_content_mask(Some(content_mask), |cx| { + for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { + let item_x = px(col_widths.iter().take(ix).sum::()); + + let item_origin = padded_bounds.origin + + point(item_x + scroll_offset.x + padding.left, padding.top); + // println!("{}, {}", item_origin.x, item_origin.y); + let available_height = padded_bounds.size.height; + let col_width = col_widths[ix]; + let available_space = size( + AvailableSpace::Definite(px(col_width)), + AvailableSpace::Definite(available_height), + ); + item.layout_as_root(available_space, cx); + item.prepaint_at(item_origin, cx); + frame_state.cols.push(item); + } + + // let bounds = Bounds::new( + // padded_bounds.origin + // + point(scroll_offset.x + padding.left, scroll_offset.y), + // padded_bounds.size, + // ); + // for decoration in &self.decorations { + // let mut decoration = decoration.as_ref().compute( + // visible_range.clone(), + // bounds, + // item_height, + // self.item_count, + // cx, + // ); + // let available_space = size( + // AvailableSpace::Definite(bounds.size.width), + // AvailableSpace::Definite(bounds.size.height), + // ); + // decoration.layout_as_root(available_space, cx); + // decoration.prepaint_at(bounds.origin, cx); + // frame_state.decorations.push(decoration); + // } + }); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + bounds: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.base + .interactivity() + .paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| { + for col in &mut request_layout.cols { + col.paint(cx); + } + }) + } +}