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