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