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