1use std::{ops::Range, rc::Rc};
2
3use crate::{
4 AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope,
5 Context, Div, DraggedColumn, ElementId, FixedWidth as _, FluentBuilder as _, HeaderResizeInfo,
6 Icon, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Pagination,
7 ParentElement, Pixels, RESIZE_DIVIDER_WIDTH, RedistributableColumnsState, RegisterComponent,
8 RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement,
9 Styled, StyledExt as _, StyledTypography, TableResizeBehavior, Window, WithScrollbar,
10 bind_redistributable_columns, div, example_group_with_title, h_flex, px,
11 render_column_resize_divider, render_redistributable_columns_resize_handles, single_example,
12 styles::semantic,
13 table_row::{IntoTableRow as _, TableRow},
14 v_flex,
15};
16use gpui::{
17 AbsoluteLength, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, FontWeight,
18 Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, ScrollHandle,
19 Stateful, UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,
20};
21
22pub mod table_row;
23#[cfg(test)]
24mod tests;
25
26pub type UncheckedTableRow<T> = Vec<T>;
29
30pub struct ResizableColumnsState {
35 initial_widths: TableRow<AbsoluteLength>,
36 widths: TableRow<AbsoluteLength>,
37 resize_behavior: TableRow<TableResizeBehavior>,
38}
39
40impl ResizableColumnsState {
41 pub fn new(
42 cols: usize,
43 initial_widths: Vec<impl Into<AbsoluteLength>>,
44 resize_behavior: Vec<TableResizeBehavior>,
45 ) -> Self {
46 let widths: TableRow<AbsoluteLength> = initial_widths
47 .into_iter()
48 .map(Into::into)
49 .collect::<Vec<_>>()
50 .into_table_row(cols);
51 Self {
52 initial_widths: widths.clone(),
53 widths,
54 resize_behavior: resize_behavior.into_table_row(cols),
55 }
56 }
57
58 pub fn cols(&self) -> usize {
59 self.widths.cols()
60 }
61
62 pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
63 &self.resize_behavior
64 }
65
66 pub(crate) fn on_drag_move(
67 &mut self,
68 drag_event: &DragMoveEvent<DraggedColumn>,
69 window: &mut Window,
70 cx: &mut Context<Self>,
71 ) {
72 let col_idx = drag_event.drag(cx).col_idx;
73 let rem_size = window.rem_size();
74 let drag_x = drag_event.event.position.x - drag_event.bounds.left();
75
76 let left_edge: Pixels = self.widths.as_slice()[..col_idx]
77 .iter()
78 .map(|width| width.to_pixels(rem_size))
79 .fold(px(0.), |acc, x| acc + x);
80
81 let new_width = drag_x - left_edge;
82 let new_width = self.apply_min_size(new_width, self.resize_behavior[col_idx], rem_size);
83
84 self.widths[col_idx] = AbsoluteLength::Pixels(new_width);
85 cx.notify();
86 }
87
88 pub fn set_column_configuration(
89 &mut self,
90 col_idx: usize,
91 width: impl Into<AbsoluteLength>,
92 resize_behavior: TableResizeBehavior,
93 ) {
94 let width = width.into();
95 self.initial_widths[col_idx] = width;
96 self.widths[col_idx] = width;
97 self.resize_behavior[col_idx] = resize_behavior;
98 }
99
100 pub fn reset_column_to_initial_width(&mut self, col_idx: usize) {
101 self.widths[col_idx] = self.initial_widths[col_idx];
102 }
103
104 fn apply_min_size(
105 &self,
106 width: Pixels,
107 behavior: TableResizeBehavior,
108 rem_size: Pixels,
109 ) -> Pixels {
110 match behavior.min_size() {
111 Some(min_rems) => {
112 let min_px = rem_size * min_rems;
113 width.max(min_px)
114 }
115 None => width,
116 }
117 }
118}
119
120struct UniformListData {
121 render_list_of_rows_fn:
122 Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
123 element_id: ElementId,
124 row_count: usize,
125}
126
127struct VariableRowHeightListData {
128 render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
130 list_state: ListState,
131 row_count: usize,
132}
133
134enum TableContents {
135 Vec(Vec<TableRow<AnyElement>>),
136 UniformList(UniformListData),
137 VariableRowHeightList(VariableRowHeightListData),
138}
139
140impl TableContents {
141 fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
142 match self {
143 TableContents::Vec(rows) => Some(rows),
144 TableContents::UniformList(_) => None,
145 TableContents::VariableRowHeightList(_) => None,
146 }
147 }
148
149 fn len(&self) -> usize {
150 match self {
151 TableContents::Vec(rows) => rows.len(),
152 TableContents::UniformList(data) => data.row_count,
153 TableContents::VariableRowHeightList(data) => data.row_count,
154 }
155 }
156
157 fn is_empty(&self) -> bool {
158 self.len() == 0
159 }
160}
161
162pub struct TableInteractionState {
163 pub focus_handle: FocusHandle,
164 pub scroll_handle: UniformListScrollHandle,
165 pub horizontal_scroll_handle: ScrollHandle,
166 pub custom_scrollbar: Option<Scrollbars>,
167}
168
169impl TableInteractionState {
170 pub fn new(cx: &mut App) -> Self {
171 Self {
172 focus_handle: cx.focus_handle(),
173 scroll_handle: UniformListScrollHandle::new(),
174 horizontal_scroll_handle: ScrollHandle::new(),
175 custom_scrollbar: None,
176 }
177 }
178
179 pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
180 self.custom_scrollbar = Some(custom_scrollbar);
181 self
182 }
183
184 pub fn scroll_offset(&self) -> Point<Pixels> {
185 self.scroll_handle.offset()
186 }
187
188 pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
189 self.scroll_handle.set_offset(offset);
190 }
191
192 pub fn listener<E: ?Sized>(
193 this: &Entity<Self>,
194 f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
195 ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
196 let view = this.downgrade();
197 move |e: &E, window: &mut Window, cx: &mut App| {
198 view.update(cx, |view, cx| f(view, e, window, cx)).ok();
199 }
200 }
201}
202
203pub enum ColumnWidthConfig {
204 Static {
206 widths: StaticColumnWidths,
207 table_width: Option<DefiniteLength>,
209 },
210 Redistributable {
213 columns_state: Entity<RedistributableColumnsState>,
214 table_width: Option<DefiniteLength>,
215 },
216 Resizable(Entity<ResizableColumnsState>),
219}
220
221pub enum StaticColumnWidths {
222 Auto,
224 Explicit(TableRow<DefiniteLength>),
226}
227
228impl ColumnWidthConfig {
229 pub fn auto() -> Self {
231 ColumnWidthConfig::Static {
232 widths: StaticColumnWidths::Auto,
233 table_width: None,
234 }
235 }
236
237 pub fn redistributable(columns_state: Entity<RedistributableColumnsState>) -> Self {
239 ColumnWidthConfig::Redistributable {
240 columns_state,
241 table_width: None,
242 }
243 }
244
245 pub fn auto_with_table_width(width: impl Into<DefiniteLength>) -> Self {
247 ColumnWidthConfig::Static {
248 widths: StaticColumnWidths::Auto,
249 table_width: Some(width.into()),
250 }
251 }
252
253 pub fn explicit<T: Into<DefiniteLength>>(widths: Vec<T>) -> Self {
255 let cols = widths.len();
256 ColumnWidthConfig::Static {
257 widths: StaticColumnWidths::Explicit(
258 widths
259 .into_iter()
260 .map(Into::into)
261 .collect::<Vec<_>>()
262 .into_table_row(cols),
263 ),
264 table_width: None,
265 }
266 }
267
268 pub fn widths_to_render(&self, cx: &App) -> Option<TableRow<Length>> {
270 match self {
271 ColumnWidthConfig::Static {
272 widths: StaticColumnWidths::Auto,
273 ..
274 } => None,
275 ColumnWidthConfig::Static {
276 widths: StaticColumnWidths::Explicit(widths),
277 ..
278 } => Some(widths.map_cloned(Length::Definite)),
279 ColumnWidthConfig::Redistributable {
280 columns_state: entity,
281 ..
282 } => Some(entity.read(cx).widths_to_render()),
283 ColumnWidthConfig::Resizable(entity) => {
284 let state = entity.read(cx);
285 Some(
286 state
287 .widths
288 .map_cloned(|abs| Length::Definite(DefiniteLength::Absolute(abs))),
289 )
290 }
291 }
292 }
293
294 pub fn table_width(&self, window: &Window, cx: &App) -> Option<Length> {
296 match self {
297 ColumnWidthConfig::Static { table_width, .. }
298 | ColumnWidthConfig::Redistributable { table_width, .. } => {
299 table_width.map(Length::Definite)
300 }
301 ColumnWidthConfig::Resizable(entity) => {
302 let state = entity.read(cx);
303 let rem_size = window.rem_size();
304 let total: Pixels = state
305 .widths
306 .as_slice()
307 .iter()
308 .map(|abs| abs.to_pixels(rem_size))
309 .fold(px(0.), |acc, x| acc + x);
310 Some(Length::Definite(DefiniteLength::Absolute(
311 AbsoluteLength::Pixels(total),
312 )))
313 }
314 }
315 }
316
317 pub fn list_horizontal_sizing(
319 &self,
320 window: &Window,
321 cx: &App,
322 ) -> ListHorizontalSizingBehavior {
323 match self {
324 ColumnWidthConfig::Resizable(_) => ListHorizontalSizingBehavior::FitList,
325 _ => match self.table_width(window, cx) {
326 Some(_) => ListHorizontalSizingBehavior::Unconstrained,
327 None => ListHorizontalSizingBehavior::FitList,
328 },
329 }
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum SortDirection {
336 Ascending,
337 Descending,
338}
339
340#[derive(RegisterComponent, IntoElement)]
342pub struct Table {
343 striped: bool,
344 show_row_borders: bool,
345 show_row_hover: bool,
346 headers: Option<TableRow<AnyElement>>,
347 rows: TableContents,
348 interaction_state: Option<WeakEntity<TableInteractionState>>,
349 column_width_config: ColumnWidthConfig,
350 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
351 use_ui_font: bool,
352 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
353 cols: usize,
355 disable_base_cell_style: bool,
356}
357
358impl Table {
359 pub fn new(cols: usize) -> Self {
361 Self {
362 cols,
363 striped: false,
364 show_row_borders: true,
365 show_row_hover: true,
366 headers: None,
367 rows: TableContents::Vec(Vec::new()),
368 interaction_state: None,
369 map_row: None,
370 use_ui_font: true,
371 empty_table_callback: None,
372 disable_base_cell_style: false,
373 column_width_config: ColumnWidthConfig::auto(),
374 }
375 }
376
377 pub fn disable_base_style(mut self) -> Self {
382 self.disable_base_cell_style = true;
383 self
384 }
385
386 pub fn uniform_list(
391 mut self,
392 id: impl Into<ElementId>,
393 row_count: usize,
394 render_item_fn: impl Fn(
395 Range<usize>,
396 &mut Window,
397 &mut App,
398 ) -> Vec<UncheckedTableRow<AnyElement>>
399 + 'static,
400 ) -> Self {
401 self.rows = TableContents::UniformList(UniformListData {
402 element_id: id.into(),
403 row_count,
404 render_list_of_rows_fn: Box::new(render_item_fn),
405 });
406 self
407 }
408
409 pub fn variable_row_height_list(
419 mut self,
420 row_count: usize,
421 list_state: ListState,
422 render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
423 ) -> Self {
424 self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
425 render_row_fn: Box::new(render_row_fn),
426 list_state,
427 row_count,
428 });
429 self
430 }
431
432 pub fn striped(mut self) -> Self {
434 self.striped = true;
435 self
436 }
437
438 pub fn hide_row_borders(mut self) -> Self {
440 self.show_row_borders = false;
441 self
442 }
443
444 pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
449 self.column_width_config = ColumnWidthConfig::auto_with_table_width(width);
450 self
451 }
452
453 pub fn width_config(mut self, config: ColumnWidthConfig) -> Self {
455 self.column_width_config = config;
456 self
457 }
458
459 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
466 self.interaction_state = Some(interaction_state.downgrade());
467 self
468 }
469
470 pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
471 self.headers = Some(
472 headers
473 .into_table_row(self.cols)
474 .map(IntoElement::into_any_element),
475 );
476 self
477 }
478
479 pub fn sortable_header(
490 mut self,
491 headers: UncheckedTableRow<impl Into<SharedString>>,
492 sort_column: Option<usize>,
493 direction: SortDirection,
494 on_sort: impl Fn(usize, &mut Window, &mut App) + 'static,
495 ) -> Self {
496 let on_sort = Rc::new(on_sort);
497 let cells: Vec<AnyElement> = headers
498 .into_iter()
499 .enumerate()
500 .map(|(column, label)| {
501 let is_active = sort_column == Some(column);
502 let icon = if is_active {
503 match direction {
504 SortDirection::Ascending => IconName::ChevronUp,
505 SortDirection::Descending => IconName::ChevronDown,
506 }
507 } else {
508 IconName::ChevronUpDown
509 };
510 let on_sort = on_sort.clone();
511 h_flex()
512 .id(("table-sort-header", column as u64))
513 .items_center()
514 .gap_1()
515 .cursor_pointer()
516 .child(label.into())
517 .child(Icon::new(icon).size(IconSize::XSmall).color(if is_active {
518 Color::Default
519 } else {
520 Color::Muted
521 }))
522 .on_click(move |_, window, cx| on_sort(column, window, cx))
523 .into_any_element()
524 })
525 .collect();
526 self.headers = Some(
527 cells
528 .into_table_row(self.cols)
529 .map(IntoElement::into_any_element),
530 );
531 self
532 }
533
534 pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
535 if let Some(rows) = self.rows.rows_mut() {
536 rows.push(
537 items
538 .into_table_row(self.cols)
539 .map(IntoElement::into_any_element),
540 );
541 }
542 self
543 }
544
545 pub fn no_ui_font(mut self) -> Self {
546 self.use_ui_font = false;
547 self
548 }
549
550 pub fn map_row(
551 mut self,
552 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
553 ) -> Self {
554 self.map_row = Some(Rc::new(callback));
555 self
556 }
557
558 pub fn hide_row_hover(mut self) -> Self {
561 self.show_row_hover = false;
562 self
563 }
564
565 pub fn empty_table_callback(
567 mut self,
568 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
569 ) -> Self {
570 self.empty_table_callback = Some(Rc::new(callback));
571 self
572 }
573}
574
575fn base_cell_style(width: Option<Length>) -> Div {
576 div()
577 .px_1p5()
578 .when_some(width, |this, width| this.w(width))
579 .when(width.is_none(), |this| this.flex_1())
580 .whitespace_nowrap()
581 .text_ellipsis()
582 .overflow_hidden()
583}
584
585fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
586 base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
587}
588
589pub fn render_table_row(
590 row_index: usize,
591 items: TableRow<impl IntoElement>,
592 table_context: TableRenderContext,
593 window: &mut Window,
594 cx: &mut App,
595) -> AnyElement {
596 let is_striped = table_context.striped;
597 let is_last = row_index == table_context.total_row_count - 1;
598 let bg = if is_striped {
599 if row_index % 2 == 1 {
600 Some(semantic::elevated_surface(cx))
601 } else {
602 Some(semantic::surface(cx))
603 }
604 } else {
605 None
606 };
607 let cols = items.cols();
608 let column_widths = table_context
609 .column_widths
610 .map_or(vec![None; cols].into_table_row(cols), |widths| {
611 widths.map(Some)
612 });
613
614 let mut row = div()
615 .flex()
618 .flex_row()
619 .id(("table_row", row_index))
620 .size_full()
621 .when_some(bg, |row, bg| row.bg(bg))
622 .when(table_context.show_row_hover, |row| {
623 row.hover(|s| s.bg(semantic::hover_bg(cx)))
624 })
625 .when(!is_striped && table_context.show_row_borders, |row| {
626 row.border_b_1()
627 .border_color(transparent_black())
628 .when(!is_last, |row| row.border_color(semantic::border_muted(cx)))
629 });
630
631 row = row.children(
632 items
633 .map(IntoElement::into_any_element)
634 .into_vec()
635 .into_iter()
636 .zip(column_widths.into_vec())
637 .map(|(cell, width)| {
638 if table_context.disable_base_cell_style {
639 div()
640 .when_some(width, |this, width| this.w(width))
641 .when(width.is_none(), |this| this.flex_1())
642 .overflow_hidden()
643 .child(cell)
644 } else {
645 base_cell_style_text(width, table_context.use_ui_font, cx)
646 .px_4()
647 .py_3()
648 .child(cell)
649 }
650 }),
651 );
652
653 let row = if let Some(map_row) = table_context.map_row {
654 map_row((row_index, row), window, cx)
655 } else {
656 row.into_any_element()
657 };
658
659 div().size_full().child(row).into_any_element()
660}
661
662pub fn render_table_header(
663 headers: TableRow<impl IntoElement>,
664 table_context: TableRenderContext,
665 resize_info: Option<HeaderResizeInfo>,
666 entity_id: Option<EntityId>,
667 cx: &mut App,
668) -> impl IntoElement {
669 let cols = headers.cols();
670 let column_widths = table_context
671 .column_widths
672 .map_or(vec![None; cols].into_table_row(cols), |widths| {
673 widths.map(Some)
674 });
675
676 let element_id = entity_id
677 .map(|entity| entity.to_string())
678 .unwrap_or_default();
679
680 let shared_element_id: SharedString = format!("table-{}", element_id).into();
681
682 div()
683 .flex()
684 .flex_row()
685 .items_center()
686 .w_full()
687 .bg(semantic::elevated_surface(cx))
688 .border_b_1()
689 .border_color(semantic::border_muted(cx))
690 .children(
691 headers
692 .into_vec()
693 .into_iter()
694 .enumerate()
695 .zip(column_widths.into_vec())
696 .map(|((header_idx, h), width)| {
697 base_cell_style_text(width, table_context.use_ui_font, cx)
698 .px_4()
699 .py_3()
700 .font_weight(FontWeight::SEMIBOLD)
701 .child(h)
702 .id(ElementId::NamedInteger(
703 shared_element_id.clone(),
704 header_idx as u64,
705 ))
706 .when_some(resize_info.as_ref().cloned(), |this, info| {
707 if info.resize_behavior[header_idx].is_resizable() {
708 this.on_click(move |event, window, cx| {
709 if event.click_count() > 1 {
710 info.reset_column(header_idx, window, cx);
711 }
712 })
713 } else {
714 this
715 }
716 })
717 }),
718 )
719}
720
721#[derive(Clone)]
722pub struct TableRenderContext {
723 pub striped: bool,
724 pub show_row_borders: bool,
725 pub show_row_hover: bool,
726 pub total_row_count: usize,
727 pub column_widths: Option<TableRow<Length>>,
728 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
729 pub use_ui_font: bool,
730 pub disable_base_cell_style: bool,
731}
732
733impl TableRenderContext {
734 fn new(table: &Table, cx: &App) -> Self {
735 Self {
736 striped: table.striped,
737 show_row_borders: table.show_row_borders,
738 show_row_hover: table.show_row_hover,
739 total_row_count: table.rows.len(),
740 column_widths: table.column_width_config.widths_to_render(cx),
741 map_row: table.map_row.clone(),
742 use_ui_font: table.use_ui_font,
743 disable_base_cell_style: table.disable_base_cell_style,
744 }
745 }
746
747 pub fn for_column_widths(column_widths: Option<TableRow<Length>>, use_ui_font: bool) -> Self {
748 Self {
749 striped: false,
750 show_row_borders: true,
751 show_row_hover: true,
752 total_row_count: 0,
753 column_widths,
754 map_row: None,
755 use_ui_font,
756 disable_base_cell_style: false,
757 }
758 }
759}
760
761fn render_resize_handles_resizable(
762 columns_state: &Entity<ResizableColumnsState>,
763 window: &mut Window,
764 cx: &mut App,
765) -> AnyElement {
766 let (widths, resize_behavior) = {
767 let state = columns_state.read(cx);
768 (state.widths.clone(), state.resize_behavior.clone())
769 };
770
771 let rem_size = window.rem_size();
772 let resize_behavior = Rc::new(resize_behavior);
773 let n_cols = widths.cols();
774 let mut dividers: Vec<AnyElement> = Vec::with_capacity(n_cols);
775 let mut accumulated_px = px(0.);
776
777 for col_idx in 0..n_cols {
778 let col_width_px = widths[col_idx].to_pixels(rem_size);
779 accumulated_px = accumulated_px + col_width_px;
780
781 {
785 let divider_left = if col_idx + 1 == n_cols {
786 accumulated_px - px(RESIZE_DIVIDER_WIDTH)
787 } else {
788 accumulated_px
789 };
790 let divider = div().id(col_idx).absolute().top_0().left(divider_left);
791 let entity_id = columns_state.entity_id();
792 let on_reset: Rc<dyn Fn(&mut Window, &mut App)> = {
793 let columns_state = columns_state.clone();
794 Rc::new(move |_window, cx| {
795 columns_state.update(cx, |state, cx| {
796 state.reset_column_to_initial_width(col_idx);
797 cx.notify();
798 });
799 })
800 };
801 dividers.push(render_column_resize_divider(
802 divider,
803 col_idx,
804 resize_behavior[col_idx].is_resizable(),
805 entity_id,
806 on_reset,
807 None,
808 window,
809 cx,
810 ));
811 }
812 }
813
814 div()
815 .id("resize-handles")
816 .absolute()
817 .inset_0()
818 .w_full()
819 .children(dividers)
820 .into_any_element()
821}
822
823impl RenderOnce for Table {
824 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
825 let table_context = TableRenderContext::new(&self, cx);
826 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
827
828 let header_resize_info =
829 interaction_state
830 .as_ref()
831 .and_then(|_| match &self.column_width_config {
832 ColumnWidthConfig::Redistributable { columns_state, .. } => {
833 Some(HeaderResizeInfo::from_redistributable(columns_state, cx))
834 }
835 ColumnWidthConfig::Resizable(entity) => {
836 Some(HeaderResizeInfo::from_resizable(entity, cx))
837 }
838 _ => None,
839 });
840
841 let table_width = self.column_width_config.table_width(window, cx);
842 let horizontal_sizing = self.column_width_config.list_horizontal_sizing(window, cx);
843 let no_rows_rendered = self.rows.is_empty();
844 let variable_list_state = if let TableContents::VariableRowHeightList(data) = &self.rows {
845 Some(data.list_state.clone())
846 } else {
847 None
848 };
849
850 let (redistributable_entity, resizable_entity, resize_handles) =
851 if let Some(_) = interaction_state.as_ref() {
852 match &self.column_width_config {
853 ColumnWidthConfig::Redistributable { columns_state, .. } => (
854 Some(columns_state.clone()),
855 None,
856 Some(render_redistributable_columns_resize_handles(
857 columns_state,
858 window,
859 cx,
860 )),
861 ),
862 ColumnWidthConfig::Resizable(entity) => (
863 None,
864 Some(entity.clone()),
865 Some(render_resize_handles_resizable(entity, window, cx)),
866 ),
867 _ => (None, None, None),
868 }
869 } else {
870 (None, None, None)
871 };
872
873 let is_resizable = resizable_entity.is_some();
874
875 let table = div()
876 .when_some(table_width, |this, width| this.w(width))
877 .h_full()
878 .v_flex()
879 .when_some(self.headers.take(), |this, headers| {
880 this.child(render_table_header(
881 headers,
882 table_context.clone(),
883 header_resize_info,
884 interaction_state.as_ref().map(Entity::entity_id),
885 cx,
886 ))
887 })
888 .when_some(redistributable_entity, |this, widths| {
889 bind_redistributable_columns(this, widths)
890 })
891 .when_some(resizable_entity, |this, entity| {
892 this.on_drag_move::<DraggedColumn>(move |event, window, cx| {
893 if event.drag(cx).state_id != entity.entity_id() {
894 return;
895 }
896 entity.update(cx, |state, cx| state.on_drag_move(event, window, cx));
897 })
898 })
899 .child({
900 let content = div()
901 .flex_grow()
902 .w_full()
903 .relative()
904 .overflow_hidden()
905 .map(|parent| match self.rows {
906 TableContents::Vec(items) => {
907 parent.children(items.into_iter().enumerate().map(|(index, row)| {
908 div().child(render_table_row(
909 index,
910 row,
911 table_context.clone(),
912 window,
913 cx,
914 ))
915 }))
916 }
917 TableContents::UniformList(uniform_list_data) => parent.child(
918 uniform_list(
919 uniform_list_data.element_id,
920 uniform_list_data.row_count,
921 {
922 let render_item_fn = uniform_list_data.render_list_of_rows_fn;
923 move |range: Range<usize>, window, cx| {
924 let elements = render_item_fn(range.clone(), window, cx)
925 .into_iter()
926 .map(|raw_row| raw_row.into_table_row(self.cols))
927 .collect::<Vec<_>>();
928 elements
929 .into_iter()
930 .zip(range)
931 .map(|(row, row_index)| {
932 render_table_row(
933 row_index,
934 row,
935 table_context.clone(),
936 window,
937 cx,
938 )
939 })
940 .collect()
941 }
942 },
943 )
944 .size_full()
945 .flex_grow()
946 .with_sizing_behavior(ListSizingBehavior::Auto)
947 .with_horizontal_sizing_behavior(horizontal_sizing)
948 .when_some(
949 interaction_state.as_ref(),
950 |this, state| {
951 this.track_scroll(
952 &state.read_with(cx, |s, _| s.scroll_handle.clone()),
953 )
954 },
955 ),
956 ),
957 TableContents::VariableRowHeightList(variable_list_data) => parent.child(
958 list(variable_list_data.list_state.clone(), {
959 let render_item_fn = variable_list_data.render_row_fn;
960 move |row_index: usize, window: &mut Window, cx: &mut App| {
961 let row = render_item_fn(row_index, window, cx)
962 .into_table_row(self.cols);
963 render_table_row(
964 row_index,
965 row,
966 table_context.clone(),
967 window,
968 cx,
969 )
970 }
971 })
972 .size_full()
973 .flex_grow()
974 .with_sizing_behavior(ListSizingBehavior::Auto),
975 ),
976 })
977 .when_some(resize_handles, |parent, handles| parent.child(handles));
978
979 content.into_any_element()
980 })
981 .when_some(
982 no_rows_rendered
983 .then_some(self.empty_table_callback)
984 .flatten(),
985 |this, callback| {
986 this.child(
987 h_flex()
988 .size_full()
989 .p_3()
990 .items_start()
991 .justify_center()
992 .child(callback(window, cx)),
993 )
994 },
995 );
996
997 if let Some(state) = interaction_state.as_ref() {
998 let content = if is_resizable {
1000 let mut h_scroll_container = div()
1001 .id("table-h-scroll")
1002 .overflow_x_scroll()
1003 .flex_grow()
1004 .h_full()
1005 .track_scroll(&state.read(cx).horizontal_scroll_handle)
1006 .child(table);
1007 h_scroll_container.style().restrict_scroll_to_axis = Some(true);
1008 div().size_full().child(h_scroll_container)
1009 } else {
1010 table
1011 };
1012
1013 let scrollbars = state
1015 .read(cx)
1016 .custom_scrollbar
1017 .clone()
1018 .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
1019 let mut content = if let Some(list_state) = variable_list_state {
1020 content.custom_scrollbars(scrollbars.tracked_scroll_handle(&list_state), window, cx)
1021 } else {
1022 content.custom_scrollbars(
1023 scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
1024 window,
1025 cx,
1026 )
1027 };
1028
1029 if is_resizable {
1031 content = content.custom_scrollbars(
1032 Scrollbars::new(ScrollAxes::Horizontal)
1033 .tracked_scroll_handle(&state.read(cx).horizontal_scroll_handle),
1034 window,
1035 cx,
1036 );
1037 }
1038 content.style().restrict_scroll_to_axis = Some(true);
1039
1040 content
1041 .track_focus(&state.read(cx).focus_handle)
1042 .id(("table", state.entity_id()))
1043 .into_any_element()
1044 } else {
1045 table.into_any_element()
1046 }
1047 }
1048}
1049
1050impl Component for Table {
1051 fn scope() -> ComponentScope {
1052 ComponentScope::Layout
1053 }
1054
1055 fn description() -> Option<&'static str> {
1056 Some("A table component for displaying data in rows and columns with optional styling.")
1057 }
1058
1059 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1060 Some(
1061 v_flex()
1062 .gap_6()
1063 .children(vec![
1064 example_group_with_title(
1065 "Basic Tables",
1066 vec![
1067 single_example(
1068 "Simple Table",
1069 Table::new(3)
1070 .width(px(400.))
1071 .header(vec!["Name", "Age", "City"])
1072 .row(vec!["Alice", "28", "New York"])
1073 .row(vec!["Bob", "32", "San Francisco"])
1074 .row(vec!["Charlie", "25", "London"])
1075 .into_any_element(),
1076 ),
1077 single_example(
1078 "Two Column Table",
1079 Table::new(2)
1080 .header(vec!["Category", "Value"])
1081 .width(px(300.))
1082 .row(vec!["Revenue", "$100,000"])
1083 .row(vec!["Expenses", "$75,000"])
1084 .row(vec!["Profit", "$25,000"])
1085 .into_any_element(),
1086 ),
1087 ],
1088 ),
1089 example_group_with_title(
1090 "Styled Tables",
1091 vec![
1092 single_example(
1093 "Default",
1094 Table::new(3)
1095 .width(px(400.))
1096 .header(vec!["Product", "Price", "Stock"])
1097 .row(vec!["Laptop", "$999", "In Stock"])
1098 .row(vec!["Phone", "$599", "Low Stock"])
1099 .row(vec!["Tablet", "$399", "Out of Stock"])
1100 .into_any_element(),
1101 ),
1102 single_example(
1103 "Striped",
1104 Table::new(3)
1105 .width(px(400.))
1106 .striped()
1107 .header(vec!["Product", "Price", "Stock"])
1108 .row(vec!["Laptop", "$999", "In Stock"])
1109 .row(vec!["Phone", "$599", "Low Stock"])
1110 .row(vec!["Tablet", "$399", "Out of Stock"])
1111 .row(vec!["Headphones", "$199", "In Stock"])
1112 .into_any_element(),
1113 ),
1114 ],
1115 ),
1116 example_group_with_title(
1117 "Mixed Content Table",
1118 vec![single_example(
1119 "Table with Elements",
1120 Table::new(5)
1121 .width(px(840.))
1122 .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1123 .row(vec![
1124 Indicator::dot().color(Color::Success).into_any_element(),
1125 "Project A".into_any_element(),
1126 "High".into_any_element(),
1127 "2023-12-31".into_any_element(),
1128 Button::new("view_a", "View")
1129 .style(ButtonStyle::Filled)
1130 .full_width()
1131 .into_any_element(),
1132 ])
1133 .row(vec![
1134 Indicator::dot().color(Color::Warning).into_any_element(),
1135 "Project B".into_any_element(),
1136 "Medium".into_any_element(),
1137 "2024-03-15".into_any_element(),
1138 Button::new("view_b", "View")
1139 .style(ButtonStyle::Filled)
1140 .full_width()
1141 .into_any_element(),
1142 ])
1143 .row(vec![
1144 Indicator::dot().color(Color::Error).into_any_element(),
1145 "Project C".into_any_element(),
1146 "Low".into_any_element(),
1147 "2024-06-30".into_any_element(),
1148 Button::new("view_c", "View")
1149 .style(ButtonStyle::Filled)
1150 .full_width()
1151 .into_any_element(),
1152 ])
1153 .into_any_element(),
1154 )],
1155 ),
1156 example_group_with_title(
1157 "Sortable Header",
1158 vec![
1159 single_example(
1160 "Ascending",
1161 Table::new(3)
1162 .width(px(400.))
1163 .sortable_header(
1164 vec!["Name", "Age", "City"],
1165 Some(0),
1166 SortDirection::Ascending,
1167 |_column, _window, _cx| {},
1168 )
1169 .row(vec!["Alice", "28", "New York"])
1170 .row(vec!["Bob", "32", "San Francisco"])
1171 .into_any_element(),
1172 ),
1173 single_example(
1174 "Descending",
1175 Table::new(3)
1176 .width(px(400.))
1177 .sortable_header(
1178 vec!["Name", "Age", "City"],
1179 Some(1),
1180 SortDirection::Descending,
1181 |_column, _window, _cx| {},
1182 )
1183 .row(vec!["Bob", "32", "San Francisco"])
1184 .row(vec!["Alice", "28", "New York"])
1185 .into_any_element(),
1186 ),
1187 ],
1188 ),
1189 example_group_with_title(
1190 "With Pagination Footer",
1191 vec![single_example(
1192 "Table + Pagination",
1193 v_flex()
1194 .gap_2()
1195 .child(
1196 Table::new(3)
1197 .width(px(400.))
1198 .header(vec!["Product", "Price", "Stock"])
1199 .row(vec!["Laptop", "$999", "In Stock"])
1200 .row(vec!["Phone", "$599", "Low Stock"])
1201 .row(vec!["Tablet", "$399", "Out of Stock"]),
1202 )
1203 .child(Pagination::new("table-pagination-preview", 2, 5))
1204 .into_any_element(),
1205 )],
1206 ),
1207 ])
1208 .into_any_element(),
1209 )
1210 }
1211}