Skip to main content

gpui_component/table/
state.rs

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