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