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