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