data_table.rs

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