data_table.rs

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