Skip to main content

ui/components/
data_table.rs

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
26/// Represents an unchecked table row, which is a vector of elements.
27/// Will be converted into `TableRow<T>` internally
28pub type UncheckedTableRow<T> = Vec<T>;
29
30/// State for independently resizable columns (spreadsheet-style).
31///
32/// Each column has its own absolute width; dragging a resize handle changes only
33/// that column's width, growing or shrinking the overall table width.
34pub 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    /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
129    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 column widths (no resize handles).
205    Static {
206        widths: StaticColumnWidths,
207        /// Controls widths of the whole table.
208        table_width: Option<DefiniteLength>,
209    },
210    /// Redistributable columns — dragging redistributes the fixed available space
211    /// among columns without changing the overall table width.
212    Redistributable {
213        columns_state: Entity<RedistributableColumnsState>,
214        table_width: Option<DefiniteLength>,
215    },
216    /// Independently resizable columns — dragging changes absolute column widths
217    /// and thus the overall table width. Like spreadsheets.
218    Resizable(Entity<ResizableColumnsState>),
219}
220
221pub enum StaticColumnWidths {
222    /// All columns share space equally (flex-1 / Length::Auto).
223    Auto,
224    /// Each column has a specific width.
225    Explicit(TableRow<DefiniteLength>),
226}
227
228impl ColumnWidthConfig {
229    /// Auto-width columns, auto-size table.
230    pub fn auto() -> Self {
231        ColumnWidthConfig::Static {
232            widths: StaticColumnWidths::Auto,
233            table_width: None,
234        }
235    }
236
237    /// Redistributable columns with no fixed table width.
238    pub fn redistributable(columns_state: Entity<RedistributableColumnsState>) -> Self {
239        ColumnWidthConfig::Redistributable {
240            columns_state,
241            table_width: None,
242        }
243    }
244
245    /// Auto-width columns, fixed table width.
246    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    /// Explicit column widths with no fixed table width.
254    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    /// Column widths for rendering.
269    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    /// Table-level width.
295    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    /// ListHorizontalSizingBehavior for uniform_list.
318    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/// Sort direction shown by a [`Table`] sortable column header indicator.
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum SortDirection {
336    Ascending,
337    Descending,
338}
339
340/// A table component
341#[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    /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
354    cols: usize,
355    disable_base_cell_style: bool,
356}
357
358impl Table {
359    /// Creates a new table with the specified number of columns.
360    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    /// Disables based styling of row cell (paddings, text ellipsis, nowrap, etc), keeping width settings
378    ///
379    /// Doesn't affect base style of header cell.
380    /// Doesn't remove overflow-hidden
381    pub fn disable_base_style(mut self) -> Self {
382        self.disable_base_cell_style = true;
383        self
384    }
385
386    /// Enables uniform list rendering.
387    /// The provided function will be passed directly to the `uniform_list` element.
388    /// Therefore, if this method is called, any calls to [`Table::row`] before or after
389    /// this method is called will be ignored.
390    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    /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
410    ///
411    /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
412    /// It is generally slower than [`Table::uniform_list`] due to the need to measure each row individually, but it provides correct layout for non-uniform or multiline content.
413    ///
414    /// # Parameters
415    /// - `row_count`: The total number of rows in the table.
416    /// - `list_state`: The [`ListState`] used for managing scroll position and virtualization. This must be initialized and managed by the caller, and should be kept in sync with the number of rows.
417    /// - `render_row_fn`: A closure that renders a single row, given the row index, a mutable reference to [`Window`], and a mutable reference to [`App`]. It should return an array of [`AnyElement`]s, one for each column.
418    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    /// Enables row striping (alternating row colors)
433    pub fn striped(mut self) -> Self {
434        self.striped = true;
435        self
436    }
437
438    /// Hides the border lines between rows
439    pub fn hide_row_borders(mut self) -> Self {
440        self.show_row_borders = false;
441        self
442    }
443
444    /// Sets a fixed table width with auto column widths.
445    ///
446    /// This is a shorthand for `.width_config(ColumnWidthConfig::auto_with_table_width(width))`.
447    /// For resizable columns or explicit column widths, use [`Table::width_config`] directly.
448    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    /// Sets the column width configuration for the table.
454    pub fn width_config(mut self, config: ColumnWidthConfig) -> Self {
455        self.column_width_config = config;
456        self
457    }
458
459    /// Enables interaction (primarily scrolling) with the table.
460    ///
461    /// Vertical scrolling will be enabled by default if the table is taller than its container.
462    ///
463    /// Horizontal scrolling will only be enabled if a table width is set via [`ColumnWidthConfig`],
464    /// otherwise the list will always shrink the table columns to fit their contents.
465    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    /// Renders a header row where every column is clickable and shows a sort
480    /// direction indicator (chevron up/down/up-down icon), calling `on_sort`
481    /// with the clicked column index.
482    ///
483    /// `sort_column` (if set) marks which column currently drives the sort
484    /// and is rendered with a direction-specific chevron
485    /// ([`SortDirection::Ascending`]/[`SortDirection::Descending`]); all
486    /// other columns show a neutral up/down chevron. This is purely additive
487    /// to [`Table::header`] — existing callers that use `.header(...)` are
488    /// unaffected.
489    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    /// Hides the default hover background on table rows.
559    /// Use this when you want to handle row hover styling manually via `map_row`.
560    pub fn hide_row_hover(mut self) -> Self {
561        self.show_row_hover = false;
562        self
563    }
564
565    /// Provide a callback that is invoked when the table is rendered without any rows
566    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        // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
616        // Applying `.flex().flex_row()` manually to overcome that
617        .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        // Add a resize divider after every column, including the last.
782        // For the last column the divider is pulled 1px inward so it isn't clipped
783        // by the overflow_hidden content container.
784        {
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            // Resizable mode: wrap table in a horizontal scroll container first
999            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            // Attach vertical scrollbars (converts Div → Stateful<Div>)
1014            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            // Add horizontal scrollbar when in resizable mode
1030            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}