Skip to main content

gpui_component/table/
state.rs

1use gpui_base::TestSupportExt as _;
2use std::{ops::Range, rc::Rc, time::Duration};
3
4use crate::{
5    ActiveTheme, ElementExt, Icon, IconName, StyleSized as _, StyledExt, VirtualListScrollHandle,
6    actions::{
7        Cancel, SelectDown, SelectFirst, SelectLast, SelectNextColumn, SelectPageDown,
8        SelectPageUp, SelectPrevColumn, SelectUp,
9    },
10    h_flex,
11    menu::{ContextMenuExt, PopupMenu},
12    scroll::{ScrollableMask, Scrollbar},
13    v_flex,
14};
15use gpui::{
16    AppContext, Axis, Bounds, ClickEvent, Context, Div, DragMoveEvent, ElementId, EventEmitter,
17    FocusHandle, Focusable, InteractiveElement, IntoElement, ListSizingBehavior, MouseButton,
18    MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, SharedString, Stateful,
19    StatefulInteractiveElement as _, Styled, Task, TextAlign, UniformListScrollHandle, Window, div,
20    prelude::FluentBuilder, px, uniform_list,
21};
22use rust_i18n::t;
23
24use super::*;
25
26/// How far behind the pointer a resize drag trails the column edge.
27const HANDLE_SIZE: Pixels = px(2.);
28
29/// Grab room on each side of a resize handle's hairline, so the divider can be
30/// caught without pixel-hunting. Both halves of the band are this wide, so
31/// widening the grab area stays symmetric about the boundary. `resizable`'s
32/// handle pads a hairline the same way, and by the same amount, see
33/// `crates/base/src/resizable/resize_handle.rs`.
34const HANDLE_PADDING: Pixels = px(4.);
35
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
37enum SelectionMode {
38    Column,
39    Row,
40    Cell,
41}
42
43impl SelectionMode {
44    #[inline(always)]
45    fn is_row(&self) -> bool {
46        matches!(self, SelectionMode::Row)
47    }
48
49    #[inline(always)]
50    fn is_column(&self) -> bool {
51        matches!(self, SelectionMode::Column)
52    }
53
54    #[inline(always)]
55    fn is_cell(&self) -> bool {
56        matches!(self, SelectionMode::Cell)
57    }
58}
59
60/// The current selection of a table, as one value.
61///
62/// Mirrors [`TableEvent::SelectRow`], [`TableEvent::SelectColumn`] and
63/// [`TableEvent::SelectCell`]. Match on it instead of combining
64/// [`TableState::selected_row`], [`TableState::selected_col`] and
65/// [`TableState::selected_cell`].
66#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
67pub enum TableSelection {
68    /// Nothing is selected.
69    #[default]
70    None,
71    /// A row is selected.
72    Row(usize),
73    /// A column is selected.
74    Column(usize),
75    /// A cell is selected, as `(row_ix, col_ix)`.
76    Cell(usize, usize),
77}
78
79/// The Table event.
80#[derive(Clone)]
81pub enum TableEvent {
82    /// Single click or move to selected row.
83    SelectRow(usize),
84    /// Double click on the row.
85    DoubleClickedRow(usize),
86    /// Selected column.
87    SelectColumn(usize),
88    /// A cell has been selected (clicked or navigated to via keyboard).
89    ///
90    /// Emitted when a cell is selected in cell selection mode.
91    /// The first `usize` is the row index, and the second `usize` is the column index.
92    ///
93    /// This event is also emitted when navigating between cells using keyboard shortcuts.
94    SelectCell(usize, usize),
95    /// A cell has been double-clicked.
96    ///
97    /// Emitted when a cell is double-clicked in cell selection mode.
98    /// The first `usize` is the row index, and the second `usize` is the column index.
99    ///
100    /// Use this event to trigger actions like opening a detail view or editing the cell content.
101    DoubleClickedCell(usize, usize),
102    /// The column widths have changed.
103    ///
104    /// The `Vec<Pixels>` contains the new widths of all columns.
105    ColumnWidthsChanged(Vec<Pixels>),
106    /// A column has been moved.
107    ///
108    /// The first `usize` is the original index of the column,
109    /// and the second `usize` is the new index of the column.
110    MoveColumn(usize, usize),
111    /// A row has been right-clicked.
112    ///
113    /// Contains the row index, or `None` if right-clicked on an empty area.
114    /// Use this event to show context menus for rows.
115    RightClickedRow(Option<usize>),
116    /// A cell has been right-clicked.
117    ///
118    /// Emitted when a cell is right-clicked in cell selection mode.
119    /// The first `usize` is the row index, and the second `usize` is the column index.
120    ///
121    /// Use this event to show context menus specific to the cell content.
122    /// The right-clicked cell is highlighted with a subtle border until another cell is clicked.
123    RightClickedCell(usize, usize),
124    /// The selection has been cleared.
125    ///
126    /// This event is emitted when the selection is cleared.
127    ClearSelection,
128}
129
130/// The visible range of the rows and columns.
131#[derive(Debug, Default)]
132pub struct TableVisibleRange {
133    /// The visible range of the rows.
134    rows: Range<usize>,
135    /// The visible range of the columns.
136    cols: Range<usize>,
137}
138
139impl TableVisibleRange {
140    /// Returns the visible range of the rows.
141    pub fn rows(&self) -> &Range<usize> {
142        &self.rows
143    }
144
145    /// Returns the visible range of the columns.
146    pub fn cols(&self) -> &Range<usize> {
147        &self.cols
148    }
149}
150
151/// The state for [`DataTable`].
152///
153/// # Selection Modes
154///
155/// The table supports three selection modes:
156/// - **Row Selection**: Select entire rows (default mode)
157/// - **Column Selection**: Select entire columns
158/// - **Cell Selection**: Select individual cells
159///
160/// ## Cell Selection
161///
162/// When `cell_selectable` is enabled, users can:
163/// - Click on cells to select them
164/// - Right-click on cells to mark them for context menus
165/// - Double-click on cells to trigger actions
166/// - Navigate between cells using keyboard (arrow keys, Home, End, PageUp, PageDown, Tab)
167///
168/// When in cell selection mode, a row header column appears on the left side,
169/// allowing users to select entire rows by clicking on it.
170///
171/// # Events
172///
173/// The table emits the following events related to cell selection:
174/// - [`TableEvent::SelectCell`]: Emitted when a cell is selected
175/// - [`TableEvent::DoubleClickedCell`]: Emitted when a cell is double-clicked
176/// - [`TableEvent::RightClickedCell`]: Emitted when a cell is right-clicked
177///
178/// # Example
179///
180/// ```rust,ignore
181/// let table_state = cx.new(|cx| {
182///     TableState::new(delegate, window, cx)
183///         .cell_selectable(true)
184///         .row_selectable(true)
185/// });
186///
187/// // Subscribe to cell events
188/// cx.subscribe(&table_state, |this, table, event, cx| {
189///     match event {
190///         TableEvent::SelectCell(row_ix, col_ix) => {
191///             println!("Selected cell: ({}, {})", row_ix, col_ix);
192///         }
193///         TableEvent::DoubleClickedCell(row_ix, col_ix) => {
194///             println!("Double-clicked cell: ({}, {})", row_ix, col_ix);
195///         }
196///         _ => {}
197///     }
198/// });
199/// ```
200#[derive(Clone)]
201pub(crate) struct HeaderCell {
202    pub label: SharedString,
203    pub width: Pixels,
204    col_span: usize,
205    is_leaf: bool,
206    leaf_col_ix: Option<usize>,
207    start_leaf_col_ix: usize,
208}
209
210pub struct TableState<D: TableDelegate> {
211    focus_handle: FocusHandle,
212    delegate: D,
213    pub(super) options: TableOptions,
214    /// The bounds of the table container.
215    bounds: Bounds<Pixels>,
216    /// The bounds of the fixed head cols.
217    fixed_head_cols_bounds: Bounds<Pixels>,
218
219    col_groups: Vec<ColGroup>,
220    header_layout: Vec<Vec<HeaderCell>>,
221
222    /// Whether the table can loop selection, default is true.
223    ///
224    /// When the prev/next selection is out of the table bounds, the selection will loop to the other side.
225    pub loop_selection: bool,
226    /// Whether the table can select column.
227    pub col_selectable: bool,
228    /// Whether the table can select row.
229    pub row_selectable: bool,
230    /// Whether a row click with the secondary modifier (⌘ on macOS, Ctrl
231    /// elsewhere) or Shift held selects the row, default is `true`.
232    ///
233    /// Set to `false` when the delegate keeps its own multi-row selection and
234    /// handles those clicks itself. The table then leaves its selected row
235    /// alone on a modified click, so a click that takes a row out of the
236    /// delegate's selection is not undone by the table selecting it.
237    ///
238    /// In cell selection without a row header, a modified click on the
239    /// selected cell does not escalate it to its row either.
240    select_on_modifier_click: bool,
241    /// Whether the table can select cell, default is false.
242    ///
243    /// When enabled:
244    /// - Users can click on individual cells to select them
245    /// - A row header column appears on the left for selecting entire rows
246    ///   (can be hidden via [`Self::row_header`])
247    /// - Keyboard navigation works at the cell level (arrow keys move between cells)
248    /// - Right-click and double-click events are supported for cells
249    pub cell_selectable: bool,
250    /// Whether the row header column is visible when `cell_selectable` is enabled,
251    /// default is `true`.
252    ///
253    /// Set to `false` to hide the narrow leftmost header column while keeping cell
254    /// selection — useful when you want to put your own content (e.g. a row index
255    /// column) on the left. When hidden, clicking the already-selected cell again
256    /// escalates the selection to the whole row so users can still pick rows; row
257    /// escalation requires `row_selectable` to be enabled.
258    pub row_header: bool,
259    /// Whether the table can sort.
260    pub sortable: bool,
261    /// Whether the table can resize columns.
262    pub col_resizable: bool,
263    /// Whether the table can move columns.
264    pub col_movable: bool,
265    /// Enable/disable fixed columns feature.
266    pub col_fixed: bool,
267
268    pub vertical_scroll_handle: UniformListScrollHandle,
269    pub horizontal_scroll_handle: VirtualListScrollHandle,
270
271    selected_row: Option<usize>,
272    selection_mode: SelectionMode,
273    right_clicked_row: Option<usize>,
274    right_clicked_cell: Option<(usize, usize)>,
275    selected_col: Option<usize>,
276    selected_cell: Option<(usize, usize)>,
277
278    /// The column index that is being resized.
279    resizing_col: Option<usize>,
280    /// The column a double-click on its resize handle just fitted.
281    ///
282    /// The double-click's second press also starts a drag on the handle, which
283    /// would move the fitted edge back under the pointer, so resize drags on
284    /// this column are ignored until the next single press.
285    fitted_col: Option<usize>,
286
287    /// The insertion gap index (`0..=cols_count`) while dragging a column
288    /// header: the dragged column will be inserted between the columns
289    /// `gap - 1` and `gap` on drop.
290    col_drag_gap: Option<usize>,
291
292    /// The visible range of the rows and columns.
293    visible_range: TableVisibleRange,
294
295    _measure: Vec<Duration>,
296    _load_more_task: Task<()>,
297}
298
299impl<D> TableState<D>
300where
301    D: TableDelegate,
302{
303    /// Create a new TableState with the given delegate.
304    pub fn new(delegate: D, _: &mut Window, cx: &mut Context<Self>) -> Self {
305        let mut this = Self {
306            focus_handle: cx.focus_handle().tab_stop(true),
307            options: TableOptions::default(),
308            delegate,
309            col_groups: Vec::new(),
310            header_layout: Vec::new(),
311            horizontal_scroll_handle: VirtualListScrollHandle::new(),
312            vertical_scroll_handle: UniformListScrollHandle::new(),
313            selection_mode: SelectionMode::Row,
314            selected_row: None,
315            right_clicked_row: None,
316            right_clicked_cell: None,
317            selected_col: None,
318            selected_cell: None,
319            resizing_col: None,
320            fitted_col: None,
321            col_drag_gap: None,
322            bounds: Bounds::default(),
323            fixed_head_cols_bounds: Bounds::default(),
324            visible_range: TableVisibleRange::default(),
325            loop_selection: true,
326            col_selectable: true,
327            row_selectable: true,
328            select_on_modifier_click: true,
329            cell_selectable: false,
330            row_header: true,
331            sortable: true,
332            col_movable: true,
333            col_resizable: true,
334            col_fixed: true,
335            _load_more_task: Task::ready(()),
336            _measure: Vec::new(),
337        };
338
339        this.prepare_col_groups(cx);
340        this
341    }
342
343    /// Returns a reference to the delegate.
344    pub fn delegate(&self) -> &D {
345        &self.delegate
346    }
347
348    /// Returns a mutable reference to the delegate.
349    pub fn delegate_mut(&mut self) -> &mut D {
350        &mut self.delegate
351    }
352
353    /// Set to loop selection, default to true.
354    pub fn loop_selection(mut self, loop_selection: bool) -> Self {
355        self.loop_selection = loop_selection;
356        self
357    }
358
359    /// Set to enable/disable column movable, default to true.
360    pub fn col_movable(mut self, col_movable: bool) -> Self {
361        self.col_movable = col_movable;
362        self
363    }
364
365    /// Set to enable/disable column resizable, default to true.
366    pub fn col_resizable(mut self, col_resizable: bool) -> Self {
367        self.col_resizable = col_resizable;
368        self
369    }
370
371    /// Set to enable/disable column sortable, default true
372    pub fn sortable(mut self, sortable: bool) -> Self {
373        self.sortable = sortable;
374        self
375    }
376
377    /// Set to enable/disable row selectable, default true
378    pub fn row_selectable(mut self, row_selectable: bool) -> Self {
379        self.row_selectable = row_selectable;
380        self
381    }
382
383    /// Set whether a row click with the secondary modifier or Shift held
384    /// selects the row, default is `true`.
385    pub fn select_on_modifier_click(mut self, select_on_modifier_click: bool) -> Self {
386        self.select_on_modifier_click = select_on_modifier_click;
387        self
388    }
389
390    /// Set to enable/disable column selectable, default true
391    pub fn col_selectable(mut self, col_selectable: bool) -> Self {
392        self.col_selectable = col_selectable;
393        self
394    }
395
396    /// Set to enable/disable cell selection, default is false.
397    ///
398    /// When enabled:
399    /// - Individual cells become selectable by clicking
400    /// - A row header column appears on the left side (can be hidden via [`Self::row_header`])
401    /// - Keyboard navigation operates at the cell level
402    /// - Cell-specific events (SelectCell, DoubleClickedCell, RightClickedCell) are emitted
403    ///
404    /// # Example
405    ///
406    /// ```rust,ignore
407    /// let table_state = cx.new(|cx| {
408    ///     TableState::new(delegate, window, cx)
409    ///         .cell_selectable(true)  // Enable cell selection
410    ///         .row_selectable(true)   // Also allow row selection via row header
411    /// });
412    /// ```
413    pub fn cell_selectable(mut self, cell_selectable: bool) -> Self {
414        self.cell_selectable = cell_selectable;
415        self
416    }
417
418    /// Set whether the row header column is shown, default is `true`.
419    ///
420    /// Only effective when `cell_selectable` is `true` — otherwise the row header
421    /// column is never rendered. Hide it when you want to use the leftmost column
422    /// for your own content (e.g. a row index column).
423    ///
424    /// When hidden, the first click on a cell selects the cell; clicking the
425    /// already-selected cell again escalates to selecting the whole row, so users
426    /// can still pick rows without the dedicated header column. The row escalation
427    /// requires `row_selectable` to be enabled.
428    pub fn row_header(mut self, row_header: bool) -> Self {
429        self.row_header = row_header;
430        self
431    }
432
433    /// When we update columns or rows, we need to refresh the table.
434    pub fn refresh(&mut self, cx: &mut Context<Self>) {
435        self.prepare_col_groups(cx);
436    }
437
438    /// Scroll to the row at the given index.
439    pub fn scroll_to_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
440        self.vertical_scroll_handle
441            .scroll_to_item(row_ix, ScrollStrategy::Top);
442        cx.notify();
443    }
444
445    // Scroll to the column at the given index.
446    pub fn scroll_to_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
447        let col_ix = col_ix.saturating_sub(self.fixed_left_cols_count());
448
449        // Resolve the offset here instead of deferring it to the virtual list.
450        //
451        // The header and the rows share `horizontal_scroll_handle`, but only
452        // the rows are a `VirtualList`, and a deferred `scroll_to_item` is
453        // applied during that list's prepaint — which runs *after* the header
454        // has already been rendered and prepainted with the old offset. The
455        // header would therefore stay one frame behind the rows.
456        match self.horizontal_offset_for_col(col_ix) {
457            Some(offset_x) => {
458                let mut offset = self.horizontal_scroll_handle.offset();
459                offset.x = offset_x;
460                self.horizontal_scroll_handle.set_offset(offset);
461            }
462            // Before the first layout the viewport size is unknown, let the
463            // virtual list resolve the offset once it has been laid out.
464            None => self
465                .horizontal_scroll_handle
466                .scroll_to_item(col_ix, ScrollStrategy::Top),
467        }
468
469        cx.notify();
470    }
471
472    /// The horizontal scroll offset that brings the scrollable (non-fixed)
473    /// column at `col_ix` fully into view.
474    ///
475    /// This mirrors the nearest-edge behavior of [`VirtualListScrollHandle::scroll_to_item`]
476    /// with [`ScrollStrategy::Top`]: an already visible column keeps the current
477    /// offset, otherwise the column is aligned to the edge it overflows. Both
478    /// of those move the offset towards a column that exists, so the result
479    /// never runs past the content and needs no extra clamping.
480    ///
481    /// Returns `None` when the viewport size is not known yet, or when `col_ix`
482    /// is out of range.
483    fn horizontal_offset_for_col(&self, col_ix: usize) -> Option<Pixels> {
484        let viewport_width = self.horizontal_scroll_handle.bounds().size.width;
485        if viewport_width <= px(0.) {
486            return None;
487        }
488
489        let cols = self.col_groups.get(self.fixed_left_cols_count()..)?;
490        let col_left: Pixels = cols.get(..col_ix)?.iter().map(|col| col.width).sum();
491        let col_right = col_left + cols.get(col_ix)?.width;
492        let offset_x = self.horizontal_scroll_handle.offset().x;
493
494        Some(if col_left + offset_x < px(0.) {
495            -col_left
496        } else if col_right + offset_x > viewport_width {
497            viewport_width - col_right
498        } else {
499            offset_x
500        })
501    }
502
503    /// Returns the current selection as one value.
504    ///
505    /// A selected cell reports as [`TableSelection::Cell`] only; use
506    /// `selected_cell().map(|(row_ix, _)| row_ix)` when the cell's row is wanted.
507    pub fn selection(&self) -> TableSelection {
508        if let Some(row_ix) = self.selected_row() {
509            TableSelection::Row(row_ix)
510        } else if let Some(col_ix) = self.selected_col() {
511            TableSelection::Column(col_ix)
512        } else if let Some((row_ix, col_ix)) = self.selected_cell() {
513            TableSelection::Cell(row_ix, col_ix)
514        } else {
515            TableSelection::None
516        }
517    }
518
519    /// Returns the selected row index.
520    ///
521    /// `Some` only when a row itself is selected; a selected cell does not
522    /// count, see [`TableState::selection`].
523    pub fn selected_row(&self) -> Option<usize> {
524        self.selected_row.filter(|_| self.selection_mode.is_row())
525    }
526
527    /// Sets the selected row to the given index.
528    pub fn set_selected_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
529        let is_down = match self.selected_row {
530            Some(selected_row) => row_ix > selected_row,
531            None => true,
532        };
533
534        self.selection_mode = SelectionMode::Row;
535        self.right_clicked_row = None;
536        self.selected_row = Some(row_ix);
537        if let Some(row_ix) = self.selected_row {
538            self.vertical_scroll_handle.scroll_to_item(
539                row_ix,
540                if is_down {
541                    ScrollStrategy::Bottom
542                } else {
543                    ScrollStrategy::Top
544                },
545            );
546        }
547        cx.emit(TableEvent::SelectRow(row_ix));
548        cx.emit(TableEvent::RightClickedRow(None));
549        cx.notify();
550    }
551
552    /// Returns the row that has been right clicked.
553    pub fn right_clicked_row(&self) -> Option<usize> {
554        self.right_clicked_row
555    }
556
557    /// Returns the cell that has been right clicked, as `(row_ix, col_ix)`.
558    pub fn right_clicked_cell(&self) -> Option<(usize, usize)> {
559        self.right_clicked_cell
560    }
561
562    /// Set or clear the right-clicked row state.
563    ///
564    /// Pass `None` to clear — useful when opening a header context menu
565    /// to prevent the row context menu from appearing simultaneously.
566    pub fn set_right_clicked_row(&mut self, row: Option<usize>, cx: &mut Context<Self>) {
567        self.right_clicked_row = row;
568        cx.notify();
569    }
570
571    /// Returns the selected column index.
572    ///
573    /// `Some` only when a column itself is selected; a selected cell does not
574    /// count, see [`TableState::selection`].
575    pub fn selected_col(&self) -> Option<usize> {
576        self.selected_col
577            .filter(|_| self.selection_mode.is_column())
578    }
579
580    /// Sets the selected col to the given index.
581    pub fn set_selected_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
582        self.selection_mode = SelectionMode::Column;
583        self.selected_col = Some(col_ix);
584        if let Some(col_ix) = self.selected_col {
585            self.scroll_to_col(col_ix, cx);
586        }
587        cx.emit(TableEvent::SelectColumn(col_ix));
588        cx.notify();
589    }
590
591    /// Returns the selected cell as `(row_ix, col_ix)`.
592    ///
593    /// `Some` only when a cell is selected; see [`TableState::selection`].
594    ///
595    /// # Example
596    ///
597    /// ```rust,ignore
598    /// if let Some((row_ix, col_ix)) = table_state.read(cx).selected_cell() {
599    ///     println!("Selected cell: ({}, {})", row_ix, col_ix);
600    /// }
601    /// ```
602    pub fn selected_cell(&self) -> Option<(usize, usize)> {
603        self.selected_cell.filter(|_| self.selection_mode.is_cell())
604    }
605
606    /// Sets the selected cell to the given row and column indices.
607    ///
608    /// This method:
609    /// - Switches the table to cell selection mode
610    /// - Scrolls to make the cell visible (centered vertically)
611    /// - Emits a [`TableEvent::SelectCell`] event
612    ///
613    /// # Example
614    ///
615    /// ```rust,ignore
616    /// // Select the cell at row 5, column 3
617    /// table_state.update(cx, |state, cx| {
618    ///     state.set_selected_cell(5, 3, cx);
619    /// });
620    /// ```
621    pub fn set_selected_cell(&mut self, row_ix: usize, col_ix: usize, cx: &mut Context<Self>) {
622        self.selection_mode = SelectionMode::Cell;
623        self.selected_cell = Some((row_ix, col_ix));
624        self.right_clicked_cell = None;
625
626        // Scroll to the cell
627        self.vertical_scroll_handle
628            .scroll_to_item(row_ix, ScrollStrategy::Center);
629        self.scroll_to_col(col_ix, cx);
630
631        cx.emit(TableEvent::SelectCell(row_ix, col_ix));
632        cx.notify();
633    }
634
635    /// Sets the selection as one value; [`TableSelection::None`] clears it.
636    ///
637    /// Scrolls and emits exactly as the matching `set_selected_*` or
638    /// [`TableState::clear_selection`] call would.
639    pub fn set_selection(&mut self, selection: TableSelection, cx: &mut Context<Self>) {
640        match selection {
641            TableSelection::None => self.clear_selection(cx),
642            TableSelection::Row(row_ix) => self.set_selected_row(row_ix, cx),
643            TableSelection::Column(col_ix) => self.set_selected_col(col_ix, cx),
644            TableSelection::Cell(row_ix, col_ix) => self.set_selected_cell(row_ix, col_ix, cx),
645        }
646    }
647
648    /// Clear the selection of the table.
649    pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
650        self.selection_mode = SelectionMode::Row;
651        self.selected_row = None;
652        self.selected_col = None;
653        self.selected_cell = None;
654        cx.emit(TableEvent::ClearSelection);
655        cx.notify();
656    }
657
658    /// Returns the visible range of the rows and columns.
659    ///
660    /// See [`TableVisibleRange`].
661    pub fn visible_range(&self) -> &TableVisibleRange {
662        &self.visible_range
663    }
664
665    /// Dump the header row of the table.
666    ///
667    /// Batched exporters can read the headers once with this, then stream the
668    /// rows with [`Self::dump_range`].
669    pub fn headers(&self, cx: &App) -> Vec<String> {
670        let columns_count = self.delegate.columns_count(cx);
671        let mut headers = Vec::with_capacity(columns_count);
672        for col_ix in 0..columns_count {
673            let column = self.delegate.column(col_ix, cx);
674            headers.push(column.name.to_string());
675        }
676
677        headers
678    }
679
680    /// Dump table data.
681    ///
682    /// Returns a tuple of (headers, rows) where each row is a vector of cell values.
683    ///
684    /// This materializes the complete table in memory. For large tables, prefer
685    /// [`Self::dump_range`] and process rows in batches.
686    pub fn dump(&self, cx: &App) -> (Vec<String>, Vec<Vec<String>>) {
687        self.dump_range(0..self.delegate.rows_count(cx), cx)
688    }
689
690    /// Dump table data for the specified row range.
691    ///
692    /// Returns the same `(headers, rows)` shape as [`Self::dump`], with only
693    /// the rows inside the clamped range.
694    ///
695    /// The requested range is clamped to the table's current row count. For
696    /// large tables, callers can invoke this repeatedly with bounded ranges.
697    pub fn dump_range(&self, range: Range<usize>, cx: &App) -> (Vec<String>, Vec<Vec<String>>) {
698        let columns_count = self.delegate.columns_count(cx);
699        let rows_count = self.delegate.rows_count(cx);
700        let start = range.start.min(rows_count);
701        let end = range.end.min(rows_count).max(start);
702
703        let mut rows = Vec::with_capacity(end - start);
704        for row_ix in start..end {
705            let mut row = Vec::with_capacity(columns_count);
706            for col_ix in 0..columns_count {
707                row.push(self.delegate.cell_text(row_ix, col_ix, cx));
708            }
709            rows.push(row);
710        }
711
712        (self.headers(cx), rows)
713    }
714
715    /// Re-compute the header layout from the current delegate.
716    ///
717    /// Call this after changing delegate state that affects `group_headers`.
718    pub fn refresh_header_layout(&mut self, cx: &mut Context<Self>) {
719        self.update_header_layout(cx);
720        cx.notify();
721    }
722
723    fn prepare_col_groups(&mut self, cx: &mut Context<Self>) {
724        self.col_groups = (0..self.delegate.columns_count(cx))
725            .map(|col_ix| {
726                let column = self.delegate().column(col_ix, cx);
727                ColGroup {
728                    width: column.width,
729                    bounds: Bounds::default(),
730                    column,
731                }
732            })
733            .collect();
734
735        self.update_header_layout(cx);
736    }
737
738    fn update_header_layout(&mut self, cx: &mut Context<Self>) {
739        let group_rows = self.delegate.group_headers(cx);
740
741        let mut layout = match group_rows.as_ref() {
742            Some(rows) => Vec::with_capacity(rows.len() + 1),
743            None => Vec::with_capacity(1),
744        };
745
746        if let Some(group_rows) = group_rows {
747            for row in group_rows {
748                let mut cell_row = Vec::with_capacity(row.len());
749                let mut current_leaf_ix = 0;
750                for group in row {
751                    let mut width = px(0.);
752                    let start_leaf_col_ix = current_leaf_ix;
753                    for i in 0..group.span {
754                        if current_leaf_ix + i < self.col_groups.len() {
755                            width += self.col_groups[current_leaf_ix + i].width;
756                        }
757                    }
758                    current_leaf_ix += group.span;
759                    cell_row.push(HeaderCell {
760                        label: group.label.clone(),
761                        width,
762                        col_span: group.span,
763                        is_leaf: false,
764                        leaf_col_ix: None,
765                        start_leaf_col_ix,
766                    });
767                }
768                layout.push(cell_row);
769            }
770        }
771
772        let mut leaf_row = Vec::with_capacity(self.col_groups.len());
773        for (ix, group) in self.col_groups.iter().enumerate() {
774            leaf_row.push(HeaderCell {
775                label: group.column.name.clone(),
776                width: group.width,
777                col_span: 1,
778                is_leaf: true,
779                leaf_col_ix: Some(ix),
780                start_leaf_col_ix: ix,
781            });
782        }
783        layout.push(leaf_row);
784
785        self.header_layout = layout;
786    }
787
788    fn fixed_left_cols_count(&self) -> usize {
789        if !self.col_fixed {
790            return 0;
791        }
792
793        self.col_groups
794            .iter()
795            .filter(|col| col.column.fixed == Some(ColumnFixed::Left))
796            .count()
797    }
798
799    /// One header row. An unset header height matches the body rows.
800    fn header_row_height(&self) -> Pixels {
801        self.options
802            .header_row_height
803            .unwrap_or(self.options.size.table_row_height())
804    }
805
806    /// Every header row, including column-group rows. An empty layout still
807    /// reserves one row, matching the vertical scrollbar's top inset.
808    fn header_band_height(&self) -> Pixels {
809        self.header_row_height() * self.header_layout.len().max(1) as f32
810    }
811
812    fn page_item_count(&self) -> usize {
813        let row_height = self.options.size.table_row_height();
814        let body_height = (self.bounds.size.height - self.header_band_height()).max(px(0.));
815        let count = (body_height / row_height).floor() as usize;
816        count.max(1)
817    }
818
819    fn on_row_right_click(
820        &mut self,
821        _: &MouseDownEvent,
822        row_ix: Option<usize>,
823        _: &mut Window,
824        cx: &mut Context<Self>,
825    ) {
826        self.right_clicked_row = row_ix;
827        self.right_clicked_cell = None;
828        cx.emit(TableEvent::RightClickedRow(row_ix));
829    }
830
831    fn on_cell_right_click(
832        &mut self,
833        _: &MouseDownEvent,
834        row_ix: usize,
835        col_ix: usize,
836        _: &mut Window,
837        cx: &mut Context<Self>,
838    ) {
839        if !self.cell_selectable {
840            return;
841        }
842
843        cx.stop_propagation();
844        self.right_clicked_cell = Some((row_ix, col_ix));
845        self.right_clicked_row = None;
846        cx.emit(TableEvent::RightClickedCell(row_ix, col_ix));
847    }
848
849    fn on_row_left_click(
850        &mut self,
851        e: &ClickEvent,
852        row_ix: usize,
853        _: &mut Window,
854        cx: &mut Context<Self>,
855    ) {
856        if !self.row_selectable {
857            return;
858        }
859
860        cx.stop_propagation();
861        if self.selects_row_on_click(e, cx) {
862            self.set_selected_row(row_ix, cx);
863        }
864
865        if e.click_count() == 2 {
866            cx.emit(TableEvent::DoubleClickedRow(row_ix));
867        }
868    }
869
870    /// Whether a click on a row selects it, see `select_on_modifier_click`.
871    fn selects_row_on_click(&self, e: &ClickEvent, cx: &App) -> bool {
872        let modifiers = e.modifiers();
873        self.select_on_modifier_click || !(modifiers.secondary(cx) || modifiers.shift)
874    }
875
876    fn on_col_head_click(&mut self, col_ix: usize, _: &mut Window, cx: &mut Context<Self>) {
877        if !self.col_selectable {
878            return;
879        }
880
881        let Some(col_group) = self.col_groups.get(col_ix) else {
882            return;
883        };
884
885        if !col_group.column.selectable {
886            return;
887        }
888
889        self.set_selected_col(col_ix, cx)
890    }
891
892    fn on_cell_click(
893        &mut self,
894        e: &ClickEvent,
895        row_ix: usize,
896        col_ix: usize,
897        _: &mut Window,
898        cx: &mut Context<Self>,
899    ) {
900        if !self.cell_selectable {
901            return;
902        }
903
904        cx.stop_propagation();
905
906        let is_double_click = e.click_count() == 2;
907
908        // When the row header column is hidden, a single click on the
909        // already-selected cell escalates the selection to the entire row —
910        // giving users a way to pick rows without the dedicated header column.
911        // Double-clicks are passed through to `DoubleClickedCell` and never
912        // trigger the escalation.
913        let is_reselect = self.selected_cell() == Some((row_ix, col_ix));
914        let should_escalate_to_row = !self.row_header
915            && self.row_selectable
916            && is_reselect
917            && !is_double_click
918            && self.selects_row_on_click(e, cx);
919        if should_escalate_to_row {
920            self.set_selected_row(row_ix, cx);
921            return;
922        }
923
924        self.set_selected_cell(row_ix, col_ix, cx);
925
926        if is_double_click {
927            cx.emit(TableEvent::DoubleClickedCell(row_ix, col_ix));
928        }
929    }
930
931    fn has_selection(&self) -> bool {
932        self.selection() != TableSelection::None
933    }
934
935    pub(super) fn action_cancel(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
936        if self.has_selection() {
937            self.clear_selection(cx);
938            return;
939        }
940        cx.propagate();
941    }
942
943    pub(super) fn action_select_prev(
944        &mut self,
945        _: &SelectUp,
946        _: &mut Window,
947        cx: &mut Context<Self>,
948    ) {
949        let rows_count = self.delegate.rows_count(cx);
950        if rows_count < 1 {
951            return;
952        }
953
954        // Cell selection mode: move up within the same column
955        if self.selection_mode.is_cell() {
956            if let Some((row_ix, col_ix)) = self.selected_cell {
957                let new_row = if row_ix > 0 {
958                    row_ix.saturating_sub(1)
959                } else if self.loop_selection {
960                    rows_count.saturating_sub(1)
961                } else {
962                    row_ix
963                };
964                self.set_selected_cell(new_row, col_ix, cx);
965            } else {
966                // No cell selected, select first cell
967                self.set_selected_cell(0, 0, cx);
968            }
969            return;
970        }
971
972        // Row selection mode
973        if !self.row_selectable {
974            return;
975        }
976        let mut selected_row = self.selected_row.unwrap_or(0);
977        if selected_row > 0 {
978            selected_row = selected_row.saturating_sub(1);
979        } else {
980            if self.loop_selection {
981                selected_row = rows_count.saturating_sub(1);
982            }
983        }
984
985        self.set_selected_row(selected_row, cx);
986    }
987
988    pub(super) fn action_select_next(
989        &mut self,
990        _: &SelectDown,
991        _: &mut Window,
992        cx: &mut Context<Self>,
993    ) {
994        let rows_count = self.delegate.rows_count(cx);
995        if rows_count < 1 {
996            return;
997        }
998
999        // Cell selection mode: move down within the same column
1000        if self.selection_mode.is_cell() {
1001            if let Some((row_ix, col_ix)) = self.selected_cell {
1002                let new_row = if row_ix < rows_count.saturating_sub(1) {
1003                    row_ix + 1
1004                } else if self.loop_selection {
1005                    0
1006                } else {
1007                    row_ix
1008                };
1009                self.set_selected_cell(new_row, col_ix, cx);
1010            } else {
1011                // No cell selected, select first cell
1012                self.set_selected_cell(0, 0, cx);
1013            }
1014            return;
1015        }
1016
1017        // Row selection mode
1018        if !self.row_selectable {
1019            return;
1020        }
1021        let selected_row = match self.selected_row {
1022            Some(selected_row) if selected_row < rows_count.saturating_sub(1) => selected_row + 1,
1023            Some(selected_row) => {
1024                if self.loop_selection {
1025                    0
1026                } else {
1027                    selected_row
1028                }
1029            }
1030            _ => 0,
1031        };
1032
1033        self.set_selected_row(selected_row, cx);
1034    }
1035
1036    pub(super) fn action_select_first_column(
1037        &mut self,
1038        _: &SelectFirst,
1039        _: &mut Window,
1040        cx: &mut Context<Self>,
1041    ) {
1042        // Cell selection mode: move to first cell in current row
1043        if self.selection_mode.is_cell() {
1044            if let Some((row_ix, _)) = self.selected_cell {
1045                self.set_selected_cell(row_ix, 0, cx);
1046            } else {
1047                // No cell selected, select first cell of first row
1048                self.set_selected_cell(0, 0, cx);
1049            }
1050            return;
1051        }
1052
1053        // Row selection mode: Home goes to the first row, as in a list
1054        if self.selection_mode.is_row() && self.row_selectable {
1055            if self.delegate.rows_count(cx) > 0 {
1056                self.set_selected_row(0, cx);
1057            }
1058            return;
1059        }
1060
1061        // Column selection mode
1062        self.set_selected_col(0, cx);
1063    }
1064
1065    pub(super) fn action_select_last_column(
1066        &mut self,
1067        _: &SelectLast,
1068        _: &mut Window,
1069        cx: &mut Context<Self>,
1070    ) {
1071        let columns_count = self.delegate.columns_count(cx);
1072
1073        // Cell selection mode: move to last cell in current row
1074        if self.selection_mode.is_cell() {
1075            if let Some((row_ix, _)) = self.selected_cell {
1076                self.set_selected_cell(row_ix, columns_count.saturating_sub(1), cx);
1077            } else {
1078                // No cell selected, select last cell of first row
1079                self.set_selected_cell(0, columns_count.saturating_sub(1), cx);
1080            }
1081            return;
1082        }
1083
1084        // Row selection mode: End goes to the last row, as in a list
1085        if self.selection_mode.is_row() && self.row_selectable {
1086            let rows_count = self.delegate.rows_count(cx);
1087            if rows_count > 0 {
1088                self.set_selected_row(rows_count - 1, cx);
1089            }
1090            return;
1091        }
1092
1093        // Column selection mode
1094        self.set_selected_col(columns_count.saturating_sub(1), cx);
1095    }
1096
1097    pub(super) fn action_select_page_up(
1098        &mut self,
1099        _: &SelectPageUp,
1100        _: &mut Window,
1101        cx: &mut Context<Self>,
1102    ) {
1103        let step = self.page_item_count();
1104
1105        // Cell selection mode: move up by page within the same column
1106        if self.selection_mode.is_cell() {
1107            if let Some((row_ix, col_ix)) = self.selected_cell {
1108                let target = row_ix.saturating_sub(step);
1109                self.set_selected_cell(target, col_ix, cx);
1110            } else {
1111                // No cell selected, select first cell
1112                self.set_selected_cell(0, 0, cx);
1113            }
1114            return;
1115        }
1116
1117        // Row selection mode
1118        if !self.row_selectable {
1119            return;
1120        }
1121        let current = self.selected_row.unwrap_or(0);
1122        let target = current.saturating_sub(step);
1123        self.set_selected_row(target, cx);
1124    }
1125
1126    pub(super) fn action_select_page_down(
1127        &mut self,
1128        _: &SelectPageDown,
1129        _: &mut Window,
1130        cx: &mut Context<Self>,
1131    ) {
1132        let rows_count = self.delegate.rows_count(cx);
1133        if rows_count == 0 {
1134            return;
1135        }
1136
1137        let step = self.page_item_count();
1138
1139        // Cell selection mode: move down by page within the same column
1140        if self.selection_mode.is_cell() {
1141            if let Some((row_ix, col_ix)) = self.selected_cell {
1142                let max_row = rows_count.saturating_sub(1);
1143                let target = (row_ix + step).min(max_row);
1144                self.set_selected_cell(target, col_ix, cx);
1145            } else {
1146                // No cell selected, select first cell
1147                self.set_selected_cell(0, 0, cx);
1148            }
1149            return;
1150        }
1151
1152        // Row selection mode
1153        if !self.row_selectable {
1154            return;
1155        }
1156        let current = self.selected_row.unwrap_or(0);
1157        let max_row = rows_count.saturating_sub(1);
1158        let target = (current + step).min(max_row);
1159        self.set_selected_row(target, cx);
1160    }
1161
1162    pub(super) fn action_select_prev_col(
1163        &mut self,
1164        _: &SelectPrevColumn,
1165        _: &mut Window,
1166        cx: &mut Context<Self>,
1167    ) {
1168        let columns_count = self.delegate.columns_count(cx);
1169
1170        // Cell selection mode: move left within the same row
1171        if self.selection_mode.is_cell() {
1172            if let Some((row_ix, col_ix)) = self.selected_cell {
1173                let new_col = if col_ix > 0 {
1174                    col_ix.saturating_sub(1)
1175                } else if self.loop_selection {
1176                    columns_count.saturating_sub(1)
1177                } else {
1178                    col_ix
1179                };
1180                self.set_selected_cell(row_ix, new_col, cx);
1181            } else {
1182                // No cell selected, select first cell
1183                self.set_selected_cell(0, 0, cx);
1184            }
1185            return;
1186        }
1187
1188        // Column selection mode
1189        let mut selected_col = self.selected_col.unwrap_or(0);
1190        if selected_col > 0 {
1191            selected_col = selected_col.saturating_sub(1);
1192        } else {
1193            if self.loop_selection {
1194                selected_col = columns_count.saturating_sub(1);
1195            }
1196        }
1197        self.set_selected_col(selected_col, cx);
1198    }
1199
1200    pub(super) fn action_select_next_col(
1201        &mut self,
1202        _: &SelectNextColumn,
1203        _: &mut Window,
1204        cx: &mut Context<Self>,
1205    ) {
1206        let columns_count = self.delegate.columns_count(cx);
1207
1208        // Cell selection mode: move right within the same row
1209        if self.selection_mode.is_cell() {
1210            if let Some((row_ix, col_ix)) = self.selected_cell {
1211                let new_col = if col_ix < columns_count.saturating_sub(1) {
1212                    col_ix + 1
1213                } else if self.loop_selection {
1214                    0
1215                } else {
1216                    col_ix
1217                };
1218                self.set_selected_cell(row_ix, new_col, cx);
1219            } else {
1220                // No cell selected, select first cell
1221                self.set_selected_cell(0, 0, cx);
1222            }
1223            return;
1224        }
1225
1226        // Column selection mode
1227        let mut selected_col = self.selected_col.unwrap_or(0);
1228        if selected_col < columns_count.saturating_sub(1) {
1229            selected_col += 1;
1230        } else {
1231            if self.loop_selection {
1232                selected_col = 0;
1233            }
1234        }
1235
1236        self.set_selected_col(selected_col, cx);
1237    }
1238
1239    /// Scroll table when mouse position is near the edge of the table bounds.
1240    fn scroll_table_by_col_resizing(
1241        &mut self,
1242        mouse_position: Point<Pixels>,
1243        col_group: &ColGroup,
1244    ) {
1245        // Do nothing if pos out of the table bounds right for avoid scroll to the right.
1246        if mouse_position.x > self.bounds.right() {
1247            return;
1248        }
1249
1250        let mut offset = self.horizontal_scroll_handle.offset();
1251        let col_bounds = col_group.bounds;
1252
1253        if mouse_position.x < self.bounds.left()
1254            && col_bounds.right() < self.bounds.left() + px(20.)
1255        {
1256            offset.x += px(1.);
1257        } else if mouse_position.x > self.bounds.right()
1258            && col_bounds.right() > self.bounds.right() - px(20.)
1259        {
1260            offset.x -= px(1.);
1261        }
1262
1263        self.horizontal_scroll_handle.set_offset(offset);
1264    }
1265
1266    /// The width column `col_ix` is drawn at now, or `None` past the last
1267    /// column.
1268    ///
1269    /// This is the table's own width, which follows a drag or a fit as it
1270    /// happens; the delegate's [`Column::width`] is read again only on
1271    /// [`Self::refresh`].
1272    pub fn col_width(&self, col_ix: usize) -> Option<Pixels> {
1273        self.col_groups.get(col_ix).map(|group| group.width)
1274    }
1275
1276    /// Fits column `col_ix` to its content, as a double-click on its resize
1277    /// handle does.
1278    ///
1279    /// The width is [`TableDelegate::fit_column`] plus the cell padding,
1280    /// clamped to the column's `min_width` and `max_width`, and
1281    /// [`TableEvent::ColumnWidthsChanged`] is emitted when it changed. Returns
1282    /// whether the delegate gave a width: `false` when the column cannot be
1283    /// resized or the delegate answered `None`.
1284    pub fn fit_col(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> bool {
1285        if !self.is_col_resizable(col_ix) {
1286            return false;
1287        }
1288        let Some(content) = self.delegate.fit_column(col_ix, window, cx) else {
1289            return false;
1290        };
1291        let padding = self
1292            .col_groups
1293            .get(col_ix)
1294            .and_then(|group| group.column.paddings)
1295            .unwrap_or_else(|| self.options.size.table_cell_padding());
1296        // Layout rounds a width to the nearest device pixel, and text even a
1297        // fraction of a pixel wider than its cell is truncated, so round every
1298        // part up to a whole device pixel.
1299        let scale_factor = window.scale_factor();
1300        let device_pixels = |length: Pixels| (f32::from(length) * scale_factor).ceil();
1301        let mut content_pixels = device_pixels(content);
1302        // Dividing back can land one ulp below the content at a fractional
1303        // scale, and the text is cut for that ulp too.
1304        if px(content_pixels / scale_factor) < content {
1305            content_pixels += 1.;
1306        }
1307        let width = content_pixels + device_pixels(padding.left) + device_pixels(padding.right);
1308        if self.resize_cols(col_ix, px(width / scale_factor), window, cx) {
1309            let new_widths = self.col_groups.iter().map(|g| g.width).collect();
1310            cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
1311        }
1312        true
1313    }
1314
1315    /// The `ix`` is the index of the col to resize,
1316    /// and the `size` is the new size for the col.
1317    ///
1318    /// Returns whether the width changed.
1319    fn resize_cols(
1320        &mut self,
1321        ix: usize,
1322        size: Pixels,
1323        _: &mut Window,
1324        cx: &mut Context<Self>,
1325    ) -> bool {
1326        if !self.col_resizable {
1327            return false;
1328        }
1329
1330        let mut changed = false;
1331        if let Some(col_group) = self.col_groups.get_mut(ix) {
1332            if col_group.is_resizable() {
1333                let new_width = size
1334                    .max(col_group.column.min_width)
1335                    .min(col_group.column.max_width);
1336                if col_group.width != new_width {
1337                    col_group.width = new_width;
1338                    changed = true;
1339                }
1340            }
1341        }
1342
1343        if changed {
1344            self.update_header_layout(cx);
1345            cx.notify();
1346        }
1347        changed
1348    }
1349
1350    fn perform_sort(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
1351        if !self.sortable {
1352            return;
1353        }
1354
1355        let sort = self.col_groups.get(col_ix).and_then(|g| g.column.sort);
1356        if sort.is_none() {
1357            return;
1358        }
1359
1360        let sort = sort.unwrap();
1361        let sort = match sort {
1362            ColumnSort::Ascending => ColumnSort::Default,
1363            ColumnSort::Descending => ColumnSort::Ascending,
1364            ColumnSort::Default => ColumnSort::Descending,
1365        };
1366
1367        for (ix, col_group) in self.col_groups.iter_mut().enumerate() {
1368            if ix == col_ix {
1369                col_group.column.sort = Some(sort);
1370            } else {
1371                if col_group.column.sort.is_some() {
1372                    col_group.column.sort = Some(ColumnSort::Default);
1373                }
1374            }
1375        }
1376
1377        self.delegate_mut().perform_sort(col_ix, sort, window, cx);
1378
1379        cx.notify();
1380    }
1381
1382    fn move_column(
1383        &mut self,
1384        col_ix: usize,
1385        to_ix: usize,
1386        window: &mut Window,
1387        cx: &mut Context<Self>,
1388    ) {
1389        if col_ix == to_ix {
1390            return;
1391        }
1392
1393        self.delegate.move_column(col_ix, to_ix, window, cx);
1394        let col_group = self.col_groups.remove(col_ix);
1395        self.col_groups.insert(to_ix, col_group);
1396
1397        cx.emit(TableEvent::MoveColumn(col_ix, to_ix));
1398        cx.notify();
1399    }
1400
1401    /// Resolve the insertion gap for a column-header drag at the window
1402    /// coordinate `x`, or `None` when dropping there would not move the
1403    /// dragged column at `drag_col_ix`.
1404    fn drag_gap_at(&self, x: Pixels, drag_col_ix: usize) -> Option<usize> {
1405        let fixed_count = self.fixed_left_cols_count();
1406
1407        // A column can only be reordered within its own region: rendering
1408        // pins the first `fixed_count` columns, so a cross-region move would
1409        // change which columns are pinned without updating their `fixed`
1410        // flags.
1411        let drag_in_fixed = drag_col_ix < fixed_count;
1412        let pointer_in_fixed = fixed_count > 0 && x < self.fixed_head_cols_bounds.right();
1413        if drag_in_fixed != pointer_in_fixed {
1414            return None;
1415        }
1416
1417        // Columns scrolled beneath the fixed region keep stale bounds, so
1418        // resolve `x` against the fixed columns alone when it falls in that
1419        // region, and against the visible scrollable columns otherwise.
1420        let candidates = if pointer_in_fixed {
1421            0..fixed_count
1422        } else {
1423            self.calculate_visible_leaf_col_range(fixed_count).0
1424        };
1425
1426        // The gap sits after the last candidate column whose center is left of `x`.
1427        let mut gap = candidates.start;
1428        for ix in candidates {
1429            if x < self.col_groups[ix].bounds.center().x {
1430                break;
1431            }
1432            gap = ix + 1;
1433        }
1434
1435        // No gap if dropping there would put the dragged column back to
1436        // where it already is.
1437        if gap == drag_col_ix || gap == drag_col_ix + 1 {
1438            None
1439        } else {
1440            Some(gap)
1441        }
1442    }
1443
1444    /// Dispatch delegate's `load_more` method when the visible range is near the end.
1445    fn load_more_if_need(
1446        &mut self,
1447        rows_count: usize,
1448        visible_end: usize,
1449        window: &mut Window,
1450        cx: &mut Context<Self>,
1451    ) {
1452        let threshold = self.delegate.load_more_threshold();
1453        // Securely handle subtract logic to prevent attempt to subtract with overflow
1454        if visible_end >= rows_count.saturating_sub(threshold) {
1455            if !self.delegate.has_more(cx) {
1456                return;
1457            }
1458
1459            self._load_more_task = cx.spawn_in(window, async move |view, window| {
1460                _ = view.update_in(window, |view, window, cx| {
1461                    view.delegate.load_more(window, cx);
1462                });
1463            });
1464        }
1465    }
1466
1467    fn update_visible_range_if_need(
1468        &mut self,
1469        visible_range: Range<usize>,
1470        items_count: usize,
1471        axis: Axis,
1472        window: &mut Window,
1473        cx: &mut Context<Self>,
1474    ) {
1475        // Skip when visible range is only 1 item, unless there is at most 1 item.
1476        // The visual_list will use first item to measure.
1477        if visible_range.len() <= 1 && items_count > 1 {
1478            return;
1479        }
1480
1481        // Stripe filler rows are rendered past the last row; never report them.
1482        let end = visible_range.end.min(items_count);
1483        let visible_range = visible_range.start.min(end)..end;
1484        // A range wholly past the last item is stale; the list scrolls back next frame.
1485        if visible_range.is_empty() && items_count > 0 {
1486            return;
1487        }
1488
1489        if axis == Axis::Vertical {
1490            if self.visible_range.rows == visible_range {
1491                return;
1492            }
1493            self.delegate_mut()
1494                .visible_rows_changed(visible_range.clone(), window, cx);
1495            self.visible_range.rows = visible_range;
1496        } else {
1497            if self.visible_range.cols == visible_range {
1498                return;
1499            }
1500            self.delegate_mut()
1501                .visible_columns_changed(visible_range.clone(), window, cx);
1502            self.visible_range.cols = visible_range;
1503        }
1504    }
1505
1506    fn render_cell(
1507        &self,
1508        _row_ix: Option<usize>,
1509        col_ix: usize,
1510        _window: &mut Window,
1511        _cx: &mut Context<Self>,
1512    ) -> Div {
1513        let Some(col_group) = self.col_groups.get(col_ix) else {
1514            return div();
1515        };
1516
1517        let col_width = col_group.width;
1518        let col_padding = col_group.column.paddings;
1519
1520        div()
1521            .w(col_width)
1522            .h_full()
1523            .flex_shrink_0()
1524            .overflow_hidden()
1525            .whitespace_nowrap()
1526            .table_cell_size(self.options.size)
1527            // Text alignment is inherited, so this aligns the header name and
1528            // the delegate's cell text without changing how cells lay out.
1529            .map(|this| match col_group.column.align {
1530                TextAlign::Center => this.text_center(),
1531                TextAlign::Right => this.text_right(),
1532                _ => this,
1533            })
1534            .map(|this| match col_padding {
1535                Some(padding) => this
1536                    .pl(padding.left)
1537                    .pr(padding.right)
1538                    .pt(padding.top)
1539                    .pb(padding.bottom),
1540                None => this,
1541            })
1542    }
1543
1544    /// Expose a body cell to assistive technology: its role, its column and
1545    /// the delegate's [`cell_label`](TableDelegate::cell_label).
1546    ///
1547    /// In cell selection mode the cell is a `GridCell` that reports whether it
1548    /// is selected; otherwise it is a plain `Cell`.
1549    fn a11y_cell<E>(&self, cell: E, row_ix: usize, col_ix: usize, window: &Window, cx: &App) -> E
1550    where
1551        E: gpui::StatefulInteractiveElement + FluentBuilder,
1552    {
1553        let cell = if self.cell_selectable {
1554            cell.role(gpui::Role::GridCell)
1555                .aria_selected(self.selected_cell() == Some((row_ix, col_ix)))
1556        } else {
1557            cell.role(gpui::Role::Cell)
1558        };
1559        // Labels are only read through the accessibility tree, so skip
1560        // building them when no assistive technology is connected (tests
1561        // read them through test support).
1562        let label = if window.is_a11y_active() || cfg!(feature = "test-support") {
1563            self.delegate.cell_label(row_ix, col_ix, cx)
1564        } else {
1565            None
1566        };
1567        cell.aria_column_index(col_ix)
1568            .when_some(label, |this, label| this.aria_label(label))
1569    }
1570
1571    /// Show Column selection style, when the column is selected and the selection state is Column.
1572    /// Note: When a cell is selected, column selection style is not shown.
1573    fn render_col_wrap(
1574        &self,
1575        _row_ix: Option<usize>,
1576        col_ix: usize,
1577        _: &mut Window,
1578        cx: &mut Context<Self>,
1579    ) -> Div {
1580        let el = h_flex().h_full();
1581        let selectable = self.col_selectable
1582            && self
1583                .col_groups
1584                .get(col_ix)
1585                .map(|col_group| col_group.column.selectable)
1586                .unwrap_or(false);
1587
1588        // `selected_col()` is `None` outside column mode, so a selected cell
1589        // never leaves its column highlighted.
1590        if selectable && self.selected_col() == Some(col_ix) {
1591            el.bg(cx.theme().tokens.table_active)
1592        } else {
1593            el
1594        }
1595    }
1596
1597    /// The other half of that band, for the boundary on the *left* of column
1598    /// `ix` — the one owned by column `ix - 1`.
1599    ///
1600    /// It is positioned absolutely so that reaching back over the boundary
1601    /// costs the header row no width, and it paints after this column's own
1602    /// cell so that it, and not a column reorder, receives the drag.
1603    fn render_leading_resize_handle(
1604        &self,
1605        ix: usize,
1606        _: &mut Window,
1607        cx: &mut Context<Self>,
1608    ) -> impl IntoElement {
1609        let Some(prev) = ix.checked_sub(1).filter(|ix| self.is_col_resizable(*ix)) else {
1610            return div().into_any_element();
1611        };
1612
1613        self.resize_handle_band(prev, ("resizable-handle-leading", ix).into(), cx)
1614            .absolute()
1615            .top_0()
1616            .left_0()
1617            .h_full()
1618            .w(HANDLE_PADDING)
1619            .into_any_element()
1620    }
1621
1622    /// The half of column `ix`'s resize handle that lies inside column `ix`,
1623    /// along with the hairline that marks the boundary.
1624    ///
1625    /// It is [`HANDLE_PADDING`] wide, like the half on the far side, so the
1626    /// grab area is symmetric about the boundary. The negative margin keeps
1627    /// the band's net contribution to the header row zero, and `justify_end`
1628    /// pins the hairline to the column edge.
1629    fn render_resize_handle(
1630        &self,
1631        ix: usize,
1632        _: &mut Window,
1633        cx: &mut Context<Self>,
1634    ) -> impl IntoElement {
1635        if !self.is_col_resizable(ix) {
1636            return div().into_any_element();
1637        }
1638
1639        let group_id = SharedString::from(format!("resizable-handle:{}", ix));
1640
1641        self.resize_handle_band(ix, ("resizable-handle", ix).into(), cx)
1642            .group(group_id.clone())
1643            .h_full()
1644            .w(HANDLE_PADDING)
1645            .ml(-HANDLE_PADDING)
1646            .justify_end()
1647            .items_center()
1648            .child(
1649                div()
1650                    .h_full()
1651                    .justify_center()
1652                    .bg(cx.theme().table_row_border)
1653                    .group_hover(&group_id, |this| this.bg(cx.theme().border).h_full())
1654                    .w(px(1.)),
1655            )
1656            .into_any_element()
1657    }
1658
1659    fn is_col_resizable(&self, ix: usize) -> bool {
1660        self.col_resizable
1661            && self
1662                .col_groups
1663                .get(ix)
1664                .map(|col| col.is_resizable())
1665                .unwrap_or(false)
1666    }
1667
1668    /// The interactive band that drags the boundary on the right of column
1669    /// `ix`, carrying no styling that places or paints it.
1670    ///
1671    /// Paint order decides which element is offered a drag first: later
1672    /// siblings win, and a header cell starts a column reorder. So a single
1673    /// band straddling the boundary would lose its outer half to the next
1674    /// column's cell. Each boundary is covered by two bands instead, one in
1675    /// the `th` on either side, each rendered after that `th`'s own cell and
1676    /// so above everything it overlaps.
1677    fn resize_handle_band(
1678        &self,
1679        ix: usize,
1680        id: ElementId,
1681        cx: &mut Context<Self>,
1682    ) -> Stateful<Div> {
1683        h_flex()
1684            .id(id)
1685            .occlude()
1686            .cursor_col_resize()
1687            .on_drag_move(
1688                cx.listener(move |view, e: &DragMoveEvent<ResizeColumn>, window, cx| {
1689                    match e.drag(cx) {
1690                        ResizeColumn((entity_id, ix)) => {
1691                            if cx.entity_id() != *entity_id {
1692                                return;
1693                            }
1694
1695                            // sync col widths into real widths
1696                            // TODO: Consider to remove this, this may not need now.
1697                            // for (_, col_group) in view.col_groups.iter_mut().enumerate() {
1698                            //     col_group.width = col_group.bounds.size.width;
1699                            // }
1700
1701                            let ix = *ix;
1702                            if view.fitted_col == Some(ix) {
1703                                return;
1704                            }
1705                            view.resizing_col = Some(ix);
1706
1707                            let col_group = view
1708                                .col_groups
1709                                .get(ix)
1710                                .expect("BUG: invalid col index")
1711                                .clone();
1712
1713                            view.resize_cols(
1714                                ix,
1715                                e.event.position.x - HANDLE_SIZE - col_group.bounds.left(),
1716                                window,
1717                                cx,
1718                            );
1719
1720                            // scroll the table if the drag is near the edge
1721                            view.scroll_table_by_col_resizing(e.event.position, &col_group);
1722                        }
1723                    };
1724                }),
1725            )
1726            .on_drag(ResizeColumn((cx.entity_id(), ix)), |drag, _, _, cx| {
1727                cx.stop_propagation();
1728                cx.new(|_| drag.clone())
1729            })
1730            .on_mouse_down(
1731                MouseButton::Left,
1732                cx.listener(
1733                    move |view, e: &MouseDownEvent, window, cx| match e.click_count {
1734                        1 => view.fitted_col = None,
1735                        2 if view.fit_col(ix, window, cx) => view.fitted_col = Some(ix),
1736                        _ => {}
1737                    },
1738                ),
1739            )
1740            .on_mouse_up_out(
1741                MouseButton::Left,
1742                cx.listener(|view, _, _, cx| {
1743                    if view.resizing_col.is_none() {
1744                        return;
1745                    }
1746
1747                    view.resizing_col = None;
1748
1749                    let new_widths = view.col_groups.iter().map(|g| g.width).collect();
1750                    cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
1751                    cx.notify();
1752                }),
1753            )
1754    }
1755
1756    /// The border on the right edge of the fixed columns, unless turned off
1757    /// with [`DataTable::fixed_cols_border`].
1758    fn render_fixed_cols_border(&self, cx: &App) -> Option<Div> {
1759        self.options.fixed_cols_border.then(|| {
1760            div()
1761                .absolute()
1762                .top_0()
1763                .right_0()
1764                .bottom_0()
1765                .w_0()
1766                .flex_shrink_0()
1767                .border_r_1()
1768                .border_color(cx.theme().border)
1769        })
1770    }
1771
1772    /// Render the row header cell (when cell_selectable is enabled)
1773    fn render_row_header_cell(
1774        &self,
1775        row_ix: usize,
1776        is_head: bool,
1777        cx: &mut Context<Self>,
1778    ) -> impl IntoElement {
1779        div()
1780            .id(("row-header", row_ix))
1781            .w_3()
1782            .h_full()
1783            .border_r_1()
1784            .border_color(cx.theme().table_row_border)
1785            .bg(cx.theme().tokens.table_head)
1786            .flex_shrink_0()
1787            .table_cell_size(self.options.size)
1788            .when(!is_head, |this| {
1789                this.when(self.row_selectable, |this| {
1790                    this.on_click(cx.listener(move |table, e: &ClickEvent, _window, cx| {
1791                        cx.stop_propagation();
1792                        if table.selects_row_on_click(e, cx) {
1793                            table.set_selected_row(row_ix, cx);
1794                        }
1795                    }))
1796                })
1797            })
1798    }
1799
1800    fn render_sort_icon(
1801        &self,
1802        col_ix: usize,
1803        col_group: &ColGroup,
1804        _: &mut Window,
1805        cx: &mut Context<Self>,
1806    ) -> Option<impl IntoElement> {
1807        if !self.sortable {
1808            return None;
1809        }
1810
1811        let Some(sort) = col_group.column.sort else {
1812            return None;
1813        };
1814
1815        let (icon, is_on) = match sort {
1816            ColumnSort::Ascending => (IconName::SortAscending, true),
1817            ColumnSort::Descending => (IconName::SortDescending, true),
1818            ColumnSort::Default => (IconName::ChevronsUpDown, false),
1819        };
1820
1821        Some(
1822            div()
1823                .id(("icon-sort", col_ix))
1824                .test_support()
1825                .role(gpui::Role::Button)
1826                .aria_label(t!("Table.SortBy", column = col_group.column.name).to_string())
1827                .p(px(2.))
1828                .rounded(cx.theme().radius / 2.)
1829                .map(|this| match is_on {
1830                    true => this,
1831                    false => this.opacity(0.5),
1832                })
1833                .hover(|this| this.bg(cx.theme().tokens.secondary).opacity(7.))
1834                .active(|this| this.bg(cx.theme().tokens.secondary_active).opacity(1.))
1835                .on_click(
1836                    cx.listener(move |table, _, window, cx| table.perform_sort(col_ix, window, cx)),
1837                )
1838                .child(
1839                    Icon::new(icon)
1840                        .size_3()
1841                        .text_color(cx.theme().secondary_foreground),
1842                ),
1843        )
1844    }
1845
1846    /// Render the column header.
1847    /// The children must be one by one items.
1848    /// Because the horizontal scroll handle will use the child_item_bounds to
1849    /// calculate the item position for itself's `scroll_to_item` method.
1850    fn render_th(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> Div {
1851        let entity_id = cx.entity_id();
1852        let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index");
1853
1854        let movable = self.col_movable && col_group.column.movable;
1855        let paddings = col_group.column.paddings;
1856        let name = col_group.column.name.clone();
1857
1858        h_flex()
1859            .h_full()
1860            .child(
1861                self.render_cell(None, col_ix, window, cx)
1862                    .id(("col-header", col_ix))
1863                    .role(gpui::Role::ColumnHeader)
1864                    .aria_label(name.clone())
1865                    .aria_column_index(col_ix)
1866                    .test_support()
1867                    .on_click(cx.listener(move |this, _, window, cx| {
1868                        this.on_col_head_click(col_ix, window, cx);
1869                    }))
1870                    .child(
1871                        h_flex()
1872                            .size_full()
1873                            .justify_between()
1874                            .items_center()
1875                            .child(self.delegate.render_th(col_ix, window, cx))
1876                            .when_some(paddings, |this, paddings| {
1877                                // Leave right space for the sort icon, if this column have custom padding
1878                                let offset_pr =
1879                                    self.options.size.table_cell_padding().right - paddings.right;
1880                                this.pr(offset_pr.max(px(0.)))
1881                            })
1882                            .children(self.render_sort_icon(col_ix, &col_group, window, cx)),
1883                    )
1884                    .when(movable, |this| {
1885                        this.on_drag(
1886                            DragColumn {
1887                                entity_id,
1888                                col_ix,
1889                                name,
1890                                width: col_group.width,
1891                            },
1892                            |drag, _, _, cx| {
1893                                cx.stop_propagation();
1894                                cx.new(|_| drag.clone())
1895                            },
1896                        )
1897                    })
1898                    .map(|this| {
1899                        // Draw the insertion indicator on the left edge of the gap
1900                        // column, or on the right edge of the last column for the
1901                        // trailing gap. Use an absolutely positioned overlay instead
1902                        // of a border, to avoid shifting the cell content.
1903                        let last_gap = col_ix + 1 == self.col_groups.len();
1904                        match self.col_drag_gap {
1905                            Some(gap)
1906                                if cx.has_active_drag()
1907                                    && (gap == col_ix || (last_gap && gap == col_ix + 1)) =>
1908                            {
1909                                let right_side = gap == col_ix + 1;
1910                                this.relative().child(
1911                                    div()
1912                                        .absolute()
1913                                        .top_0()
1914                                        .bottom_0()
1915                                        .w(px(2.))
1916                                        .map(|d| if right_side { d.right_0() } else { d.left_0() })
1917                                        .bg(cx.theme().drag_border),
1918                                )
1919                            }
1920                            _ => this,
1921                        }
1922                    }),
1923            )
1924            // resize handle cell right side
1925            .child(self.render_resize_handle(col_ix, window, cx))
1926            // resize handle cell left side
1927            .child(self.render_leading_resize_handle(col_ix, window, cx))
1928            // to save the bounds of this col.
1929            .on_prepaint({
1930                let view = cx.entity().clone();
1931                move |bounds, _, cx| view.update(cx, |r, _| r.col_groups[col_ix].bounds = bounds)
1932            })
1933    }
1934
1935    /// Compute the visible non-fixed leaf-column range for header rendering.
1936    ///
1937    /// Returns `(visible_range, left_spacer_width)` where:
1938    /// - `visible_range` is the column-index range that should be rendered.
1939    /// - `left_spacer_width` is the total width of the off-screen left columns,
1940    ///   used as a spacer div to keep visible columns at the correct position.
1941    ///
1942    /// On the first frame `self.bounds` is zero, so a fallback that covers all
1943    /// columns is returned to avoid a blank header on initial paint.
1944    fn calculate_visible_leaf_col_range(
1945        &self,
1946        left_columns_count: usize,
1947    ) -> (Range<usize>, Pixels) {
1948        let total_cols = self.col_groups.len();
1949
1950        if self.bounds.size.width == px(0.) {
1951            return (left_columns_count..total_cols, px(0.));
1952        }
1953
1954        let fixed_width = self.fixed_head_cols_bounds.size.width;
1955        let available_width = (self.bounds.size.width - fixed_width).max(px(0.));
1956        // The scroll handle offset is negative when scrolled right; negate it
1957        // to obtain a positive distance from the left edge of the scroll area.
1958        let scroll_x = (-self.horizontal_scroll_handle.offset().x).max(px(0.));
1959
1960        // Walk left-to-right through non-fixed columns to find the first one
1961        // whose right edge enters the viewport. The accumulated width of the
1962        // skipped columns becomes the left spacer width.
1963        let mut range_start = left_columns_count;
1964        let mut left_spacer = px(0.);
1965        let mut cumulative = px(0.);
1966        for i in left_columns_count..total_cols {
1967            let right_edge = cumulative + self.col_groups[i].width;
1968            if right_edge > scroll_x {
1969                range_start = i;
1970                left_spacer = cumulative;
1971                break;
1972            }
1973            cumulative = right_edge;
1974        }
1975
1976        // Continue from `range_start` (skipping already-scanned columns) to
1977        // find the last column still within the viewport. The 200 px overdraw
1978        // buffer prevents a visible flash when the user scrolls quickly.
1979        let right_bound = scroll_x + available_width + px(200.);
1980        let mut range_end = total_cols;
1981        let mut cumulative = left_spacer; // already summed widths before `range_start`
1982        for i in range_start..total_cols {
1983            cumulative += self.col_groups[i].width;
1984            if cumulative > right_bound {
1985                range_end = (i + 1).min(total_cols);
1986                break;
1987            }
1988        }
1989
1990        (range_start..range_end, left_spacer)
1991    }
1992
1993    fn render_table_header(
1994        &mut self,
1995        left_columns_count: usize,
1996        window: &mut Window,
1997        cx: &mut Context<Self>,
1998    ) -> impl IntoElement {
1999        let view = cx.entity().clone();
2000        let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
2001
2002        // Header leaf-column virtualization.
2003        //
2004        // `render_th` creates interactive elements with resize-handle listeners.
2005        // Calling it for every column every frame is O(n) in column count; with
2006        // 1000+ columns this alone drops FPS below 60 even in release mode.
2007        //
2008        // We restrict rendering to the columns currently visible inside the
2009        // overflow-scroll viewport, surrounding them with inert spacer divs:
2010        //
2011        //   [left_spacer] [visible columns…] [right_spacer] [last_empty_col]
2012        //
2013        // The spacers preserve the flex container's total content width so that
2014        // the scrollbar range stays correct.
2015        let total_cols = self.col_groups.len();
2016        let (visible_col_range, left_spacer) =
2017            self.calculate_visible_leaf_col_range(left_columns_count);
2018
2019        let layout_len = self.header_layout.len();
2020        let header_row_height = self.header_row_height();
2021
2022        // Reset fixed head columns bounds, if no fixed columns are present
2023        if left_columns_count == 0 {
2024            self.fixed_head_cols_bounds = Bounds::default();
2025        }
2026
2027        let mut header = self.delegate_mut().render_header(window, cx);
2028        let style = header.style().clone();
2029        let layout = self.header_layout.clone();
2030
2031        header
2032            // One header row holds every column header, including the pinned
2033            // ones, which are laid out in a separate container.
2034            .role(gpui::Role::Row)
2035            .aria_row_index(0)
2036            .h_flex()
2037            .w_full()
2038            .flex_shrink_0()
2039            .bg(cx.theme().tokens.table_head)
2040            .text_color(cx.theme().table_head_foreground)
2041            .refine_style(&style)
2042            .on_drag_move(cx.listener(|table, e: &DragMoveEvent<DragColumn>, _, cx| {
2043                let drag = e.drag(cx);
2044                let (drag_entity_id, drag_col_ix) = (drag.entity_id, drag.col_ix);
2045
2046                let gap =
2047                    if drag_entity_id == cx.entity_id() && e.bounds.contains(&e.event.position) {
2048                        table.drag_gap_at(e.event.position.x, drag_col_ix)
2049                    } else {
2050                        None
2051                    };
2052
2053                if table.col_drag_gap != gap {
2054                    table.col_drag_gap = gap;
2055                    cx.notify();
2056                }
2057            }))
2058            .on_drop(cx.listener(|table, drag: &DragColumn, window, cx| {
2059                if drag.entity_id != cx.entity_id() {
2060                    return;
2061                }
2062
2063                // Insert the dragged column into the indicated gap.
2064                let Some(gap) = table.col_drag_gap.take() else {
2065                    return;
2066                };
2067                let to_ix = if drag.col_ix < gap { gap - 1 } else { gap };
2068                table.move_column(drag.col_ix, to_ix, window, cx);
2069            }))
2070            .when(self.cell_selectable && self.row_header, |this| {
2071                this.child(self.render_row_header_cell(0, true, cx))
2072            })
2073            .when(left_columns_count > 0, |this| {
2074                let view = view.clone();
2075                // Render left fixed columns
2076                this.child(
2077                    h_flex()
2078                        .relative()
2079                        .h_full()
2080                        .bg(cx.theme().tokens.table_head)
2081                        .child(v_flex().min_w_full().flex_shrink_0().children(
2082                            layout.iter().enumerate().map(|(_row_ix, row_cells)| {
2083                                h_flex()
2084                                    .min_w_full()
2085                                    .h(header_row_height)
2086                                    .border_b_1()
2087                                    .border_color(cx.theme().border)
2088                                    .children(row_cells.iter().filter_map(|cell| {
2089                                        if cell.start_leaf_col_ix < left_columns_count {
2090                                            if cell.is_leaf {
2091                                                if let Some(ix) = cell.leaf_col_ix {
2092                                                    return Some(
2093                                                        self.render_th(ix, window, cx)
2094                                                            .into_any_element(),
2095                                                    );
2096                                                }
2097                                            } else {
2098                                                return Some(
2099                                                    self.delegate_mut()
2100                                                        .render_group_th(
2101                                                            &cell.label,
2102                                                            cell.col_span,
2103                                                            cell.width,
2104                                                            window,
2105                                                            cx,
2106                                                        )
2107                                                        .into_any_element(),
2108                                                );
2109                                            }
2110                                        }
2111                                        None
2112                                    }))
2113                            }),
2114                        ))
2115                        .children(self.render_fixed_cols_border(cx))
2116                        .on_prepaint(move |bounds, _, cx| {
2117                            view.update(cx, |r, _| r.fixed_head_cols_bounds = bounds)
2118                        }),
2119                )
2120            })
2121            .child(
2122                // Columns
2123                h_flex()
2124                    .id("table-head")
2125                    .size_full()
2126                    .overflow_scroll()
2127                    .relative()
2128                    .track_scroll(&horizontal_scroll_handle)
2129                    .bg(cx.theme().tokens.table_head)
2130                    .child(v_flex().min_w_full().flex_shrink_0().children(
2131                        layout.iter().enumerate().map(|(row_ix, row_cells)| {
2132                            let is_leaf_row = row_ix + 1 == layout_len;
2133                            h_flex()
2134                                .min_w_full()
2135                                .h(header_row_height)
2136                                .border_b_1()
2137                                .border_color(cx.theme().border)
2138                                .map(|this| {
2139                                    if is_leaf_row {
2140                                        // Leaf row: apply the spacer virtualization pattern.
2141                                        // Only columns in `visible_range` are rendered; the two
2142                                        // spacer divs preserve the container's total content width
2143                                        // so the scrollbar range stays correct.
2144                                        this.when(left_spacer > px(0.), |r| {
2145                                            r.child(div().w(left_spacer).h_full().flex_shrink_0())
2146                                        })
2147                                        .children(row_cells.iter().filter_map(|cell| {
2148                                            if cell.is_leaf {
2149                                                let ix = cell.leaf_col_ix?;
2150                                                if !visible_col_range.contains(&ix) {
2151                                                    return None;
2152                                                }
2153                                                Some(
2154                                                    self.render_th(ix, window, cx)
2155                                                        .into_any_element(),
2156                                                )
2157                                            } else {
2158                                                None
2159                                            }
2160                                        }))
2161                                        .when(visible_col_range.end < total_cols, |r| {
2162                                            let right_spacer: Pixels = self.col_groups
2163                                                [visible_col_range.end..total_cols]
2164                                                .iter()
2165                                                .map(|g| g.width)
2166                                                .sum();
2167                                            r.child(div().w(right_spacer).h_full().flex_shrink_0())
2168                                        })
2169                                        .child(self.delegate.render_last_empty_col(window, cx))
2170                                    } else {
2171                                        // Group header rows have far fewer cells (one per group),
2172                                        // so the cost of rendering all of them is negligible.
2173                                        this.children(row_cells.iter().filter_map(|cell| {
2174                                            if cell.start_leaf_col_ix >= left_columns_count {
2175                                                if cell.is_leaf {
2176                                                    if let Some(ix) = cell.leaf_col_ix {
2177                                                        return Some(
2178                                                            self.render_th(ix, window, cx)
2179                                                                .into_any_element(),
2180                                                        );
2181                                                    }
2182                                                } else {
2183                                                    return Some(
2184                                                        self.delegate_mut()
2185                                                            .render_group_th(
2186                                                                &cell.label,
2187                                                                cell.col_span,
2188                                                                cell.width,
2189                                                                window,
2190                                                                cx,
2191                                                            )
2192                                                            .into_any_element(),
2193                                                    );
2194                                                }
2195                                            }
2196                                            None
2197                                        }))
2198                                        .child(self.delegate.render_last_empty_col(window, cx))
2199                                    }
2200                                })
2201                        }),
2202                    )),
2203            )
2204    }
2205
2206    #[allow(clippy::too_many_arguments)]
2207    fn render_table_row(
2208        &mut self,
2209        row_ix: usize,
2210        rows_count: usize,
2211        left_columns_count: usize,
2212        col_sizes: Rc<Vec<gpui::Size<Pixels>>>,
2213        columns_count: usize,
2214        is_filled: bool,
2215        window: &mut Window,
2216        cx: &mut Context<Self>,
2217    ) -> gpui::AnyElement {
2218        let is_stripe_row = self.options.stripe && row_ix % 2 != 0;
2219        // `selected_row()` is `None` outside row mode, so a selected cell or
2220        // column never highlights its row.
2221        let is_selected = self.selected_row() == Some(row_ix);
2222        let view = cx.entity().clone();
2223        let row_height = self.options.size.table_row_height();
2224
2225        if row_ix < rows_count {
2226            let is_last_row = row_ix + 1 == rows_count;
2227            let need_render_border = is_selected || !is_last_row || !is_filled;
2228
2229            let mut tr = self.delegate.render_tr(row_ix, window, cx);
2230            let style = tr.style().clone();
2231
2232            tr.test_support()
2233                .role(gpui::Role::Row)
2234                // Row 0 is the header row.
2235                .aria_row_index(row_ix + 1)
2236                .aria_selected(is_selected)
2237                .h_flex()
2238                .w_full()
2239                .h(row_height)
2240                .when(need_render_border, |this| {
2241                    this.border_b_1().border_color(cx.theme().table_row_border)
2242                })
2243                .when(is_stripe_row, |this| this.bg(cx.theme().tokens.table_even))
2244                .refine_style(&style)
2245                .hover(|this| {
2246                    if is_selected || self.right_clicked_row == Some(row_ix) {
2247                        this
2248                    } else {
2249                        this.bg(cx.theme().tokens.table_hover)
2250                    }
2251                })
2252                .when(self.cell_selectable && self.row_header, |this| {
2253                    this.child(self.render_row_header_cell(row_ix, false, cx))
2254                })
2255                .when(left_columns_count > 0, |this| {
2256                    // Left fixed columns
2257                    this.child(
2258                        h_flex()
2259                            .relative()
2260                            .h_full()
2261                            .children({
2262                                let mut items = Vec::with_capacity(left_columns_count);
2263
2264                                (0..left_columns_count).for_each(|col_ix| {
2265                                    let is_cell_selected =
2266                                        self.selected_cell() == Some((row_ix, col_ix));
2267                                    let is_cell_right_clicked =
2268                                        self.right_clicked_cell == Some((row_ix, col_ix));
2269
2270                                    items.push(
2271                                        self.render_col_wrap(Some(row_ix), col_ix, window, cx)
2272                                            .child(
2273                                                self.a11y_cell(
2274                                                    self.render_cell(
2275                                                        Some(row_ix),
2276                                                        col_ix,
2277                                                        window,
2278                                                        cx,
2279                                                    )
2280                                                    .id(format!("table-cell:{}:{}", row_ix, col_ix))
2281                                                    .test_support(),
2282                                                    row_ix,
2283                                                    col_ix,
2284                                                    window,
2285                                                    cx,
2286                                                )
2287                                                .relative()
2288                                                .child(
2289                                                    self.measure_render_td(
2290                                                        row_ix, col_ix, window, cx,
2291                                                    ),
2292                                                )
2293                                                .when(is_cell_selected, |this| {
2294                                                    this.child(
2295                                                        div()
2296                                                            .absolute()
2297                                                            .inset_0()
2298                                                            .bg(cx.theme().tokens.table_active),
2299                                                    )
2300                                                })
2301                                                .when(
2302                                                    is_cell_right_clicked && !is_cell_selected,
2303                                                    |this| {
2304                                                        this.child(
2305                                                            div()
2306                                                                .absolute()
2307                                                                .inset_0()
2308                                                                .border_1()
2309                                                                .border_color(
2310                                                                    cx.theme()
2311                                                                        .table_active_border
2312                                                                        .opacity(0.5),
2313                                                                ),
2314                                                        )
2315                                                    },
2316                                                )
2317                                                .when(self.cell_selectable, |this| {
2318                                                    this.on_click(cx.listener(
2319                                                        move |table, e, window, cx| {
2320                                                            table.on_cell_click(
2321                                                                e, row_ix, col_ix, window, cx,
2322                                                            );
2323                                                        },
2324                                                    ))
2325                                                    .on_mouse_down(
2326                                                        MouseButton::Right,
2327                                                        cx.listener(move |table, e, window, cx| {
2328                                                            table.on_cell_right_click(
2329                                                                e, row_ix, col_ix, window, cx,
2330                                                            );
2331                                                        }),
2332                                                    )
2333                                                }),
2334                                            ),
2335                                    );
2336                                });
2337
2338                                items
2339                            })
2340                            .children(self.render_fixed_cols_border(cx)),
2341                    )
2342                })
2343                .child(
2344                    h_flex()
2345                        .flex_1()
2346                        .h_full()
2347                        .overflow_hidden()
2348                        .relative()
2349                        .child(
2350                            crate::virtual_list::virtual_list(
2351                                view,
2352                                row_ix,
2353                                Axis::Horizontal,
2354                                col_sizes,
2355                                {
2356                                    move |table, visible_range: Range<usize>, window, cx| {
2357                                        table.update_visible_range_if_need(
2358                                            visible_range.clone(),
2359                                            columns_count.saturating_sub(left_columns_count),
2360                                            Axis::Horizontal,
2361                                            window,
2362                                            cx,
2363                                        );
2364
2365                                        let mut items = Vec::with_capacity(
2366                                            visible_range.end - visible_range.start,
2367                                        );
2368
2369                                        visible_range.for_each(|col_ix| {
2370                                            let col_ix = col_ix + left_columns_count;
2371                                            let is_cell_selected =
2372                                                table.selected_cell() == Some((row_ix, col_ix));
2373                                            let is_cell_right_clicked =
2374                                                table.right_clicked_cell == Some((row_ix, col_ix));
2375
2376                                            let el = table
2377                                                .render_col_wrap(Some(row_ix), col_ix, window, cx)
2378                                                .child(
2379                                                    table
2380                                                        .a11y_cell(
2381                                                            table
2382                                                                .render_cell(
2383                                                                    Some(row_ix),
2384                                                                    col_ix,
2385                                                                    window,
2386                                                                    cx,
2387                                                                )
2388                                                                .id(format!(
2389                                                                    "table-cell-{}:{}",
2390                                                                    row_ix, col_ix
2391                                                                ))
2392                                                                .test_support(),
2393                                                            row_ix,
2394                                                            col_ix,
2395                                                            window,
2396                                                            cx,
2397                                                        )
2398                                                        .relative()
2399                                                        .child(table.measure_render_td(
2400                                                            row_ix, col_ix, window, cx,
2401                                                        ))
2402                                                        .when(is_cell_selected, |this| {
2403                                                            this.child(
2404                                                                div().absolute().inset_0().bg(cx
2405                                                                    .theme()
2406                                                                    .tokens
2407                                                                    .table_active),
2408                                                            )
2409                                                        })
2410                                                        .when(
2411                                                            is_cell_right_clicked
2412                                                                && !is_cell_selected,
2413                                                            |this| {
2414                                                                this.child(
2415                                                                    div()
2416                                                                        .absolute()
2417                                                                        .inset_0()
2418                                                                        .border_1()
2419                                                                        .border_color(
2420                                                                            cx.theme()
2421                                                                                .table_active_border
2422                                                                                .opacity(0.5),
2423                                                                        ),
2424                                                                )
2425                                                            },
2426                                                        )
2427                                                        .when(table.cell_selectable, |this| {
2428                                                            this.on_click(cx.listener(
2429                                                                move |table, e, window, cx| {
2430                                                                    cx.stop_propagation();
2431                                                                    table.on_cell_click(
2432                                                                        e, row_ix, col_ix, window,
2433                                                                        cx,
2434                                                                    );
2435                                                                },
2436                                                            ))
2437                                                            .on_mouse_down(
2438                                                                MouseButton::Right,
2439                                                                cx.listener(
2440                                                                    move |table, e, window, cx| {
2441                                                                        table.on_cell_right_click(
2442                                                                            e, row_ix, col_ix,
2443                                                                            window, cx,
2444                                                                        );
2445                                                                    },
2446                                                                ),
2447                                                            )
2448                                                        }),
2449                                                );
2450
2451                                            items.push(el);
2452                                        });
2453
2454                                        items
2455                                    }
2456                                },
2457                            )
2458                            .with_scroll_handle(&self.horizontal_scroll_handle),
2459                        )
2460                        .child(self.delegate.render_last_empty_col(window, cx)),
2461                )
2462                // Row selected style
2463                .when(is_selected, |this| {
2464                    let bg = if cx.theme().list.active_highlight {
2465                        cx.theme().tokens.table_active
2466                    } else {
2467                        cx.theme().tokens.accent
2468                    };
2469                    this.bg(bg)
2470                })
2471                // Row right click row style
2472                .when(self.right_clicked_row == Some(row_ix), |this| {
2473                    this.border_color(gpui::transparent_white()).child(
2474                        div()
2475                            .top(if row_ix == 0 { px(0.) } else { px(-1.) })
2476                            .left(px(0.))
2477                            .right(px(0.))
2478                            .bottom(px(-1.))
2479                            .absolute()
2480                            .border_1()
2481                            .border_color(cx.theme().selection),
2482                    )
2483                })
2484                .on_mouse_down(
2485                    MouseButton::Right,
2486                    cx.listener(move |this, e, window, cx| {
2487                        this.on_row_right_click(e, Some(row_ix), window, cx);
2488                    }),
2489                )
2490                .on_click(cx.listener(move |this, e, window, cx| {
2491                    this.on_row_left_click(e, row_ix, window, cx);
2492                }))
2493                .into_any_element()
2494        } else {
2495            let cols_width: Pixels = self
2496                .col_groups
2497                .iter()
2498                .take(columns_count)
2499                .map(|col_group| col_group.width)
2500                .sum();
2501
2502            // Render fake rows to fill the rest table space
2503            self.delegate
2504                .render_tr(row_ix, window, cx)
2505                .h_flex()
2506                .w_full()
2507                .h(row_height)
2508                .border_b_1()
2509                .border_color(cx.theme().table_row_border)
2510                .when(is_stripe_row, |this| this.bg(cx.theme().tokens.table_even))
2511                .when(self.cell_selectable && self.row_header, |this| {
2512                    // Render empty row header cell for fake rows
2513                    this.child(
2514                        div()
2515                            .w(px(40.))
2516                            .h_full()
2517                            .flex_shrink_0()
2518                            .table_cell_size(self.options.size),
2519                    )
2520                })
2521                // The fake cells paint nothing, so a single spacer as wide as all
2522                // the columns keeps the last empty column in place without laying
2523                // out one element per column.
2524                .child(div().flex_shrink_0().h_full().w(cols_width))
2525                .child(self.delegate.render_last_empty_col(window, cx))
2526                .into_any_element()
2527        }
2528    }
2529
2530    /// Calculate the extra rows needed to fill the table empty space when `stripe` is true.
2531    fn calculate_extra_rows_needed(
2532        &self,
2533        total_height: Pixels,
2534        actual_height: Pixels,
2535        row_height: Pixels,
2536    ) -> usize {
2537        let mut extra_rows_needed = 0;
2538
2539        let remaining_height = total_height - actual_height;
2540        if remaining_height > px(0.) {
2541            extra_rows_needed = (remaining_height / row_height).floor() as usize;
2542        }
2543
2544        extra_rows_needed
2545    }
2546
2547    #[inline]
2548    fn measure_render_td(
2549        &mut self,
2550        row_ix: usize,
2551        col_ix: usize,
2552        window: &mut Window,
2553        cx: &mut Context<Self>,
2554    ) -> impl IntoElement {
2555        if !crate::measure_enable() {
2556            return self
2557                .delegate
2558                .render_td(row_ix, col_ix, window, cx)
2559                .into_any_element();
2560        }
2561
2562        let start = std::time::Instant::now();
2563        let el = self.delegate.render_td(row_ix, col_ix, window, cx);
2564        self._measure.push(start.elapsed());
2565        el.into_any_element()
2566    }
2567
2568    fn measure(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
2569        if !crate::measure_enable() {
2570            return;
2571        }
2572
2573        // Print avg measure time of each td
2574        if self._measure.len() > 0 {
2575            let total = self
2576                ._measure
2577                .iter()
2578                .fold(Duration::default(), |acc, d| acc + *d);
2579            let avg = total / self._measure.len() as u32;
2580            eprintln!(
2581                "last render {} cells total: {:?}, avg: {:?}",
2582                self._measure.len(),
2583                total,
2584                avg,
2585            );
2586        }
2587        self._measure.clear();
2588    }
2589
2590    fn render_vertical_scrollbar(
2591        &mut self,
2592        _: &mut Window,
2593        _: &mut Context<Self>,
2594    ) -> Option<impl IntoElement> {
2595        Some(
2596            div()
2597                .absolute()
2598                .top(self.header_band_height())
2599                .right_0()
2600                .bottom_0()
2601                .w(Scrollbar::width())
2602                .child(
2603                    Scrollbar::vertical(&self.vertical_scroll_handle)
2604                        .viewport_from_layout()
2605                        .max_fps(60),
2606                ),
2607        )
2608    }
2609
2610    fn render_horizontal_scrollbar(
2611        &mut self,
2612        _: &mut Window,
2613        _: &mut Context<Self>,
2614    ) -> impl IntoElement {
2615        div()
2616            .absolute()
2617            .left(self.fixed_head_cols_bounds.size.width)
2618            .right_0()
2619            .bottom_0()
2620            .h(Scrollbar::width())
2621            .child(Scrollbar::horizontal(&self.horizontal_scroll_handle).viewport_from_layout())
2622    }
2623}
2624
2625impl<D> Focusable for TableState<D>
2626where
2627    D: TableDelegate,
2628{
2629    fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
2630        self.focus_handle.clone()
2631    }
2632}
2633impl<D> EventEmitter<TableEvent> for TableState<D> where D: TableDelegate {}
2634
2635impl<D> Render for TableState<D>
2636where
2637    D: TableDelegate,
2638{
2639    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2640        self.measure(window, cx);
2641
2642        let columns_count = self.delegate.columns_count(cx);
2643        let left_columns_count = self
2644            .col_groups
2645            .iter()
2646            .filter(|col| self.col_fixed && col.column.fixed == Some(ColumnFixed::Left))
2647            .count();
2648        let rows_count = self.delegate.rows_count(cx);
2649        let loading = self.delegate.loading(cx);
2650
2651        let row_height = self.options.size.table_row_height();
2652        let total_height = self
2653            .vertical_scroll_handle
2654            .0
2655            .borrow()
2656            .base_handle
2657            .bounds()
2658            .size
2659            .height;
2660        let actual_height = row_height * rows_count as f32;
2661        let extra_rows_count =
2662            self.calculate_extra_rows_needed(total_height, actual_height, row_height);
2663        let render_rows_count = if self.options.stripe {
2664            rows_count + extra_rows_count
2665        } else {
2666            rows_count
2667        };
2668        let has_right_click = self.right_clicked_row.is_some() || self.right_clicked_cell.is_some();
2669        let is_filled = total_height > Pixels::ZERO && total_height <= actual_height;
2670
2671        let loading_view = if loading {
2672            Some(
2673                self.delegate
2674                    .render_loading(self.options.size, window, cx)
2675                    .into_any_element(),
2676            )
2677        } else {
2678            None
2679        };
2680
2681        let empty_view = if rows_count == 0 {
2682            // The rows list is not rendered, so report the empty range here.
2683            self.update_visible_range_if_need(0..0, 0, Axis::Vertical, window, cx);
2684            Some(
2685                div()
2686                    .size_full()
2687                    .child(self.delegate.render_empty(window, cx))
2688                    .into_any_element(),
2689            )
2690        } else {
2691            None
2692        };
2693
2694        let inner_table = v_flex()
2695            .id("table-inner")
2696            .test_support()
2697            // A table that selects rows or cells is a grid. The counts cover
2698            // every row, not just the rendered ones, plus the header row.
2699            .role(if self.row_selectable || self.cell_selectable {
2700                gpui::Role::Grid
2701            } else {
2702                gpui::Role::Table
2703            })
2704            .aria_row_count(rows_count + 1)
2705            .aria_column_count(columns_count)
2706            .size_full()
2707            .overflow_hidden()
2708            .child(self.render_table_header(left_columns_count, window, cx))
2709            .context_menu({
2710                let view = cx.entity().clone();
2711                move |this, window: &mut Window, cx: &mut Context<PopupMenu>| {
2712                    if let Some(row_ix) = view.read(cx).right_clicked_row {
2713                        view.update(cx, |menu, cx| {
2714                            menu.delegate_mut().context_menu(row_ix, this, window, cx)
2715                        })
2716                    } else {
2717                        this
2718                    }
2719                }
2720            })
2721            .map(|this| {
2722                if rows_count == 0 {
2723                    this.children(empty_view)
2724                } else {
2725                    this.child(
2726                        h_flex().id("table-body").flex_grow_1().size_full().child(
2727                            uniform_list(
2728                                "table-uniform-list",
2729                                render_rows_count,
2730                                cx.processor(
2731                                    move |table, visible_range: Range<usize>, window, cx| {
2732                                        // Use `col.width` (always up-to-date) rather than
2733                                        // `col.bounds.size.width`, which is only set after
2734                                        // prepaint and is therefore zero on the first frame.
2735                                        let col_sizes: Rc<Vec<gpui::Size<Pixels>>> = Rc::new(
2736                                            table
2737                                                .col_groups
2738                                                .iter()
2739                                                .skip(left_columns_count)
2740                                                .map(|col| gpui::Size {
2741                                                    width: col.width,
2742                                                    height: px(0.),
2743                                                })
2744                                                .collect(),
2745                                        );
2746
2747                                        table.load_more_if_need(
2748                                            rows_count,
2749                                            visible_range.end,
2750                                            window,
2751                                            cx,
2752                                        );
2753                                        table.update_visible_range_if_need(
2754                                            visible_range.clone(),
2755                                            rows_count,
2756                                            Axis::Vertical,
2757                                            window,
2758                                            cx,
2759                                        );
2760
2761                                        if visible_range.end > rows_count {
2762                                            table.scroll_to_row(
2763                                                std::cmp::min(
2764                                                    visible_range.start,
2765                                                    rows_count.saturating_sub(1),
2766                                                ),
2767                                                cx,
2768                                            );
2769                                        }
2770
2771                                        let mut items = Vec::with_capacity(
2772                                            visible_range.end.saturating_sub(visible_range.start),
2773                                        );
2774
2775                                        // Render fake rows to fill the table
2776                                        visible_range.for_each(|row_ix| {
2777                                            // Render real rows for available data
2778                                            items.push(table.render_table_row(
2779                                                row_ix,
2780                                                rows_count,
2781                                                left_columns_count,
2782                                                col_sizes.clone(),
2783                                                columns_count,
2784                                                is_filled,
2785                                                window,
2786                                                cx,
2787                                            ));
2788                                        });
2789
2790                                        items
2791                                    },
2792                                ),
2793                            )
2794                            .flex_grow_1()
2795                            .size_full()
2796                            .with_sizing_behavior(ListSizingBehavior::Auto)
2797                            .track_scroll(&self.vertical_scroll_handle)
2798                            .map(|rows| {
2799                                gpui_base::ScrollBounce::new(
2800                                    "table-scroll-bounce",
2801                                    &self.vertical_scroll_handle,
2802                                    rows,
2803                                )
2804                                .when_some(self.options.scroll_bounce, |bounce, on| {
2805                                    bounce.enabled(on)
2806                                })
2807                            })
2808                            .into_any_element(),
2809                        ),
2810                    )
2811                }
2812            });
2813
2814        div()
2815            .size_full()
2816            .children(loading_view)
2817            .when(!loading, |this| {
2818                this.child(inner_table)
2819                    .child(ScrollableMask::new(
2820                        Axis::Horizontal,
2821                        &self.horizontal_scroll_handle,
2822                    ))
2823                    // Keep vertical wheel scrolling from leaking into an
2824                    // ancestor scroller. Skipped when the table is empty:
2825                    // the `uniform_list` is not rendered then, so the
2826                    // handle's offset and `max_offset` are stale.
2827                    .when(rows_count > 0, |this| {
2828                        this.child(ScrollableMask::new(
2829                            Axis::Vertical,
2830                            &self.vertical_scroll_handle.0.borrow().base_handle,
2831                        ))
2832                    })
2833                    .when(has_right_click, |this| {
2834                        this.on_mouse_down_out(cx.listener(|this, e, window, cx| {
2835                            this.on_row_right_click(e, None, window, cx);
2836                            cx.notify();
2837                        }))
2838                    })
2839            })
2840            .on_prepaint({
2841                let state = cx.entity();
2842                move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
2843            })
2844            .when(!window.is_inspector_picking(cx), |this| {
2845                this.child(
2846                    div()
2847                        .absolute()
2848                        .top_0()
2849                        .size_full()
2850                        .when(self.options.scrollbar_visible.bottom, |this| {
2851                            this.child(self.render_horizontal_scrollbar(window, cx))
2852                        })
2853                        .when(
2854                            self.options.scrollbar_visible.right && rows_count > 0,
2855                            |this| this.children(self.render_vertical_scrollbar(window, cx)),
2856                        ),
2857                )
2858            })
2859    }
2860}
2861
2862#[cfg(test)]
2863mod tests {
2864    use gpui::{App, Entity, TestAppContext};
2865
2866    use super::*;
2867
2868    struct WidthsDelegate;
2869
2870    struct BounceDelegate;
2871
2872    impl TableDelegate for BounceDelegate {
2873        fn columns_count(&self, _: &App) -> usize {
2874            1
2875        }
2876        fn rows_count(&self, _: &App) -> usize {
2877            40
2878        }
2879        fn column(&self, _: usize, _: &App) -> Column {
2880            Column::new("value", "Value")
2881        }
2882        fn render_td(
2883            &mut self,
2884            row: usize,
2885            _: usize,
2886            _: &mut Window,
2887            _: &mut Context<TableState<Self>>,
2888        ) -> impl IntoElement {
2889            div().child(row.to_string())
2890        }
2891    }
2892
2893    struct BounceTable(Entity<TableState<BounceDelegate>>);
2894
2895    impl Render for BounceTable {
2896        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2897            div()
2898                .w(px(240.))
2899                .h(px(200.))
2900                .child(DataTable::new(&self.0).scroll_bounce(true))
2901        }
2902    }
2903
2904    #[gpui::test]
2905    fn scroll_bounce_table_keeps_headers_fixed_and_accepts_new_wheel_input(
2906        cx: &mut TestAppContext,
2907    ) {
2908        cx.update(crate::init);
2909        let (view, cx) = cx.add_window_view(|window, cx| {
2910            BounceTable(cx.new(|cx| TableState::new(BounceDelegate, window, cx)))
2911        });
2912        cx.update(|window, cx| window.draw(cx).clear(cx));
2913        let state = view.read_with(cx, |view, _| view.0.clone());
2914        let header = state.read_with(cx, |state, _| state.col_groups[0].bounds);
2915        let wheel = |cx: &mut gpui::VisualTestContext, dy| {
2916            cx.simulate_event(gpui::ScrollEvent {
2917                position: gpui::point(px(100.), px(100.)),
2918                delta: gpui::ScrollDelta::Lines(gpui::point(0., dy)),
2919                ..Default::default()
2920            });
2921        };
2922        for (dy, phase) in [
2923            (-10000., gpui::TouchPhase::Started),
2924            (0., gpui::TouchPhase::Ended),
2925        ] {
2926            cx.simulate_event(gpui::ScrollEvent {
2927                position: gpui::point(px(100.), px(100.)),
2928                delta: gpui::ScrollDelta::Pixels(gpui::point(px(0.), px(dy))),
2929                touch_phase: phase,
2930                ..Default::default()
2931            });
2932        }
2933        cx.update(|window, cx| window.draw(cx).clear(cx));
2934        let end = state.read_with(cx, |state, _| {
2935            state
2936                .vertical_scroll_handle
2937                .0
2938                .borrow()
2939                .base_handle
2940                .offset()
2941                .y
2942        });
2943        assert!(end < px(0.));
2944        cx.executor().advance_clock(Duration::from_millis(200));
2945        cx.update(|window, cx| window.draw(cx).clear(cx));
2946        assert_eq!(
2947            state.read_with(cx, |state, _| state.col_groups[0].bounds),
2948            header
2949        );
2950        wheel(cx, 2.);
2951        assert!(
2952            state.read_with(cx, |state, _| state
2953                .vertical_scroll_handle
2954                .0
2955                .borrow()
2956                .base_handle
2957                .offset()
2958                .y)
2959                > end
2960        );
2961    }
2962
2963    impl TableDelegate for WidthsDelegate {
2964        fn columns_count(&self, _: &App) -> usize {
2965            6
2966        }
2967
2968        fn rows_count(&self, _: &App) -> usize {
2969            0
2970        }
2971
2972        fn column(&self, col_ix: usize, _: &App) -> Column {
2973            match col_ix {
2974                0 => Column::new("narrow", "Narrow")
2975                    .min_width(px(80.))
2976                    .width(px(40.)),
2977                1 => Column::new("wide", "Wide")
2978                    .max_width(px(120.))
2979                    .width(px(300.)),
2980                // `width` alone below the default `min_width` of 20px.
2981                2 => Column::new("tiny", "Tiny").width(px(10.)),
2982                // The default resize protection does not limit initial width.
2983                3 => Column::new("wide_default", "Wide Default").width(px(2000.)),
2984                // Applications can explicitly allow wider resize ranges.
2985                4 => Column::new("wide_explicit", "Wide Explicit")
2986                    .max_width(px(2400.))
2987                    .width(px(2000.)),
2988                // `min_width` above `max_width` must not panic.
2989                _ => Column::new("inverted", "Inverted")
2990                    .min_width(px(200.))
2991                    .max_width(px(100.)),
2992            }
2993        }
2994
2995        fn render_td(
2996            &mut self,
2997            _: usize,
2998            _: usize,
2999            _: &mut Window,
3000            _: &mut Context<TableState<Self>>,
3001        ) -> impl IntoElement {
3002            div()
3003        }
3004    }
3005
3006    #[gpui::test]
3007    fn column_resize_bounds_preserve_initial_widths(cx: &mut TestAppContext) {
3008        cx.skip_drawing();
3009        let window = cx.add_window(|window, cx| TableState::new(WidthsDelegate, window, cx));
3010
3011        window
3012            .update(cx, |table, window, cx| {
3013                let widths = |table: &TableState<WidthsDelegate>| {
3014                    table
3015                        .col_groups
3016                        .iter()
3017                        .map(|col_group| col_group.width)
3018                        .collect::<Vec<_>>()
3019                };
3020                assert_eq!(
3021                    widths(table),
3022                    [px(40.), px(300.), px(10.), px(2000.), px(2000.), px(100.)]
3023                );
3024
3025                for ix in 0..table.col_groups.len() {
3026                    table.resize_cols(ix, px(10000.), window, cx);
3027                }
3028                assert_eq!(
3029                    widths(table),
3030                    [
3031                        px(1200.),
3032                        px(120.),
3033                        px(1200.),
3034                        px(1200.),
3035                        px(2400.),
3036                        px(100.)
3037                    ]
3038                );
3039
3040                for ix in 0..table.col_groups.len() {
3041                    table.resize_cols(ix, px(-100.), window, cx);
3042                }
3043                assert_eq!(
3044                    widths(table),
3045                    [px(80.), px(20.), px(20.), px(20.), px(20.), px(100.)]
3046                );
3047            })
3048            .unwrap();
3049    }
3050}