data_table.rs

   1use std::{ops::Range, rc::Rc};
   2
   3use crate::{
   4    ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
   5    ComponentScope, Context, Div, DraggedColumn, ElementId, FixedWidth as _, FluentBuilder as _,
   6    HeaderResizeInfo, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels,
   7    RESIZE_DIVIDER_WIDTH, RedistributableColumnsState, RegisterComponent, RenderOnce, ScrollAxes,
   8    ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
   9    StyledTypography, TableResizeBehavior, Window, WithScrollbar, bind_redistributable_columns,
  10    div, example_group_with_title, h_flex, px, render_column_resize_divider,
  11    render_redistributable_columns_resize_handles, single_example,
  12    table_row::{IntoTableRow as _, TableRow},
  13    v_flex,
  14};
  15use gpui::{
  16    AbsoluteLength, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, Length,
  17    ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, ScrollHandle, Stateful,
  18    UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,
  19};
  20
  21pub mod table_row;
  22#[cfg(test)]
  23mod tests;
  24
  25/// Represents an unchecked table row, which is a vector of elements.
  26/// Will be converted into `TableRow<T>` internally
  27pub type UncheckedTableRow<T> = Vec<T>;
  28
  29/// State for independently resizable columns (spreadsheet-style).
  30///
  31/// Each column has its own absolute width; dragging a resize handle changes only
  32/// that column's width, growing or shrinking the overall table width.
  33pub struct ResizableColumnsState {
  34    initial_widths: TableRow<AbsoluteLength>,
  35    widths: TableRow<AbsoluteLength>,
  36    resize_behavior: TableRow<TableResizeBehavior>,
  37}
  38
  39impl ResizableColumnsState {
  40    pub fn new(
  41        cols: usize,
  42        initial_widths: Vec<impl Into<AbsoluteLength>>,
  43        resize_behavior: Vec<TableResizeBehavior>,
  44    ) -> Self {
  45        let widths: TableRow<AbsoluteLength> = initial_widths
  46            .into_iter()
  47            .map(Into::into)
  48            .collect::<Vec<_>>()
  49            .into_table_row(cols);
  50        Self {
  51            initial_widths: widths.clone(),
  52            widths,
  53            resize_behavior: resize_behavior.into_table_row(cols),
  54        }
  55    }
  56
  57    pub fn cols(&self) -> usize {
  58        self.widths.cols()
  59    }
  60
  61    pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
  62        &self.resize_behavior
  63    }
  64
  65    pub(crate) fn on_drag_move(
  66        &mut self,
  67        drag_event: &DragMoveEvent<DraggedColumn>,
  68        window: &mut Window,
  69        cx: &mut Context<Self>,
  70    ) {
  71        let col_idx = drag_event.drag(cx).col_idx;
  72        let rem_size = window.rem_size();
  73        let drag_x = drag_event.event.position.x - drag_event.bounds.left();
  74
  75        let left_edge: Pixels = self.widths.as_slice()[..col_idx]
  76            .iter()
  77            .map(|width| width.to_pixels(rem_size))
  78            .fold(px(0.), |acc, x| acc + x);
  79
  80        let new_width = drag_x - left_edge;
  81        let new_width = self.apply_min_size(new_width, self.resize_behavior[col_idx], rem_size);
  82
  83        self.widths[col_idx] = AbsoluteLength::Pixels(new_width);
  84        cx.notify();
  85    }
  86
  87    pub fn set_column_configuration(
  88        &mut self,
  89        col_idx: usize,
  90        width: impl Into<AbsoluteLength>,
  91        resize_behavior: TableResizeBehavior,
  92    ) {
  93        let width = width.into();
  94        self.initial_widths[col_idx] = width;
  95        self.widths[col_idx] = width;
  96        self.resize_behavior[col_idx] = resize_behavior;
  97    }
  98
  99    pub fn reset_column_to_initial_width(&mut self, col_idx: usize) {
 100        self.widths[col_idx] = self.initial_widths[col_idx];
 101    }
 102
 103    fn apply_min_size(
 104        &self,
 105        width: Pixels,
 106        behavior: TableResizeBehavior,
 107        rem_size: Pixels,
 108    ) -> Pixels {
 109        match behavior.min_size() {
 110            Some(min_rems) => {
 111                let min_px = rem_size * min_rems;
 112                width.max(min_px)
 113            }
 114            None => width,
 115        }
 116    }
 117}
 118
 119struct UniformListData {
 120    render_list_of_rows_fn:
 121        Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
 122    element_id: ElementId,
 123    row_count: usize,
 124}
 125
 126struct VariableRowHeightListData {
 127    /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
 128    render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
 129    list_state: ListState,
 130    row_count: usize,
 131}
 132
 133enum TableContents {
 134    Vec(Vec<TableRow<AnyElement>>),
 135    UniformList(UniformListData),
 136    VariableRowHeightList(VariableRowHeightListData),
 137}
 138
 139impl TableContents {
 140    fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
 141        match self {
 142            TableContents::Vec(rows) => Some(rows),
 143            TableContents::UniformList(_) => None,
 144            TableContents::VariableRowHeightList(_) => None,
 145        }
 146    }
 147
 148    fn len(&self) -> usize {
 149        match self {
 150            TableContents::Vec(rows) => rows.len(),
 151            TableContents::UniformList(data) => data.row_count,
 152            TableContents::VariableRowHeightList(data) => data.row_count,
 153        }
 154    }
 155
 156    fn is_empty(&self) -> bool {
 157        self.len() == 0
 158    }
 159}
 160
 161pub struct TableInteractionState {
 162    pub focus_handle: FocusHandle,
 163    pub scroll_handle: UniformListScrollHandle,
 164    pub horizontal_scroll_handle: ScrollHandle,
 165    pub custom_scrollbar: Option<Scrollbars>,
 166}
 167
 168impl TableInteractionState {
 169    pub fn new(cx: &mut App) -> Self {
 170        Self {
 171            focus_handle: cx.focus_handle(),
 172            scroll_handle: UniformListScrollHandle::new(),
 173            horizontal_scroll_handle: ScrollHandle::new(),
 174            custom_scrollbar: None,
 175        }
 176    }
 177
 178    pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
 179        self.custom_scrollbar = Some(custom_scrollbar);
 180        self
 181    }
 182
 183    pub fn scroll_offset(&self) -> Point<Pixels> {
 184        self.scroll_handle.offset()
 185    }
 186
 187    pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
 188        self.scroll_handle.set_offset(offset);
 189    }
 190
 191    pub fn listener<E: ?Sized>(
 192        this: &Entity<Self>,
 193        f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
 194    ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
 195        let view = this.downgrade();
 196        move |e: &E, window: &mut Window, cx: &mut App| {
 197            view.update(cx, |view, cx| f(view, e, window, cx)).ok();
 198        }
 199    }
 200}
 201
 202pub enum ColumnWidthConfig {
 203    /// Static column widths (no resize handles).
 204    Static {
 205        widths: StaticColumnWidths,
 206        /// Controls widths of the whole table.
 207        table_width: Option<DefiniteLength>,
 208    },
 209    /// Redistributable columns — dragging redistributes the fixed available space
 210    /// among columns without changing the overall table width.
 211    Redistributable {
 212        columns_state: Entity<RedistributableColumnsState>,
 213        table_width: Option<DefiniteLength>,
 214    },
 215    /// Independently resizable columns — dragging changes absolute column widths
 216    /// and thus the overall table width. Like spreadsheets.
 217    Resizable(Entity<ResizableColumnsState>),
 218}
 219
 220pub enum StaticColumnWidths {
 221    /// All columns share space equally (flex-1 / Length::Auto).
 222    Auto,
 223    /// Each column has a specific width.
 224    Explicit(TableRow<DefiniteLength>),
 225}
 226
 227impl ColumnWidthConfig {
 228    /// Auto-width columns, auto-size table.
 229    pub fn auto() -> Self {
 230        ColumnWidthConfig::Static {
 231            widths: StaticColumnWidths::Auto,
 232            table_width: None,
 233        }
 234    }
 235
 236    /// Redistributable columns with no fixed table width.
 237    pub fn redistributable(columns_state: Entity<RedistributableColumnsState>) -> Self {
 238        ColumnWidthConfig::Redistributable {
 239            columns_state,
 240            table_width: None,
 241        }
 242    }
 243
 244    /// Auto-width columns, fixed table width.
 245    pub fn auto_with_table_width(width: impl Into<DefiniteLength>) -> Self {
 246        ColumnWidthConfig::Static {
 247            widths: StaticColumnWidths::Auto,
 248            table_width: Some(width.into()),
 249        }
 250    }
 251
 252    /// Explicit column widths with no fixed table width.
 253    pub fn explicit<T: Into<DefiniteLength>>(widths: Vec<T>) -> Self {
 254        let cols = widths.len();
 255        ColumnWidthConfig::Static {
 256            widths: StaticColumnWidths::Explicit(
 257                widths
 258                    .into_iter()
 259                    .map(Into::into)
 260                    .collect::<Vec<_>>()
 261                    .into_table_row(cols),
 262            ),
 263            table_width: None,
 264        }
 265    }
 266
 267    /// Column widths for rendering.
 268    pub fn widths_to_render(&self, cx: &App) -> Option<TableRow<Length>> {
 269        match self {
 270            ColumnWidthConfig::Static {
 271                widths: StaticColumnWidths::Auto,
 272                ..
 273            } => None,
 274            ColumnWidthConfig::Static {
 275                widths: StaticColumnWidths::Explicit(widths),
 276                ..
 277            } => Some(widths.map_cloned(Length::Definite)),
 278            ColumnWidthConfig::Redistributable {
 279                columns_state: entity,
 280                ..
 281            } => Some(entity.read(cx).widths_to_render()),
 282            ColumnWidthConfig::Resizable(entity) => {
 283                let state = entity.read(cx);
 284                Some(
 285                    state
 286                        .widths
 287                        .map_cloned(|abs| Length::Definite(DefiniteLength::Absolute(abs))),
 288                )
 289            }
 290        }
 291    }
 292
 293    /// Table-level width.
 294    pub fn table_width(&self, window: &Window, cx: &App) -> Option<Length> {
 295        match self {
 296            ColumnWidthConfig::Static { table_width, .. }
 297            | ColumnWidthConfig::Redistributable { table_width, .. } => {
 298                table_width.map(Length::Definite)
 299            }
 300            ColumnWidthConfig::Resizable(entity) => {
 301                let state = entity.read(cx);
 302                let rem_size = window.rem_size();
 303                let total: Pixels = state
 304                    .widths
 305                    .as_slice()
 306                    .iter()
 307                    .map(|abs| abs.to_pixels(rem_size))
 308                    .fold(px(0.), |acc, x| acc + x);
 309                Some(Length::Definite(DefiniteLength::Absolute(
 310                    AbsoluteLength::Pixels(total),
 311                )))
 312            }
 313        }
 314    }
 315
 316    /// ListHorizontalSizingBehavior for uniform_list.
 317    pub fn list_horizontal_sizing(
 318        &self,
 319        window: &Window,
 320        cx: &App,
 321    ) -> ListHorizontalSizingBehavior {
 322        match self {
 323            ColumnWidthConfig::Resizable(_) => ListHorizontalSizingBehavior::FitList,
 324            _ => match self.table_width(window, cx) {
 325                Some(_) => ListHorizontalSizingBehavior::Unconstrained,
 326                None => ListHorizontalSizingBehavior::FitList,
 327            },
 328        }
 329    }
 330}
 331
 332/// A table component
 333#[derive(RegisterComponent, IntoElement)]
 334pub struct Table {
 335    striped: bool,
 336    show_row_borders: bool,
 337    show_row_hover: bool,
 338    headers: Option<TableRow<AnyElement>>,
 339    rows: TableContents,
 340    interaction_state: Option<WeakEntity<TableInteractionState>>,
 341    column_width_config: ColumnWidthConfig,
 342    map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 343    use_ui_font: bool,
 344    empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
 345    /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
 346    cols: usize,
 347    disable_base_cell_style: bool,
 348}
 349
 350impl Table {
 351    /// Creates a new table with the specified number of columns.
 352    pub fn new(cols: usize) -> Self {
 353        Self {
 354            cols,
 355            striped: false,
 356            show_row_borders: true,
 357            show_row_hover: true,
 358            headers: None,
 359            rows: TableContents::Vec(Vec::new()),
 360            interaction_state: None,
 361            map_row: None,
 362            use_ui_font: true,
 363            empty_table_callback: None,
 364            disable_base_cell_style: false,
 365            column_width_config: ColumnWidthConfig::auto(),
 366        }
 367    }
 368
 369    /// Disables based styling of row cell (paddings, text ellipsis, nowrap, etc), keeping width settings
 370    ///
 371    /// Doesn't affect base style of header cell.
 372    /// Doesn't remove overflow-hidden
 373    pub fn disable_base_style(mut self) -> Self {
 374        self.disable_base_cell_style = true;
 375        self
 376    }
 377
 378    /// Enables uniform list rendering.
 379    /// The provided function will be passed directly to the `uniform_list` element.
 380    /// Therefore, if this method is called, any calls to [`Table::row`] before or after
 381    /// this method is called will be ignored.
 382    pub fn uniform_list(
 383        mut self,
 384        id: impl Into<ElementId>,
 385        row_count: usize,
 386        render_item_fn: impl Fn(
 387            Range<usize>,
 388            &mut Window,
 389            &mut App,
 390        ) -> Vec<UncheckedTableRow<AnyElement>>
 391        + 'static,
 392    ) -> Self {
 393        self.rows = TableContents::UniformList(UniformListData {
 394            element_id: id.into(),
 395            row_count,
 396            render_list_of_rows_fn: Box::new(render_item_fn),
 397        });
 398        self
 399    }
 400
 401    /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
 402    ///
 403    /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
 404    /// 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.
 405    ///
 406    /// # Parameters
 407    /// - `row_count`: The total number of rows in the table.
 408    /// - `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.
 409    /// - `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.
 410    pub fn variable_row_height_list(
 411        mut self,
 412        row_count: usize,
 413        list_state: ListState,
 414        render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
 415    ) -> Self {
 416        self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
 417            render_row_fn: Box::new(render_row_fn),
 418            list_state,
 419            row_count,
 420        });
 421        self
 422    }
 423
 424    /// Enables row striping (alternating row colors)
 425    pub fn striped(mut self) -> Self {
 426        self.striped = true;
 427        self
 428    }
 429
 430    /// Hides the border lines between rows
 431    pub fn hide_row_borders(mut self) -> Self {
 432        self.show_row_borders = false;
 433        self
 434    }
 435
 436    /// Sets a fixed table width with auto column widths.
 437    ///
 438    /// This is a shorthand for `.width_config(ColumnWidthConfig::auto_with_table_width(width))`.
 439    /// For resizable columns or explicit column widths, use [`Table::width_config`] directly.
 440    pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
 441        self.column_width_config = ColumnWidthConfig::auto_with_table_width(width);
 442        self
 443    }
 444
 445    /// Sets the column width configuration for the table.
 446    pub fn width_config(mut self, config: ColumnWidthConfig) -> Self {
 447        self.column_width_config = config;
 448        self
 449    }
 450
 451    /// Enables interaction (primarily scrolling) with the table.
 452    ///
 453    /// Vertical scrolling will be enabled by default if the table is taller than its container.
 454    ///
 455    /// Horizontal scrolling will only be enabled if a table width is set via [`ColumnWidthConfig`],
 456    /// otherwise the list will always shrink the table columns to fit their contents.
 457    pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
 458        self.interaction_state = Some(interaction_state.downgrade());
 459        self
 460    }
 461
 462    pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
 463        self.headers = Some(
 464            headers
 465                .into_table_row(self.cols)
 466                .map(IntoElement::into_any_element),
 467        );
 468        self
 469    }
 470
 471    pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
 472        if let Some(rows) = self.rows.rows_mut() {
 473            rows.push(
 474                items
 475                    .into_table_row(self.cols)
 476                    .map(IntoElement::into_any_element),
 477            );
 478        }
 479        self
 480    }
 481
 482    pub fn no_ui_font(mut self) -> Self {
 483        self.use_ui_font = false;
 484        self
 485    }
 486
 487    pub fn map_row(
 488        mut self,
 489        callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
 490    ) -> Self {
 491        self.map_row = Some(Rc::new(callback));
 492        self
 493    }
 494
 495    /// Hides the default hover background on table rows.
 496    /// Use this when you want to handle row hover styling manually via `map_row`.
 497    pub fn hide_row_hover(mut self) -> Self {
 498        self.show_row_hover = false;
 499        self
 500    }
 501
 502    /// Provide a callback that is invoked when the table is rendered without any rows
 503    pub fn empty_table_callback(
 504        mut self,
 505        callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 506    ) -> Self {
 507        self.empty_table_callback = Some(Rc::new(callback));
 508        self
 509    }
 510}
 511
 512fn base_cell_style(width: Option<Length>) -> Div {
 513    div()
 514        .px_1p5()
 515        .when_some(width, |this, width| this.w(width))
 516        .when(width.is_none(), |this| this.flex_1())
 517        .whitespace_nowrap()
 518        .text_ellipsis()
 519        .overflow_hidden()
 520}
 521
 522fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
 523    base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
 524}
 525
 526pub fn render_table_row(
 527    row_index: usize,
 528    items: TableRow<impl IntoElement>,
 529    table_context: TableRenderContext,
 530    window: &mut Window,
 531    cx: &mut App,
 532) -> AnyElement {
 533    let is_striped = table_context.striped;
 534    let is_last = row_index == table_context.total_row_count - 1;
 535    let bg = if row_index % 2 == 1 && is_striped {
 536        Some(cx.theme().colors().text.opacity(0.05))
 537    } else {
 538        None
 539    };
 540    let cols = items.cols();
 541    let column_widths = table_context
 542        .column_widths
 543        .map_or(vec![None; cols].into_table_row(cols), |widths| {
 544            widths.map(Some)
 545        });
 546
 547    let mut row = div()
 548        // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
 549        // Applying `.flex().flex_row()` manually to overcome that
 550        .flex()
 551        .flex_row()
 552        .id(("table_row", row_index))
 553        .size_full()
 554        .when_some(bg, |row, bg| row.bg(bg))
 555        .when(table_context.show_row_hover, |row| {
 556            row.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
 557        })
 558        .when(!is_striped && table_context.show_row_borders, |row| {
 559            row.border_b_1()
 560                .border_color(transparent_black())
 561                .when(!is_last, |row| row.border_color(cx.theme().colors().border))
 562        });
 563
 564    row = row.children(
 565        items
 566            .map(IntoElement::into_any_element)
 567            .into_vec()
 568            .into_iter()
 569            .zip(column_widths.into_vec())
 570            .map(|(cell, width)| {
 571                if table_context.disable_base_cell_style {
 572                    div()
 573                        .when_some(width, |this, width| this.w(width))
 574                        .when(width.is_none(), |this| this.flex_1())
 575                        .overflow_hidden()
 576                        .child(cell)
 577                } else {
 578                    base_cell_style_text(width, table_context.use_ui_font, cx)
 579                        .px_1()
 580                        .py_0p5()
 581                        .child(cell)
 582                }
 583            }),
 584    );
 585
 586    let row = if let Some(map_row) = table_context.map_row {
 587        map_row((row_index, row), window, cx)
 588    } else {
 589        row.into_any_element()
 590    };
 591
 592    div().size_full().child(row).into_any_element()
 593}
 594
 595pub fn render_table_header(
 596    headers: TableRow<impl IntoElement>,
 597    table_context: TableRenderContext,
 598    resize_info: Option<HeaderResizeInfo>,
 599    entity_id: Option<EntityId>,
 600    cx: &mut App,
 601) -> impl IntoElement {
 602    let cols = headers.cols();
 603    let column_widths = table_context
 604        .column_widths
 605        .map_or(vec![None; cols].into_table_row(cols), |widths| {
 606            widths.map(Some)
 607        });
 608
 609    let element_id = entity_id
 610        .map(|entity| entity.to_string())
 611        .unwrap_or_default();
 612
 613    let shared_element_id: SharedString = format!("table-{}", element_id).into();
 614
 615    div()
 616        .flex()
 617        .flex_row()
 618        .items_center()
 619        .w_full()
 620        .border_b_1()
 621        .border_color(cx.theme().colors().border)
 622        .children(
 623            headers
 624                .into_vec()
 625                .into_iter()
 626                .enumerate()
 627                .zip(column_widths.into_vec())
 628                .map(|((header_idx, h), width)| {
 629                    base_cell_style_text(width, table_context.use_ui_font, cx)
 630                        .px_1()
 631                        .py_0p5()
 632                        .child(h)
 633                        .id(ElementId::NamedInteger(
 634                            shared_element_id.clone(),
 635                            header_idx as u64,
 636                        ))
 637                        .when_some(resize_info.as_ref().cloned(), |this, info| {
 638                            if info.resize_behavior[header_idx].is_resizable() {
 639                                this.on_click(move |event, window, cx| {
 640                                    if event.click_count() > 1 {
 641                                        info.reset_column(header_idx, window, cx);
 642                                    }
 643                                })
 644                            } else {
 645                                this
 646                            }
 647                        })
 648                }),
 649        )
 650}
 651
 652#[derive(Clone)]
 653pub struct TableRenderContext {
 654    pub striped: bool,
 655    pub show_row_borders: bool,
 656    pub show_row_hover: bool,
 657    pub total_row_count: usize,
 658    pub column_widths: Option<TableRow<Length>>,
 659    pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 660    pub use_ui_font: bool,
 661    pub disable_base_cell_style: bool,
 662}
 663
 664impl TableRenderContext {
 665    fn new(table: &Table, cx: &App) -> Self {
 666        Self {
 667            striped: table.striped,
 668            show_row_borders: table.show_row_borders,
 669            show_row_hover: table.show_row_hover,
 670            total_row_count: table.rows.len(),
 671            column_widths: table.column_width_config.widths_to_render(cx),
 672            map_row: table.map_row.clone(),
 673            use_ui_font: table.use_ui_font,
 674            disable_base_cell_style: table.disable_base_cell_style,
 675        }
 676    }
 677
 678    pub fn for_column_widths(column_widths: Option<TableRow<Length>>, use_ui_font: bool) -> Self {
 679        Self {
 680            striped: false,
 681            show_row_borders: true,
 682            show_row_hover: true,
 683            total_row_count: 0,
 684            column_widths,
 685            map_row: None,
 686            use_ui_font,
 687            disable_base_cell_style: false,
 688        }
 689    }
 690}
 691
 692fn render_resize_handles_resizable(
 693    columns_state: &Entity<ResizableColumnsState>,
 694    window: &mut Window,
 695    cx: &mut App,
 696) -> AnyElement {
 697    let (widths, resize_behavior) = {
 698        let state = columns_state.read(cx);
 699        (state.widths.clone(), state.resize_behavior.clone())
 700    };
 701
 702    let rem_size = window.rem_size();
 703    let resize_behavior = Rc::new(resize_behavior);
 704    let n_cols = widths.cols();
 705    let mut dividers: Vec<AnyElement> = Vec::with_capacity(n_cols);
 706    let mut accumulated_px = px(0.);
 707
 708    for col_idx in 0..n_cols {
 709        let col_width_px = widths[col_idx].to_pixels(rem_size);
 710        accumulated_px = accumulated_px + col_width_px;
 711
 712        // Add a resize divider after every column, including the last.
 713        // For the last column the divider is pulled 1px inward so it isn't clipped
 714        // by the overflow_hidden content container.
 715        {
 716            let divider_left = if col_idx + 1 == n_cols {
 717                accumulated_px - px(RESIZE_DIVIDER_WIDTH)
 718            } else {
 719                accumulated_px
 720            };
 721            let divider = div().id(col_idx).absolute().top_0().left(divider_left);
 722            let entity_id = columns_state.entity_id();
 723            let on_reset: Rc<dyn Fn(&mut Window, &mut App)> = {
 724                let columns_state = columns_state.clone();
 725                Rc::new(move |_window, cx| {
 726                    columns_state.update(cx, |state, cx| {
 727                        state.reset_column_to_initial_width(col_idx);
 728                        cx.notify();
 729                    });
 730                })
 731            };
 732            dividers.push(render_column_resize_divider(
 733                divider,
 734                col_idx,
 735                resize_behavior[col_idx].is_resizable(),
 736                entity_id,
 737                on_reset,
 738                None,
 739                window,
 740                cx,
 741            ));
 742        }
 743    }
 744
 745    div()
 746        .id("resize-handles")
 747        .absolute()
 748        .inset_0()
 749        .w_full()
 750        .children(dividers)
 751        .into_any_element()
 752}
 753
 754impl RenderOnce for Table {
 755    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 756        let table_context = TableRenderContext::new(&self, cx);
 757        let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
 758
 759        let header_resize_info =
 760            interaction_state
 761                .as_ref()
 762                .and_then(|_| match &self.column_width_config {
 763                    ColumnWidthConfig::Redistributable { columns_state, .. } => {
 764                        Some(HeaderResizeInfo::from_redistributable(columns_state, cx))
 765                    }
 766                    ColumnWidthConfig::Resizable(entity) => {
 767                        Some(HeaderResizeInfo::from_resizable(entity, cx))
 768                    }
 769                    _ => None,
 770                });
 771
 772        let table_width = self.column_width_config.table_width(window, cx);
 773        let horizontal_sizing = self.column_width_config.list_horizontal_sizing(window, cx);
 774        let no_rows_rendered = self.rows.is_empty();
 775        let variable_list_state = if let TableContents::VariableRowHeightList(data) = &self.rows {
 776            Some(data.list_state.clone())
 777        } else {
 778            None
 779        };
 780
 781        let (redistributable_entity, resizable_entity, resize_handles) =
 782            if let Some(_) = interaction_state.as_ref() {
 783                match &self.column_width_config {
 784                    ColumnWidthConfig::Redistributable { columns_state, .. } => (
 785                        Some(columns_state.clone()),
 786                        None,
 787                        Some(render_redistributable_columns_resize_handles(
 788                            columns_state,
 789                            window,
 790                            cx,
 791                        )),
 792                    ),
 793                    ColumnWidthConfig::Resizable(entity) => (
 794                        None,
 795                        Some(entity.clone()),
 796                        Some(render_resize_handles_resizable(entity, window, cx)),
 797                    ),
 798                    _ => (None, None, None),
 799                }
 800            } else {
 801                (None, None, None)
 802            };
 803
 804        let is_resizable = resizable_entity.is_some();
 805
 806        let table = div()
 807            .when_some(table_width, |this, width| this.w(width))
 808            .h_full()
 809            .v_flex()
 810            .when_some(self.headers.take(), |this, headers| {
 811                this.child(render_table_header(
 812                    headers,
 813                    table_context.clone(),
 814                    header_resize_info,
 815                    interaction_state.as_ref().map(Entity::entity_id),
 816                    cx,
 817                ))
 818            })
 819            .when_some(redistributable_entity, |this, widths| {
 820                bind_redistributable_columns(this, widths)
 821            })
 822            .when_some(resizable_entity, |this, entity| {
 823                this.on_drag_move::<DraggedColumn>(move |event, window, cx| {
 824                    if event.drag(cx).state_id != entity.entity_id() {
 825                        return;
 826                    }
 827                    entity.update(cx, |state, cx| state.on_drag_move(event, window, cx));
 828                })
 829            })
 830            .child({
 831                let content = div()
 832                    .flex_grow()
 833                    .w_full()
 834                    .relative()
 835                    .overflow_hidden()
 836                    .map(|parent| match self.rows {
 837                        TableContents::Vec(items) => {
 838                            parent.children(items.into_iter().enumerate().map(|(index, row)| {
 839                                div().child(render_table_row(
 840                                    index,
 841                                    row,
 842                                    table_context.clone(),
 843                                    window,
 844                                    cx,
 845                                ))
 846                            }))
 847                        }
 848                        TableContents::UniformList(uniform_list_data) => parent.child(
 849                            uniform_list(
 850                                uniform_list_data.element_id,
 851                                uniform_list_data.row_count,
 852                                {
 853                                    let render_item_fn = uniform_list_data.render_list_of_rows_fn;
 854                                    move |range: Range<usize>, window, cx| {
 855                                        let elements = render_item_fn(range.clone(), window, cx)
 856                                            .into_iter()
 857                                            .map(|raw_row| raw_row.into_table_row(self.cols))
 858                                            .collect::<Vec<_>>();
 859                                        elements
 860                                            .into_iter()
 861                                            .zip(range)
 862                                            .map(|(row, row_index)| {
 863                                                render_table_row(
 864                                                    row_index,
 865                                                    row,
 866                                                    table_context.clone(),
 867                                                    window,
 868                                                    cx,
 869                                                )
 870                                            })
 871                                            .collect()
 872                                    }
 873                                },
 874                            )
 875                            .size_full()
 876                            .flex_grow()
 877                            .with_sizing_behavior(ListSizingBehavior::Auto)
 878                            .with_horizontal_sizing_behavior(horizontal_sizing)
 879                            .when_some(
 880                                interaction_state.as_ref(),
 881                                |this, state| {
 882                                    this.track_scroll(
 883                                        &state.read_with(cx, |s, _| s.scroll_handle.clone()),
 884                                    )
 885                                },
 886                            ),
 887                        ),
 888                        TableContents::VariableRowHeightList(variable_list_data) => parent.child(
 889                            list(variable_list_data.list_state.clone(), {
 890                                let render_item_fn = variable_list_data.render_row_fn;
 891                                move |row_index: usize, window: &mut Window, cx: &mut App| {
 892                                    let row = render_item_fn(row_index, window, cx)
 893                                        .into_table_row(self.cols);
 894                                    render_table_row(
 895                                        row_index,
 896                                        row,
 897                                        table_context.clone(),
 898                                        window,
 899                                        cx,
 900                                    )
 901                                }
 902                            })
 903                            .size_full()
 904                            .flex_grow()
 905                            .with_sizing_behavior(ListSizingBehavior::Auto),
 906                        ),
 907                    })
 908                    .when_some(resize_handles, |parent, handles| parent.child(handles));
 909
 910                content.into_any_element()
 911            })
 912            .when_some(
 913                no_rows_rendered
 914                    .then_some(self.empty_table_callback)
 915                    .flatten(),
 916                |this, callback| {
 917                    this.child(
 918                        h_flex()
 919                            .size_full()
 920                            .p_3()
 921                            .items_start()
 922                            .justify_center()
 923                            .child(callback(window, cx)),
 924                    )
 925                },
 926            );
 927
 928        if let Some(state) = interaction_state.as_ref() {
 929            // Resizable mode: wrap table in a horizontal scroll container first
 930            let content = if is_resizable {
 931                let mut h_scroll_container = div()
 932                    .id("table-h-scroll")
 933                    .overflow_x_scroll()
 934                    .flex_grow()
 935                    .h_full()
 936                    .track_scroll(&state.read(cx).horizontal_scroll_handle)
 937                    .child(table);
 938                h_scroll_container.style().restrict_scroll_to_axis = Some(true);
 939                div().size_full().child(h_scroll_container)
 940            } else {
 941                table
 942            };
 943
 944            // Attach vertical scrollbars (converts Div → Stateful<Div>)
 945            let scrollbars = state
 946                .read(cx)
 947                .custom_scrollbar
 948                .clone()
 949                .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
 950            let mut content = if let Some(list_state) = variable_list_state {
 951                content.custom_scrollbars(scrollbars.tracked_scroll_handle(&list_state), window, cx)
 952            } else {
 953                content.custom_scrollbars(
 954                    scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
 955                    window,
 956                    cx,
 957                )
 958            };
 959
 960            // Add horizontal scrollbar when in resizable mode
 961            if is_resizable {
 962                content = content.custom_scrollbars(
 963                    Scrollbars::new(ScrollAxes::Horizontal)
 964                        .tracked_scroll_handle(&state.read(cx).horizontal_scroll_handle),
 965                    window,
 966                    cx,
 967                );
 968            }
 969            content.style().restrict_scroll_to_axis = Some(true);
 970
 971            content
 972                .track_focus(&state.read(cx).focus_handle)
 973                .id(("table", state.entity_id()))
 974                .into_any_element()
 975        } else {
 976            table.into_any_element()
 977        }
 978    }
 979}
 980
 981impl Component for Table {
 982    fn scope() -> ComponentScope {
 983        ComponentScope::Layout
 984    }
 985
 986    fn description() -> Option<&'static str> {
 987        Some("A table component for displaying data in rows and columns with optional styling.")
 988    }
 989
 990    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
 991        Some(
 992            v_flex()
 993                .gap_6()
 994                .children(vec![
 995                    example_group_with_title(
 996                        "Basic Tables",
 997                        vec![
 998                            single_example(
 999                                "Simple Table",
1000                                Table::new(3)
1001                                    .width(px(400.))
1002                                    .header(vec!["Name", "Age", "City"])
1003                                    .row(vec!["Alice", "28", "New York"])
1004                                    .row(vec!["Bob", "32", "San Francisco"])
1005                                    .row(vec!["Charlie", "25", "London"])
1006                                    .into_any_element(),
1007                            ),
1008                            single_example(
1009                                "Two Column Table",
1010                                Table::new(2)
1011                                    .header(vec!["Category", "Value"])
1012                                    .width(px(300.))
1013                                    .row(vec!["Revenue", "$100,000"])
1014                                    .row(vec!["Expenses", "$75,000"])
1015                                    .row(vec!["Profit", "$25,000"])
1016                                    .into_any_element(),
1017                            ),
1018                        ],
1019                    ),
1020                    example_group_with_title(
1021                        "Styled Tables",
1022                        vec![
1023                            single_example(
1024                                "Default",
1025                                Table::new(3)
1026                                    .width(px(400.))
1027                                    .header(vec!["Product", "Price", "Stock"])
1028                                    .row(vec!["Laptop", "$999", "In Stock"])
1029                                    .row(vec!["Phone", "$599", "Low Stock"])
1030                                    .row(vec!["Tablet", "$399", "Out of Stock"])
1031                                    .into_any_element(),
1032                            ),
1033                            single_example(
1034                                "Striped",
1035                                Table::new(3)
1036                                    .width(px(400.))
1037                                    .striped()
1038                                    .header(vec!["Product", "Price", "Stock"])
1039                                    .row(vec!["Laptop", "$999", "In Stock"])
1040                                    .row(vec!["Phone", "$599", "Low Stock"])
1041                                    .row(vec!["Tablet", "$399", "Out of Stock"])
1042                                    .row(vec!["Headphones", "$199", "In Stock"])
1043                                    .into_any_element(),
1044                            ),
1045                        ],
1046                    ),
1047                    example_group_with_title(
1048                        "Mixed Content Table",
1049                        vec![single_example(
1050                            "Table with Elements",
1051                            Table::new(5)
1052                                .width(px(840.))
1053                                .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1054                                .row(vec![
1055                                    Indicator::dot().color(Color::Success).into_any_element(),
1056                                    "Project A".into_any_element(),
1057                                    "High".into_any_element(),
1058                                    "2023-12-31".into_any_element(),
1059                                    Button::new("view_a", "View")
1060                                        .style(ButtonStyle::Filled)
1061                                        .full_width()
1062                                        .into_any_element(),
1063                                ])
1064                                .row(vec![
1065                                    Indicator::dot().color(Color::Warning).into_any_element(),
1066                                    "Project B".into_any_element(),
1067                                    "Medium".into_any_element(),
1068                                    "2024-03-15".into_any_element(),
1069                                    Button::new("view_b", "View")
1070                                        .style(ButtonStyle::Filled)
1071                                        .full_width()
1072                                        .into_any_element(),
1073                                ])
1074                                .row(vec![
1075                                    Indicator::dot().color(Color::Error).into_any_element(),
1076                                    "Project C".into_any_element(),
1077                                    "Low".into_any_element(),
1078                                    "2024-06-30".into_any_element(),
1079                                    Button::new("view_c", "View")
1080                                        .style(ButtonStyle::Filled)
1081                                        .full_width()
1082                                        .into_any_element(),
1083                                ])
1084                                .into_any_element(),
1085                        )],
1086                    ),
1087                ])
1088                .into_any_element(),
1089        )
1090    }
1091}