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