Skip to main content

gpui_component/table/
state.rs

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