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