data_table.rs

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