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