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