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