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