1use gpui_base::TestSupportExt as _;
2use std::{ops::Range, rc::Rc, time::Duration};
3
4use crate::{
5 ActiveTheme, ElementExt, Icon, IconName, StyleSized as _, StyledExt, VirtualListScrollHandle,
6 actions::{
7 Cancel, SelectDown, SelectFirst, SelectLast, SelectNextColumn, SelectPageDown,
8 SelectPageUp, SelectPrevColumn, SelectUp,
9 },
10 h_flex,
11 menu::{ContextMenuExt, PopupMenu},
12 scroll::{ScrollableMask, Scrollbar},
13 v_flex,
14};
15use gpui::{
16 AppContext, Axis, Bounds, ClickEvent, Context, Div, DragMoveEvent, ElementId, EventEmitter,
17 FocusHandle, Focusable, InteractiveElement, IntoElement, ListSizingBehavior, MouseButton,
18 MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, SharedString, Stateful,
19 StatefulInteractiveElement as _, Styled, Task, UniformListScrollHandle, Window, div,
20 prelude::FluentBuilder, px, uniform_list,
21};
22use rust_i18n::t;
23
24use super::*;
25
26const HANDLE_SIZE: Pixels = px(2.);
28
29const HANDLE_PADDING: Pixels = px(4.);
35
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
37enum SelectionMode {
38 Column,
39 Row,
40 Cell,
41}
42
43impl SelectionMode {
44 #[inline(always)]
45 fn is_row(&self) -> bool {
46 matches!(self, SelectionMode::Row)
47 }
48
49 #[inline(always)]
50 fn is_column(&self) -> bool {
51 matches!(self, SelectionMode::Column)
52 }
53
54 #[inline(always)]
55 fn is_cell(&self) -> bool {
56 matches!(self, SelectionMode::Cell)
57 }
58}
59
60#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
67pub enum TableSelection {
68 #[default]
70 None,
71 Row(usize),
73 Column(usize),
75 Cell(usize, usize),
77}
78
79#[derive(Clone)]
81pub enum TableEvent {
82 SelectRow(usize),
84 DoubleClickedRow(usize),
86 SelectColumn(usize),
88 SelectCell(usize, usize),
95 DoubleClickedCell(usize, usize),
102 ColumnWidthsChanged(Vec<Pixels>),
106 MoveColumn(usize, usize),
111 RightClickedRow(Option<usize>),
116 RightClickedCell(usize, usize),
124 ClearSelection,
128}
129
130#[derive(Debug, Default)]
132pub struct TableVisibleRange {
133 rows: Range<usize>,
135 cols: Range<usize>,
137}
138
139impl TableVisibleRange {
140 pub fn rows(&self) -> &Range<usize> {
142 &self.rows
143 }
144
145 pub fn cols(&self) -> &Range<usize> {
147 &self.cols
148 }
149}
150
151#[derive(Clone)]
201pub(crate) struct HeaderCell {
202 pub label: SharedString,
203 pub width: Pixels,
204 col_span: usize,
205 is_leaf: bool,
206 leaf_col_ix: Option<usize>,
207 start_leaf_col_ix: usize,
208}
209
210pub struct TableState<D: TableDelegate> {
211 focus_handle: FocusHandle,
212 delegate: D,
213 pub(super) options: TableOptions,
214 bounds: Bounds<Pixels>,
216 fixed_head_cols_bounds: Bounds<Pixels>,
218
219 col_groups: Vec<ColGroup>,
220 header_layout: Vec<Vec<HeaderCell>>,
221
222 pub loop_selection: bool,
226 pub col_selectable: bool,
228 pub row_selectable: bool,
230 select_on_modifier_click: bool,
241 pub cell_selectable: bool,
250 pub row_header: bool,
259 pub sortable: bool,
261 pub col_resizable: bool,
263 pub col_movable: bool,
265 pub col_fixed: bool,
267
268 pub vertical_scroll_handle: UniformListScrollHandle,
269 pub horizontal_scroll_handle: VirtualListScrollHandle,
270
271 selected_row: Option<usize>,
272 selection_mode: SelectionMode,
273 right_clicked_row: Option<usize>,
274 right_clicked_cell: Option<(usize, usize)>,
275 selected_col: Option<usize>,
276 selected_cell: Option<(usize, usize)>,
277
278 resizing_col: Option<usize>,
280 fitted_col: Option<usize>,
286
287 col_drag_gap: Option<usize>,
291
292 visible_range: TableVisibleRange,
294
295 _measure: Vec<Duration>,
296 _load_more_task: Task<()>,
297}
298
299impl<D> TableState<D>
300where
301 D: TableDelegate,
302{
303 pub fn new(delegate: D, _: &mut Window, cx: &mut Context<Self>) -> Self {
305 let mut this = Self {
306 focus_handle: cx.focus_handle().tab_stop(true),
307 options: TableOptions::default(),
308 delegate,
309 col_groups: Vec::new(),
310 header_layout: Vec::new(),
311 horizontal_scroll_handle: VirtualListScrollHandle::new(),
312 vertical_scroll_handle: UniformListScrollHandle::new(),
313 selection_mode: SelectionMode::Row,
314 selected_row: None,
315 right_clicked_row: None,
316 right_clicked_cell: None,
317 selected_col: None,
318 selected_cell: None,
319 resizing_col: None,
320 fitted_col: None,
321 col_drag_gap: None,
322 bounds: Bounds::default(),
323 fixed_head_cols_bounds: Bounds::default(),
324 visible_range: TableVisibleRange::default(),
325 loop_selection: true,
326 col_selectable: true,
327 row_selectable: true,
328 select_on_modifier_click: true,
329 cell_selectable: false,
330 row_header: true,
331 sortable: true,
332 col_movable: true,
333 col_resizable: true,
334 col_fixed: true,
335 _load_more_task: Task::ready(()),
336 _measure: Vec::new(),
337 };
338
339 this.prepare_col_groups(cx);
340 this
341 }
342
343 pub fn delegate(&self) -> &D {
345 &self.delegate
346 }
347
348 pub fn delegate_mut(&mut self) -> &mut D {
350 &mut self.delegate
351 }
352
353 pub fn loop_selection(mut self, loop_selection: bool) -> Self {
355 self.loop_selection = loop_selection;
356 self
357 }
358
359 pub fn col_movable(mut self, col_movable: bool) -> Self {
361 self.col_movable = col_movable;
362 self
363 }
364
365 pub fn col_resizable(mut self, col_resizable: bool) -> Self {
367 self.col_resizable = col_resizable;
368 self
369 }
370
371 pub fn sortable(mut self, sortable: bool) -> Self {
373 self.sortable = sortable;
374 self
375 }
376
377 pub fn row_selectable(mut self, row_selectable: bool) -> Self {
379 self.row_selectable = row_selectable;
380 self
381 }
382
383 pub fn select_on_modifier_click(mut self, select_on_modifier_click: bool) -> Self {
386 self.select_on_modifier_click = select_on_modifier_click;
387 self
388 }
389
390 pub fn col_selectable(mut self, col_selectable: bool) -> Self {
392 self.col_selectable = col_selectable;
393 self
394 }
395
396 pub fn cell_selectable(mut self, cell_selectable: bool) -> Self {
414 self.cell_selectable = cell_selectable;
415 self
416 }
417
418 pub fn row_header(mut self, row_header: bool) -> Self {
429 self.row_header = row_header;
430 self
431 }
432
433 pub fn refresh(&mut self, cx: &mut Context<Self>) {
435 self.prepare_col_groups(cx);
436 }
437
438 pub fn scroll_to_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
440 self.vertical_scroll_handle
441 .scroll_to_item(row_ix, ScrollStrategy::Top);
442 cx.notify();
443 }
444
445 pub fn scroll_to_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
447 let col_ix = col_ix.saturating_sub(self.fixed_left_cols_count());
448
449 match self.horizontal_offset_for_col(col_ix) {
457 Some(offset_x) => {
458 let mut offset = self.horizontal_scroll_handle.offset();
459 offset.x = offset_x;
460 self.horizontal_scroll_handle.set_offset(offset);
461 }
462 None => self
465 .horizontal_scroll_handle
466 .scroll_to_item(col_ix, ScrollStrategy::Top),
467 }
468
469 cx.notify();
470 }
471
472 fn horizontal_offset_for_col(&self, col_ix: usize) -> Option<Pixels> {
484 let viewport_width = self.horizontal_scroll_handle.bounds().size.width;
485 if viewport_width <= px(0.) {
486 return None;
487 }
488
489 let cols = self.col_groups.get(self.fixed_left_cols_count()..)?;
490 let col_left: Pixels = cols.get(..col_ix)?.iter().map(|col| col.width).sum();
491 let col_right = col_left + cols.get(col_ix)?.width;
492 let offset_x = self.horizontal_scroll_handle.offset().x;
493
494 Some(if col_left + offset_x < px(0.) {
495 -col_left
496 } else if col_right + offset_x > viewport_width {
497 viewport_width - col_right
498 } else {
499 offset_x
500 })
501 }
502
503 pub fn selection(&self) -> TableSelection {
508 if let Some(row_ix) = self.selected_row() {
509 TableSelection::Row(row_ix)
510 } else if let Some(col_ix) = self.selected_col() {
511 TableSelection::Column(col_ix)
512 } else if let Some((row_ix, col_ix)) = self.selected_cell() {
513 TableSelection::Cell(row_ix, col_ix)
514 } else {
515 TableSelection::None
516 }
517 }
518
519 pub fn selected_row(&self) -> Option<usize> {
524 self.selected_row.filter(|_| self.selection_mode.is_row())
525 }
526
527 pub fn set_selected_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
529 let is_down = match self.selected_row {
530 Some(selected_row) => row_ix > selected_row,
531 None => true,
532 };
533
534 self.selection_mode = SelectionMode::Row;
535 self.right_clicked_row = None;
536 self.selected_row = Some(row_ix);
537 if let Some(row_ix) = self.selected_row {
538 self.vertical_scroll_handle.scroll_to_item(
539 row_ix,
540 if is_down {
541 ScrollStrategy::Bottom
542 } else {
543 ScrollStrategy::Top
544 },
545 );
546 }
547 cx.emit(TableEvent::SelectRow(row_ix));
548 cx.emit(TableEvent::RightClickedRow(None));
549 cx.notify();
550 }
551
552 pub fn right_clicked_row(&self) -> Option<usize> {
554 self.right_clicked_row
555 }
556
557 pub fn right_clicked_cell(&self) -> Option<(usize, usize)> {
559 self.right_clicked_cell
560 }
561
562 pub fn set_right_clicked_row(&mut self, row: Option<usize>, cx: &mut Context<Self>) {
567 self.right_clicked_row = row;
568 cx.notify();
569 }
570
571 pub fn selected_col(&self) -> Option<usize> {
576 self.selected_col
577 .filter(|_| self.selection_mode.is_column())
578 }
579
580 pub fn set_selected_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
582 self.selection_mode = SelectionMode::Column;
583 self.selected_col = Some(col_ix);
584 if let Some(col_ix) = self.selected_col {
585 self.scroll_to_col(col_ix, cx);
586 }
587 cx.emit(TableEvent::SelectColumn(col_ix));
588 cx.notify();
589 }
590
591 pub fn selected_cell(&self) -> Option<(usize, usize)> {
603 self.selected_cell.filter(|_| self.selection_mode.is_cell())
604 }
605
606 pub fn set_selected_cell(&mut self, row_ix: usize, col_ix: usize, cx: &mut Context<Self>) {
622 self.selection_mode = SelectionMode::Cell;
623 self.selected_cell = Some((row_ix, col_ix));
624 self.right_clicked_cell = None;
625
626 self.vertical_scroll_handle
628 .scroll_to_item(row_ix, ScrollStrategy::Center);
629 self.scroll_to_col(col_ix, cx);
630
631 cx.emit(TableEvent::SelectCell(row_ix, col_ix));
632 cx.notify();
633 }
634
635 pub fn set_selection(&mut self, selection: TableSelection, cx: &mut Context<Self>) {
640 match selection {
641 TableSelection::None => self.clear_selection(cx),
642 TableSelection::Row(row_ix) => self.set_selected_row(row_ix, cx),
643 TableSelection::Column(col_ix) => self.set_selected_col(col_ix, cx),
644 TableSelection::Cell(row_ix, col_ix) => self.set_selected_cell(row_ix, col_ix, cx),
645 }
646 }
647
648 pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
650 self.selection_mode = SelectionMode::Row;
651 self.selected_row = None;
652 self.selected_col = None;
653 self.selected_cell = None;
654 cx.emit(TableEvent::ClearSelection);
655 cx.notify();
656 }
657
658 pub fn visible_range(&self) -> &TableVisibleRange {
662 &self.visible_range
663 }
664
665 pub fn headers(&self, cx: &App) -> Vec<String> {
670 let columns_count = self.delegate.columns_count(cx);
671 let mut headers = Vec::with_capacity(columns_count);
672 for col_ix in 0..columns_count {
673 let column = self.delegate.column(col_ix, cx);
674 headers.push(column.name.to_string());
675 }
676
677 headers
678 }
679
680 pub fn dump(&self, cx: &App) -> (Vec<String>, Vec<Vec<String>>) {
687 self.dump_range(0..self.delegate.rows_count(cx), cx)
688 }
689
690 pub fn dump_range(&self, range: Range<usize>, cx: &App) -> (Vec<String>, Vec<Vec<String>>) {
698 let columns_count = self.delegate.columns_count(cx);
699 let rows_count = self.delegate.rows_count(cx);
700 let start = range.start.min(rows_count);
701 let end = range.end.min(rows_count).max(start);
702
703 let mut rows = Vec::with_capacity(end - start);
704 for row_ix in start..end {
705 let mut row = Vec::with_capacity(columns_count);
706 for col_ix in 0..columns_count {
707 row.push(self.delegate.cell_text(row_ix, col_ix, cx));
708 }
709 rows.push(row);
710 }
711
712 (self.headers(cx), rows)
713 }
714
715 pub fn refresh_header_layout(&mut self, cx: &mut Context<Self>) {
719 self.update_header_layout(cx);
720 cx.notify();
721 }
722
723 fn prepare_col_groups(&mut self, cx: &mut Context<Self>) {
724 self.col_groups = (0..self.delegate.columns_count(cx))
725 .map(|col_ix| {
726 let column = self.delegate().column(col_ix, cx);
727 ColGroup {
728 width: column.width,
729 bounds: Bounds::default(),
730 column,
731 }
732 })
733 .collect();
734
735 self.update_header_layout(cx);
736 }
737
738 fn update_header_layout(&mut self, cx: &mut Context<Self>) {
739 let group_rows = self.delegate.group_headers(cx);
740
741 let mut layout = match group_rows.as_ref() {
742 Some(rows) => Vec::with_capacity(rows.len() + 1),
743 None => Vec::with_capacity(1),
744 };
745
746 if let Some(group_rows) = group_rows {
747 for row in group_rows {
748 let mut cell_row = Vec::with_capacity(row.len());
749 let mut current_leaf_ix = 0;
750 for group in row {
751 let mut width = px(0.);
752 let start_leaf_col_ix = current_leaf_ix;
753 for i in 0..group.span {
754 if current_leaf_ix + i < self.col_groups.len() {
755 width += self.col_groups[current_leaf_ix + i].width;
756 }
757 }
758 current_leaf_ix += group.span;
759 cell_row.push(HeaderCell {
760 label: group.label.clone(),
761 width,
762 col_span: group.span,
763 is_leaf: false,
764 leaf_col_ix: None,
765 start_leaf_col_ix,
766 });
767 }
768 layout.push(cell_row);
769 }
770 }
771
772 let mut leaf_row = Vec::with_capacity(self.col_groups.len());
773 for (ix, group) in self.col_groups.iter().enumerate() {
774 leaf_row.push(HeaderCell {
775 label: group.column.name.clone(),
776 width: group.width,
777 col_span: 1,
778 is_leaf: true,
779 leaf_col_ix: Some(ix),
780 start_leaf_col_ix: ix,
781 });
782 }
783 layout.push(leaf_row);
784
785 self.header_layout = layout;
786 }
787
788 fn fixed_left_cols_count(&self) -> usize {
789 if !self.col_fixed {
790 return 0;
791 }
792
793 self.col_groups
794 .iter()
795 .filter(|col| col.column.fixed == Some(ColumnFixed::Left))
796 .count()
797 }
798
799 fn header_row_height(&self) -> Pixels {
801 self.options
802 .header_row_height
803 .unwrap_or(self.options.size.table_row_height())
804 }
805
806 fn header_band_height(&self) -> Pixels {
809 self.header_row_height() * self.header_layout.len().max(1) as f32
810 }
811
812 fn page_item_count(&self) -> usize {
813 let row_height = self.options.size.table_row_height();
814 let body_height = (self.bounds.size.height - self.header_band_height()).max(px(0.));
815 let count = (body_height / row_height).floor() as usize;
816 count.max(1)
817 }
818
819 fn on_row_right_click(
820 &mut self,
821 _: &MouseDownEvent,
822 row_ix: Option<usize>,
823 _: &mut Window,
824 cx: &mut Context<Self>,
825 ) {
826 self.right_clicked_row = row_ix;
827 self.right_clicked_cell = None;
828 cx.emit(TableEvent::RightClickedRow(row_ix));
829 }
830
831 fn on_cell_right_click(
832 &mut self,
833 _: &MouseDownEvent,
834 row_ix: usize,
835 col_ix: usize,
836 _: &mut Window,
837 cx: &mut Context<Self>,
838 ) {
839 if !self.cell_selectable {
840 return;
841 }
842
843 cx.stop_propagation();
844 self.right_clicked_cell = Some((row_ix, col_ix));
845 self.right_clicked_row = None;
846 cx.emit(TableEvent::RightClickedCell(row_ix, col_ix));
847 }
848
849 fn on_row_left_click(
850 &mut self,
851 e: &ClickEvent,
852 row_ix: usize,
853 _: &mut Window,
854 cx: &mut Context<Self>,
855 ) {
856 if !self.row_selectable {
857 return;
858 }
859
860 cx.stop_propagation();
861 if self.selects_row_on_click(e) {
862 self.set_selected_row(row_ix, cx);
863 }
864
865 if e.click_count() == 2 {
866 cx.emit(TableEvent::DoubleClickedRow(row_ix));
867 }
868 }
869
870 fn selects_row_on_click(&self, e: &ClickEvent) -> bool {
872 let modifiers = e.modifiers();
873 self.select_on_modifier_click || !(modifiers.secondary() || modifiers.shift)
874 }
875
876 fn on_col_head_click(&mut self, col_ix: usize, _: &mut Window, cx: &mut Context<Self>) {
877 if !self.col_selectable {
878 return;
879 }
880
881 let Some(col_group) = self.col_groups.get(col_ix) else {
882 return;
883 };
884
885 if !col_group.column.selectable {
886 return;
887 }
888
889 self.set_selected_col(col_ix, cx)
890 }
891
892 fn on_cell_click(
893 &mut self,
894 e: &ClickEvent,
895 row_ix: usize,
896 col_ix: usize,
897 _: &mut Window,
898 cx: &mut Context<Self>,
899 ) {
900 if !self.cell_selectable {
901 return;
902 }
903
904 cx.stop_propagation();
905
906 let is_double_click = e.click_count() == 2;
907
908 let is_reselect = self.selected_cell() == Some((row_ix, col_ix));
914 let should_escalate_to_row = !self.row_header
915 && self.row_selectable
916 && is_reselect
917 && !is_double_click
918 && self.selects_row_on_click(e);
919 if should_escalate_to_row {
920 self.set_selected_row(row_ix, cx);
921 return;
922 }
923
924 self.set_selected_cell(row_ix, col_ix, cx);
925
926 if is_double_click {
927 cx.emit(TableEvent::DoubleClickedCell(row_ix, col_ix));
928 }
929 }
930
931 fn has_selection(&self) -> bool {
932 self.selection() != TableSelection::None
933 }
934
935 pub(super) fn action_cancel(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
936 if self.has_selection() {
937 self.clear_selection(cx);
938 return;
939 }
940 cx.propagate();
941 }
942
943 pub(super) fn action_select_prev(
944 &mut self,
945 _: &SelectUp,
946 _: &mut Window,
947 cx: &mut Context<Self>,
948 ) {
949 let rows_count = self.delegate.rows_count(cx);
950 if rows_count < 1 {
951 return;
952 }
953
954 if self.selection_mode.is_cell() {
956 if let Some((row_ix, col_ix)) = self.selected_cell {
957 let new_row = if row_ix > 0 {
958 row_ix.saturating_sub(1)
959 } else if self.loop_selection {
960 rows_count.saturating_sub(1)
961 } else {
962 row_ix
963 };
964 self.set_selected_cell(new_row, col_ix, cx);
965 } else {
966 self.set_selected_cell(0, 0, cx);
968 }
969 return;
970 }
971
972 if !self.row_selectable {
974 return;
975 }
976 let mut selected_row = self.selected_row.unwrap_or(0);
977 if selected_row > 0 {
978 selected_row = selected_row.saturating_sub(1);
979 } else {
980 if self.loop_selection {
981 selected_row = rows_count.saturating_sub(1);
982 }
983 }
984
985 self.set_selected_row(selected_row, cx);
986 }
987
988 pub(super) fn action_select_next(
989 &mut self,
990 _: &SelectDown,
991 _: &mut Window,
992 cx: &mut Context<Self>,
993 ) {
994 let rows_count = self.delegate.rows_count(cx);
995 if rows_count < 1 {
996 return;
997 }
998
999 if self.selection_mode.is_cell() {
1001 if let Some((row_ix, col_ix)) = self.selected_cell {
1002 let new_row = if row_ix < rows_count.saturating_sub(1) {
1003 row_ix + 1
1004 } else if self.loop_selection {
1005 0
1006 } else {
1007 row_ix
1008 };
1009 self.set_selected_cell(new_row, col_ix, cx);
1010 } else {
1011 self.set_selected_cell(0, 0, cx);
1013 }
1014 return;
1015 }
1016
1017 if !self.row_selectable {
1019 return;
1020 }
1021 let selected_row = match self.selected_row {
1022 Some(selected_row) if selected_row < rows_count.saturating_sub(1) => selected_row + 1,
1023 Some(selected_row) => {
1024 if self.loop_selection {
1025 0
1026 } else {
1027 selected_row
1028 }
1029 }
1030 _ => 0,
1031 };
1032
1033 self.set_selected_row(selected_row, cx);
1034 }
1035
1036 pub(super) fn action_select_first_column(
1037 &mut self,
1038 _: &SelectFirst,
1039 _: &mut Window,
1040 cx: &mut Context<Self>,
1041 ) {
1042 if self.selection_mode.is_cell() {
1044 if let Some((row_ix, _)) = self.selected_cell {
1045 self.set_selected_cell(row_ix, 0, cx);
1046 } else {
1047 self.set_selected_cell(0, 0, cx);
1049 }
1050 return;
1051 }
1052
1053 if self.selection_mode.is_row() && self.row_selectable {
1055 if self.delegate.rows_count(cx) > 0 {
1056 self.set_selected_row(0, cx);
1057 }
1058 return;
1059 }
1060
1061 self.set_selected_col(0, cx);
1063 }
1064
1065 pub(super) fn action_select_last_column(
1066 &mut self,
1067 _: &SelectLast,
1068 _: &mut Window,
1069 cx: &mut Context<Self>,
1070 ) {
1071 let columns_count = self.delegate.columns_count(cx);
1072
1073 if self.selection_mode.is_cell() {
1075 if let Some((row_ix, _)) = self.selected_cell {
1076 self.set_selected_cell(row_ix, columns_count.saturating_sub(1), cx);
1077 } else {
1078 self.set_selected_cell(0, columns_count.saturating_sub(1), cx);
1080 }
1081 return;
1082 }
1083
1084 if self.selection_mode.is_row() && self.row_selectable {
1086 let rows_count = self.delegate.rows_count(cx);
1087 if rows_count > 0 {
1088 self.set_selected_row(rows_count - 1, cx);
1089 }
1090 return;
1091 }
1092
1093 self.set_selected_col(columns_count.saturating_sub(1), cx);
1095 }
1096
1097 pub(super) fn action_select_page_up(
1098 &mut self,
1099 _: &SelectPageUp,
1100 _: &mut Window,
1101 cx: &mut Context<Self>,
1102 ) {
1103 let step = self.page_item_count();
1104
1105 if self.selection_mode.is_cell() {
1107 if let Some((row_ix, col_ix)) = self.selected_cell {
1108 let target = row_ix.saturating_sub(step);
1109 self.set_selected_cell(target, col_ix, cx);
1110 } else {
1111 self.set_selected_cell(0, 0, cx);
1113 }
1114 return;
1115 }
1116
1117 if !self.row_selectable {
1119 return;
1120 }
1121 let current = self.selected_row.unwrap_or(0);
1122 let target = current.saturating_sub(step);
1123 self.set_selected_row(target, cx);
1124 }
1125
1126 pub(super) fn action_select_page_down(
1127 &mut self,
1128 _: &SelectPageDown,
1129 _: &mut Window,
1130 cx: &mut Context<Self>,
1131 ) {
1132 let rows_count = self.delegate.rows_count(cx);
1133 if rows_count == 0 {
1134 return;
1135 }
1136
1137 let step = self.page_item_count();
1138
1139 if self.selection_mode.is_cell() {
1141 if let Some((row_ix, col_ix)) = self.selected_cell {
1142 let max_row = rows_count.saturating_sub(1);
1143 let target = (row_ix + step).min(max_row);
1144 self.set_selected_cell(target, col_ix, cx);
1145 } else {
1146 self.set_selected_cell(0, 0, cx);
1148 }
1149 return;
1150 }
1151
1152 if !self.row_selectable {
1154 return;
1155 }
1156 let current = self.selected_row.unwrap_or(0);
1157 let max_row = rows_count.saturating_sub(1);
1158 let target = (current + step).min(max_row);
1159 self.set_selected_row(target, cx);
1160 }
1161
1162 pub(super) fn action_select_prev_col(
1163 &mut self,
1164 _: &SelectPrevColumn,
1165 _: &mut Window,
1166 cx: &mut Context<Self>,
1167 ) {
1168 let columns_count = self.delegate.columns_count(cx);
1169
1170 if self.selection_mode.is_cell() {
1172 if let Some((row_ix, col_ix)) = self.selected_cell {
1173 let new_col = if col_ix > 0 {
1174 col_ix.saturating_sub(1)
1175 } else if self.loop_selection {
1176 columns_count.saturating_sub(1)
1177 } else {
1178 col_ix
1179 };
1180 self.set_selected_cell(row_ix, new_col, cx);
1181 } else {
1182 self.set_selected_cell(0, 0, cx);
1184 }
1185 return;
1186 }
1187
1188 let mut selected_col = self.selected_col.unwrap_or(0);
1190 if selected_col > 0 {
1191 selected_col = selected_col.saturating_sub(1);
1192 } else {
1193 if self.loop_selection {
1194 selected_col = columns_count.saturating_sub(1);
1195 }
1196 }
1197 self.set_selected_col(selected_col, cx);
1198 }
1199
1200 pub(super) fn action_select_next_col(
1201 &mut self,
1202 _: &SelectNextColumn,
1203 _: &mut Window,
1204 cx: &mut Context<Self>,
1205 ) {
1206 let columns_count = self.delegate.columns_count(cx);
1207
1208 if self.selection_mode.is_cell() {
1210 if let Some((row_ix, col_ix)) = self.selected_cell {
1211 let new_col = if col_ix < columns_count.saturating_sub(1) {
1212 col_ix + 1
1213 } else if self.loop_selection {
1214 0
1215 } else {
1216 col_ix
1217 };
1218 self.set_selected_cell(row_ix, new_col, cx);
1219 } else {
1220 self.set_selected_cell(0, 0, cx);
1222 }
1223 return;
1224 }
1225
1226 let mut selected_col = self.selected_col.unwrap_or(0);
1228 if selected_col < columns_count.saturating_sub(1) {
1229 selected_col += 1;
1230 } else {
1231 if self.loop_selection {
1232 selected_col = 0;
1233 }
1234 }
1235
1236 self.set_selected_col(selected_col, cx);
1237 }
1238
1239 fn scroll_table_by_col_resizing(
1241 &mut self,
1242 mouse_position: Point<Pixels>,
1243 col_group: &ColGroup,
1244 ) {
1245 if mouse_position.x > self.bounds.right() {
1247 return;
1248 }
1249
1250 let mut offset = self.horizontal_scroll_handle.offset();
1251 let col_bounds = col_group.bounds;
1252
1253 if mouse_position.x < self.bounds.left()
1254 && col_bounds.right() < self.bounds.left() + px(20.)
1255 {
1256 offset.x += px(1.);
1257 } else if mouse_position.x > self.bounds.right()
1258 && col_bounds.right() > self.bounds.right() - px(20.)
1259 {
1260 offset.x -= px(1.);
1261 }
1262
1263 self.horizontal_scroll_handle.set_offset(offset);
1264 }
1265
1266 pub fn col_width(&self, col_ix: usize) -> Option<Pixels> {
1273 self.col_groups.get(col_ix).map(|group| group.width)
1274 }
1275
1276 pub fn fit_col(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> bool {
1285 if !self.is_col_resizable(col_ix) {
1286 return false;
1287 }
1288 let Some(content) = self.delegate.fit_column(col_ix, window, cx) else {
1289 return false;
1290 };
1291 let padding = self
1292 .col_groups
1293 .get(col_ix)
1294 .and_then(|group| group.column.paddings)
1295 .unwrap_or_else(|| self.options.size.table_cell_padding());
1296 let scale_factor = window.scale_factor();
1300 let device_pixels = |length: Pixels| (f32::from(length) * scale_factor).ceil();
1301 let mut content_pixels = device_pixels(content);
1302 if px(content_pixels / scale_factor) < content {
1305 content_pixels += 1.;
1306 }
1307 let width = content_pixels + device_pixels(padding.left) + device_pixels(padding.right);
1308 if self.resize_cols(col_ix, px(width / scale_factor), window, cx) {
1309 let new_widths = self.col_groups.iter().map(|g| g.width).collect();
1310 cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
1311 }
1312 true
1313 }
1314
1315 fn resize_cols(
1320 &mut self,
1321 ix: usize,
1322 size: Pixels,
1323 _: &mut Window,
1324 cx: &mut Context<Self>,
1325 ) -> bool {
1326 if !self.col_resizable {
1327 return false;
1328 }
1329
1330 let mut changed = false;
1331 if let Some(col_group) = self.col_groups.get_mut(ix) {
1332 if col_group.is_resizable() {
1333 let new_width = size
1334 .max(col_group.column.min_width)
1335 .min(col_group.column.max_width);
1336 if col_group.width != new_width {
1337 col_group.width = new_width;
1338 changed = true;
1339 }
1340 }
1341 }
1342
1343 if changed {
1344 self.update_header_layout(cx);
1345 cx.notify();
1346 }
1347 changed
1348 }
1349
1350 fn perform_sort(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
1351 if !self.sortable {
1352 return;
1353 }
1354
1355 let sort = self.col_groups.get(col_ix).and_then(|g| g.column.sort);
1356 if sort.is_none() {
1357 return;
1358 }
1359
1360 let sort = sort.unwrap();
1361 let sort = match sort {
1362 ColumnSort::Ascending => ColumnSort::Default,
1363 ColumnSort::Descending => ColumnSort::Ascending,
1364 ColumnSort::Default => ColumnSort::Descending,
1365 };
1366
1367 for (ix, col_group) in self.col_groups.iter_mut().enumerate() {
1368 if ix == col_ix {
1369 col_group.column.sort = Some(sort);
1370 } else {
1371 if col_group.column.sort.is_some() {
1372 col_group.column.sort = Some(ColumnSort::Default);
1373 }
1374 }
1375 }
1376
1377 self.delegate_mut().perform_sort(col_ix, sort, window, cx);
1378
1379 cx.notify();
1380 }
1381
1382 fn move_column(
1383 &mut self,
1384 col_ix: usize,
1385 to_ix: usize,
1386 window: &mut Window,
1387 cx: &mut Context<Self>,
1388 ) {
1389 if col_ix == to_ix {
1390 return;
1391 }
1392
1393 self.delegate.move_column(col_ix, to_ix, window, cx);
1394 let col_group = self.col_groups.remove(col_ix);
1395 self.col_groups.insert(to_ix, col_group);
1396
1397 cx.emit(TableEvent::MoveColumn(col_ix, to_ix));
1398 cx.notify();
1399 }
1400
1401 fn drag_gap_at(&self, x: Pixels, drag_col_ix: usize) -> Option<usize> {
1405 let fixed_count = self.fixed_left_cols_count();
1406
1407 let drag_in_fixed = drag_col_ix < fixed_count;
1412 let pointer_in_fixed = fixed_count > 0 && x < self.fixed_head_cols_bounds.right();
1413 if drag_in_fixed != pointer_in_fixed {
1414 return None;
1415 }
1416
1417 let candidates = if pointer_in_fixed {
1421 0..fixed_count
1422 } else {
1423 self.calculate_visible_leaf_col_range(fixed_count).0
1424 };
1425
1426 let mut gap = candidates.start;
1428 for ix in candidates {
1429 if x < self.col_groups[ix].bounds.center().x {
1430 break;
1431 }
1432 gap = ix + 1;
1433 }
1434
1435 if gap == drag_col_ix || gap == drag_col_ix + 1 {
1438 None
1439 } else {
1440 Some(gap)
1441 }
1442 }
1443
1444 fn load_more_if_need(
1446 &mut self,
1447 rows_count: usize,
1448 visible_end: usize,
1449 window: &mut Window,
1450 cx: &mut Context<Self>,
1451 ) {
1452 let threshold = self.delegate.load_more_threshold();
1453 if visible_end >= rows_count.saturating_sub(threshold) {
1455 if !self.delegate.has_more(cx) {
1456 return;
1457 }
1458
1459 self._load_more_task = cx.spawn_in(window, async move |view, window| {
1460 _ = view.update_in(window, |view, window, cx| {
1461 view.delegate.load_more(window, cx);
1462 });
1463 });
1464 }
1465 }
1466
1467 fn update_visible_range_if_need(
1468 &mut self,
1469 visible_range: Range<usize>,
1470 items_count: usize,
1471 axis: Axis,
1472 window: &mut Window,
1473 cx: &mut Context<Self>,
1474 ) {
1475 if visible_range.len() <= 1 && items_count > 1 {
1478 return;
1479 }
1480
1481 let end = visible_range.end.min(items_count);
1483 let visible_range = visible_range.start.min(end)..end;
1484 if visible_range.is_empty() && items_count > 0 {
1486 return;
1487 }
1488
1489 if axis == Axis::Vertical {
1490 if self.visible_range.rows == visible_range {
1491 return;
1492 }
1493 self.delegate_mut()
1494 .visible_rows_changed(visible_range.clone(), window, cx);
1495 self.visible_range.rows = visible_range;
1496 } else {
1497 if self.visible_range.cols == visible_range {
1498 return;
1499 }
1500 self.delegate_mut()
1501 .visible_columns_changed(visible_range.clone(), window, cx);
1502 self.visible_range.cols = visible_range;
1503 }
1504 }
1505
1506 fn render_cell(
1507 &self,
1508 _row_ix: Option<usize>,
1509 col_ix: usize,
1510 _window: &mut Window,
1511 _cx: &mut Context<Self>,
1512 ) -> Div {
1513 let Some(col_group) = self.col_groups.get(col_ix) else {
1514 return div();
1515 };
1516
1517 let col_width = col_group.width;
1518 let col_padding = col_group.column.paddings;
1519
1520 div()
1521 .w(col_width)
1522 .h_full()
1523 .flex_shrink_0()
1524 .overflow_hidden()
1525 .whitespace_nowrap()
1526 .table_cell_size(self.options.size)
1527 .map(|this| match col_padding {
1528 Some(padding) => this
1529 .pl(padding.left)
1530 .pr(padding.right)
1531 .pt(padding.top)
1532 .pb(padding.bottom),
1533 None => this,
1534 })
1535 }
1536
1537 fn render_col_wrap(
1540 &self,
1541 _row_ix: Option<usize>,
1542 col_ix: usize,
1543 _: &mut Window,
1544 cx: &mut Context<Self>,
1545 ) -> Div {
1546 let el = h_flex().h_full();
1547 let selectable = self.col_selectable
1548 && self
1549 .col_groups
1550 .get(col_ix)
1551 .map(|col_group| col_group.column.selectable)
1552 .unwrap_or(false);
1553
1554 if selectable && self.selected_col() == Some(col_ix) {
1557 el.bg(cx.theme().tokens.table_active)
1558 } else {
1559 el
1560 }
1561 }
1562
1563 fn render_leading_resize_handle(
1570 &self,
1571 ix: usize,
1572 _: &mut Window,
1573 cx: &mut Context<Self>,
1574 ) -> impl IntoElement {
1575 let Some(prev) = ix.checked_sub(1).filter(|ix| self.is_col_resizable(*ix)) else {
1576 return div().into_any_element();
1577 };
1578
1579 self.resize_handle_band(prev, ("resizable-handle-leading", ix).into(), cx)
1580 .absolute()
1581 .top_0()
1582 .left_0()
1583 .h_full()
1584 .w(HANDLE_PADDING)
1585 .into_any_element()
1586 }
1587
1588 fn render_resize_handle(
1596 &self,
1597 ix: usize,
1598 _: &mut Window,
1599 cx: &mut Context<Self>,
1600 ) -> impl IntoElement {
1601 if !self.is_col_resizable(ix) {
1602 return div().into_any_element();
1603 }
1604
1605 let group_id = SharedString::from(format!("resizable-handle:{}", ix));
1606
1607 self.resize_handle_band(ix, ("resizable-handle", ix).into(), cx)
1608 .group(group_id.clone())
1609 .h_full()
1610 .w(HANDLE_PADDING)
1611 .ml(-HANDLE_PADDING)
1612 .justify_end()
1613 .items_center()
1614 .child(
1615 div()
1616 .h_full()
1617 .justify_center()
1618 .bg(cx.theme().table_row_border)
1619 .group_hover(&group_id, |this| this.bg(cx.theme().border).h_full())
1620 .w(px(1.)),
1621 )
1622 .into_any_element()
1623 }
1624
1625 fn is_col_resizable(&self, ix: usize) -> bool {
1626 self.col_resizable
1627 && self
1628 .col_groups
1629 .get(ix)
1630 .map(|col| col.is_resizable())
1631 .unwrap_or(false)
1632 }
1633
1634 fn resize_handle_band(
1644 &self,
1645 ix: usize,
1646 id: ElementId,
1647 cx: &mut Context<Self>,
1648 ) -> Stateful<Div> {
1649 h_flex()
1650 .id(id)
1651 .occlude()
1652 .cursor_col_resize()
1653 .on_drag_move(
1654 cx.listener(move |view, e: &DragMoveEvent<ResizeColumn>, window, cx| {
1655 match e.drag(cx) {
1656 ResizeColumn((entity_id, ix)) => {
1657 if cx.entity_id() != *entity_id {
1658 return;
1659 }
1660
1661 let ix = *ix;
1668 if view.fitted_col == Some(ix) {
1669 return;
1670 }
1671 view.resizing_col = Some(ix);
1672
1673 let col_group = view
1674 .col_groups
1675 .get(ix)
1676 .expect("BUG: invalid col index")
1677 .clone();
1678
1679 view.resize_cols(
1680 ix,
1681 e.event.position.x - HANDLE_SIZE - col_group.bounds.left(),
1682 window,
1683 cx,
1684 );
1685
1686 view.scroll_table_by_col_resizing(e.event.position, &col_group);
1688 }
1689 };
1690 }),
1691 )
1692 .on_drag(ResizeColumn((cx.entity_id(), ix)), |drag, _, _, cx| {
1693 cx.stop_propagation();
1694 cx.new(|_| drag.clone())
1695 })
1696 .on_mouse_down(
1697 MouseButton::Left,
1698 cx.listener(
1699 move |view, e: &MouseDownEvent, window, cx| match e.click_count {
1700 1 => view.fitted_col = None,
1701 2 if view.fit_col(ix, window, cx) => view.fitted_col = Some(ix),
1702 _ => {}
1703 },
1704 ),
1705 )
1706 .on_mouse_up_out(
1707 MouseButton::Left,
1708 cx.listener(|view, _, _, cx| {
1709 if view.resizing_col.is_none() {
1710 return;
1711 }
1712
1713 view.resizing_col = None;
1714
1715 let new_widths = view.col_groups.iter().map(|g| g.width).collect();
1716 cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
1717 cx.notify();
1718 }),
1719 )
1720 }
1721
1722 fn render_fixed_cols_border(&self, cx: &App) -> Option<Div> {
1725 self.options.fixed_cols_border.then(|| {
1726 div()
1727 .absolute()
1728 .top_0()
1729 .right_0()
1730 .bottom_0()
1731 .w_0()
1732 .flex_shrink_0()
1733 .border_r_1()
1734 .border_color(cx.theme().border)
1735 })
1736 }
1737
1738 fn render_row_header_cell(
1740 &self,
1741 row_ix: usize,
1742 is_head: bool,
1743 cx: &mut Context<Self>,
1744 ) -> impl IntoElement {
1745 div()
1746 .id(("row-header", row_ix))
1747 .w_3()
1748 .h_full()
1749 .border_r_1()
1750 .border_color(cx.theme().table_row_border)
1751 .bg(cx.theme().tokens.table_head)
1752 .flex_shrink_0()
1753 .table_cell_size(self.options.size)
1754 .when(!is_head, |this| {
1755 this.when(self.row_selectable, |this| {
1756 this.on_click(cx.listener(move |table, e: &ClickEvent, _window, cx| {
1757 cx.stop_propagation();
1758 if table.selects_row_on_click(e) {
1759 table.set_selected_row(row_ix, cx);
1760 }
1761 }))
1762 })
1763 })
1764 }
1765
1766 fn render_sort_icon(
1767 &self,
1768 col_ix: usize,
1769 col_group: &ColGroup,
1770 _: &mut Window,
1771 cx: &mut Context<Self>,
1772 ) -> Option<impl IntoElement> {
1773 if !self.sortable {
1774 return None;
1775 }
1776
1777 let Some(sort) = col_group.column.sort else {
1778 return None;
1779 };
1780
1781 let (icon, is_on) = match sort {
1782 ColumnSort::Ascending => (IconName::SortAscending, true),
1783 ColumnSort::Descending => (IconName::SortDescending, true),
1784 ColumnSort::Default => (IconName::ChevronsUpDown, false),
1785 };
1786
1787 Some(
1788 div()
1789 .id(("icon-sort", col_ix))
1790 .test_support()
1791 .role(gpui::Role::Button)
1792 .aria_label(t!("Table.SortBy", column = col_group.column.name).to_string())
1793 .p(px(2.))
1794 .rounded(cx.theme().radius / 2.)
1795 .map(|this| match is_on {
1796 true => this,
1797 false => this.opacity(0.5),
1798 })
1799 .hover(|this| this.bg(cx.theme().tokens.secondary).opacity(7.))
1800 .active(|this| this.bg(cx.theme().tokens.secondary_active).opacity(1.))
1801 .on_click(
1802 cx.listener(move |table, _, window, cx| table.perform_sort(col_ix, window, cx)),
1803 )
1804 .child(
1805 Icon::new(icon)
1806 .size_3()
1807 .text_color(cx.theme().secondary_foreground),
1808 ),
1809 )
1810 }
1811
1812 fn render_th(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> Div {
1817 let entity_id = cx.entity_id();
1818 let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index");
1819
1820 let movable = self.col_movable && col_group.column.movable;
1821 let paddings = col_group.column.paddings;
1822 let name = col_group.column.name.clone();
1823
1824 h_flex()
1825 .h_full()
1826 .child(
1827 self.render_cell(None, col_ix, window, cx)
1828 .id(("col-header", col_ix))
1829 .test_support()
1830 .on_click(cx.listener(move |this, _, window, cx| {
1831 this.on_col_head_click(col_ix, window, cx);
1832 }))
1833 .child(
1834 h_flex()
1835 .size_full()
1836 .justify_between()
1837 .items_center()
1838 .child(self.delegate.render_th(col_ix, window, cx))
1839 .when_some(paddings, |this, paddings| {
1840 let offset_pr =
1842 self.options.size.table_cell_padding().right - paddings.right;
1843 this.pr(offset_pr.max(px(0.)))
1844 })
1845 .children(self.render_sort_icon(col_ix, &col_group, window, cx)),
1846 )
1847 .when(movable, |this| {
1848 this.on_drag(
1849 DragColumn {
1850 entity_id,
1851 col_ix,
1852 name,
1853 width: col_group.width,
1854 },
1855 |drag, _, _, cx| {
1856 cx.stop_propagation();
1857 cx.new(|_| drag.clone())
1858 },
1859 )
1860 })
1861 .map(|this| {
1862 let last_gap = col_ix + 1 == self.col_groups.len();
1867 match self.col_drag_gap {
1868 Some(gap)
1869 if cx.has_active_drag()
1870 && (gap == col_ix || (last_gap && gap == col_ix + 1)) =>
1871 {
1872 let right_side = gap == col_ix + 1;
1873 this.relative().child(
1874 div()
1875 .absolute()
1876 .top_0()
1877 .bottom_0()
1878 .w(px(2.))
1879 .map(|d| if right_side { d.right_0() } else { d.left_0() })
1880 .bg(cx.theme().drag_border),
1881 )
1882 }
1883 _ => this,
1884 }
1885 }),
1886 )
1887 .child(self.render_resize_handle(col_ix, window, cx))
1889 .child(self.render_leading_resize_handle(col_ix, window, cx))
1891 .on_prepaint({
1893 let view = cx.entity().clone();
1894 move |bounds, _, cx| view.update(cx, |r, _| r.col_groups[col_ix].bounds = bounds)
1895 })
1896 }
1897
1898 fn calculate_visible_leaf_col_range(
1908 &self,
1909 left_columns_count: usize,
1910 ) -> (Range<usize>, Pixels) {
1911 let total_cols = self.col_groups.len();
1912
1913 if self.bounds.size.width == px(0.) {
1914 return (left_columns_count..total_cols, px(0.));
1915 }
1916
1917 let fixed_width = self.fixed_head_cols_bounds.size.width;
1918 let available_width = (self.bounds.size.width - fixed_width).max(px(0.));
1919 let scroll_x = (-self.horizontal_scroll_handle.offset().x).max(px(0.));
1922
1923 let mut range_start = left_columns_count;
1927 let mut left_spacer = px(0.);
1928 let mut cumulative = px(0.);
1929 for i in left_columns_count..total_cols {
1930 let right_edge = cumulative + self.col_groups[i].width;
1931 if right_edge > scroll_x {
1932 range_start = i;
1933 left_spacer = cumulative;
1934 break;
1935 }
1936 cumulative = right_edge;
1937 }
1938
1939 let right_bound = scroll_x + available_width + px(200.);
1943 let mut range_end = total_cols;
1944 let mut cumulative = left_spacer; for i in range_start..total_cols {
1946 cumulative += self.col_groups[i].width;
1947 if cumulative > right_bound {
1948 range_end = (i + 1).min(total_cols);
1949 break;
1950 }
1951 }
1952
1953 (range_start..range_end, left_spacer)
1954 }
1955
1956 fn render_table_header(
1957 &mut self,
1958 left_columns_count: usize,
1959 window: &mut Window,
1960 cx: &mut Context<Self>,
1961 ) -> impl IntoElement {
1962 let view = cx.entity().clone();
1963 let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
1964
1965 let total_cols = self.col_groups.len();
1979 let (visible_col_range, left_spacer) =
1980 self.calculate_visible_leaf_col_range(left_columns_count);
1981
1982 let layout_len = self.header_layout.len();
1983 let header_row_height = self.header_row_height();
1984
1985 if left_columns_count == 0 {
1987 self.fixed_head_cols_bounds = Bounds::default();
1988 }
1989
1990 let mut header = self.delegate_mut().render_header(window, cx);
1991 let style = header.style().clone();
1992 let layout = self.header_layout.clone();
1993
1994 header
1995 .h_flex()
1996 .w_full()
1997 .flex_shrink_0()
1998 .bg(cx.theme().tokens.table_head)
1999 .text_color(cx.theme().table_head_foreground)
2000 .refine_style(&style)
2001 .on_drag_move(cx.listener(|table, e: &DragMoveEvent<DragColumn>, _, cx| {
2002 let drag = e.drag(cx);
2003 let (drag_entity_id, drag_col_ix) = (drag.entity_id, drag.col_ix);
2004
2005 let gap =
2006 if drag_entity_id == cx.entity_id() && e.bounds.contains(&e.event.position) {
2007 table.drag_gap_at(e.event.position.x, drag_col_ix)
2008 } else {
2009 None
2010 };
2011
2012 if table.col_drag_gap != gap {
2013 table.col_drag_gap = gap;
2014 cx.notify();
2015 }
2016 }))
2017 .on_drop(cx.listener(|table, drag: &DragColumn, window, cx| {
2018 if drag.entity_id != cx.entity_id() {
2019 return;
2020 }
2021
2022 let Some(gap) = table.col_drag_gap.take() else {
2024 return;
2025 };
2026 let to_ix = if drag.col_ix < gap { gap - 1 } else { gap };
2027 table.move_column(drag.col_ix, to_ix, window, cx);
2028 }))
2029 .when(self.cell_selectable && self.row_header, |this| {
2030 this.child(self.render_row_header_cell(0, true, cx))
2031 })
2032 .when(left_columns_count > 0, |this| {
2033 let view = view.clone();
2034 this.child(
2036 h_flex()
2037 .relative()
2038 .h_full()
2039 .bg(cx.theme().tokens.table_head)
2040 .child(v_flex().min_w_full().flex_shrink_0().children(
2041 layout.iter().enumerate().map(|(_row_ix, row_cells)| {
2042 h_flex()
2043 .min_w_full()
2044 .h(header_row_height)
2045 .border_b_1()
2046 .border_color(cx.theme().border)
2047 .children(row_cells.iter().filter_map(|cell| {
2048 if cell.start_leaf_col_ix < left_columns_count {
2049 if cell.is_leaf {
2050 if let Some(ix) = cell.leaf_col_ix {
2051 return Some(
2052 self.render_th(ix, window, cx)
2053 .into_any_element(),
2054 );
2055 }
2056 } else {
2057 return Some(
2058 self.delegate_mut()
2059 .render_group_th(
2060 &cell.label,
2061 cell.col_span,
2062 cell.width,
2063 window,
2064 cx,
2065 )
2066 .into_any_element(),
2067 );
2068 }
2069 }
2070 None
2071 }))
2072 }),
2073 ))
2074 .children(self.render_fixed_cols_border(cx))
2075 .on_prepaint(move |bounds, _, cx| {
2076 view.update(cx, |r, _| r.fixed_head_cols_bounds = bounds)
2077 }),
2078 )
2079 })
2080 .child(
2081 h_flex()
2083 .id("table-head")
2084 .size_full()
2085 .overflow_scroll()
2086 .relative()
2087 .track_scroll(&horizontal_scroll_handle)
2088 .bg(cx.theme().tokens.table_head)
2089 .child(v_flex().min_w_full().flex_shrink_0().children(
2090 layout.iter().enumerate().map(|(row_ix, row_cells)| {
2091 let is_leaf_row = row_ix + 1 == layout_len;
2092 h_flex()
2093 .min_w_full()
2094 .h(header_row_height)
2095 .border_b_1()
2096 .border_color(cx.theme().border)
2097 .map(|this| {
2098 if is_leaf_row {
2099 this.when(left_spacer > px(0.), |r| {
2104 r.child(div().w(left_spacer).h_full().flex_shrink_0())
2105 })
2106 .children(row_cells.iter().filter_map(|cell| {
2107 if cell.is_leaf {
2108 let ix = cell.leaf_col_ix?;
2109 if !visible_col_range.contains(&ix) {
2110 return None;
2111 }
2112 Some(
2113 self.render_th(ix, window, cx)
2114 .into_any_element(),
2115 )
2116 } else {
2117 None
2118 }
2119 }))
2120 .when(visible_col_range.end < total_cols, |r| {
2121 let right_spacer: Pixels = self.col_groups
2122 [visible_col_range.end..total_cols]
2123 .iter()
2124 .map(|g| g.width)
2125 .sum();
2126 r.child(div().w(right_spacer).h_full().flex_shrink_0())
2127 })
2128 .child(self.delegate.render_last_empty_col(window, cx))
2129 } else {
2130 this.children(row_cells.iter().filter_map(|cell| {
2133 if cell.start_leaf_col_ix >= left_columns_count {
2134 if cell.is_leaf {
2135 if let Some(ix) = cell.leaf_col_ix {
2136 return Some(
2137 self.render_th(ix, window, cx)
2138 .into_any_element(),
2139 );
2140 }
2141 } else {
2142 return Some(
2143 self.delegate_mut()
2144 .render_group_th(
2145 &cell.label,
2146 cell.col_span,
2147 cell.width,
2148 window,
2149 cx,
2150 )
2151 .into_any_element(),
2152 );
2153 }
2154 }
2155 None
2156 }))
2157 .child(self.delegate.render_last_empty_col(window, cx))
2158 }
2159 })
2160 }),
2161 )),
2162 )
2163 }
2164
2165 #[allow(clippy::too_many_arguments)]
2166 fn render_table_row(
2167 &mut self,
2168 row_ix: usize,
2169 rows_count: usize,
2170 left_columns_count: usize,
2171 col_sizes: Rc<Vec<gpui::Size<Pixels>>>,
2172 columns_count: usize,
2173 is_filled: bool,
2174 window: &mut Window,
2175 cx: &mut Context<Self>,
2176 ) -> gpui::AnyElement {
2177 let is_stripe_row = self.options.stripe && row_ix % 2 != 0;
2178 let is_selected = self.selected_row() == Some(row_ix);
2181 let view = cx.entity().clone();
2182 let row_height = self.options.size.table_row_height();
2183
2184 if row_ix < rows_count {
2185 let is_last_row = row_ix + 1 == rows_count;
2186 let need_render_border = is_selected || !is_last_row || !is_filled;
2187
2188 let mut tr = self.delegate.render_tr(row_ix, window, cx);
2189 let style = tr.style().clone();
2190
2191 tr.test_support()
2192 .role(gpui::Role::Row)
2193 .aria_selected(is_selected)
2194 .h_flex()
2195 .w_full()
2196 .h(row_height)
2197 .when(need_render_border, |this| {
2198 this.border_b_1().border_color(cx.theme().table_row_border)
2199 })
2200 .when(is_stripe_row, |this| this.bg(cx.theme().tokens.table_even))
2201 .refine_style(&style)
2202 .hover(|this| {
2203 if is_selected || self.right_clicked_row == Some(row_ix) {
2204 this
2205 } else {
2206 this.bg(cx.theme().tokens.table_hover)
2207 }
2208 })
2209 .when(self.cell_selectable && self.row_header, |this| {
2210 this.child(self.render_row_header_cell(row_ix, false, cx))
2211 })
2212 .when(left_columns_count > 0, |this| {
2213 this.child(
2215 h_flex()
2216 .relative()
2217 .h_full()
2218 .children({
2219 let mut items = Vec::with_capacity(left_columns_count);
2220
2221 (0..left_columns_count).for_each(|col_ix| {
2222 let is_cell_selected =
2223 self.selected_cell() == Some((row_ix, col_ix));
2224 let is_cell_right_clicked =
2225 self.right_clicked_cell == Some((row_ix, col_ix));
2226
2227 items.push(
2228 self.render_col_wrap(Some(row_ix), col_ix, window, cx)
2229 .child(
2230 self.render_cell(Some(row_ix), col_ix, window, cx)
2231 .id(format!("table-cell:{}:{}", row_ix, col_ix))
2232 .relative()
2233 .child(self.measure_render_td(
2234 row_ix, col_ix, window, cx,
2235 ))
2236 .when(is_cell_selected, |this| {
2237 this.child(
2238 div()
2239 .absolute()
2240 .inset_0()
2241 .bg(cx.theme().tokens.table_active),
2242 )
2243 })
2244 .when(
2245 is_cell_right_clicked && !is_cell_selected,
2246 |this| {
2247 this.child(
2248 div()
2249 .absolute()
2250 .inset_0()
2251 .border_1()
2252 .border_color(
2253 cx.theme()
2254 .table_active_border
2255 .opacity(0.5),
2256 ),
2257 )
2258 },
2259 )
2260 .when(self.cell_selectable, |this| {
2261 this.on_click(cx.listener(
2262 move |table, e, window, cx| {
2263 table.on_cell_click(
2264 e, row_ix, col_ix, window, cx,
2265 );
2266 },
2267 ))
2268 .on_mouse_down(
2269 MouseButton::Right,
2270 cx.listener(
2271 move |table, e, window, cx| {
2272 table.on_cell_right_click(
2273 e, row_ix, col_ix, window,
2274 cx,
2275 );
2276 },
2277 ),
2278 )
2279 }),
2280 ),
2281 );
2282 });
2283
2284 items
2285 })
2286 .children(self.render_fixed_cols_border(cx)),
2287 )
2288 })
2289 .child(
2290 h_flex()
2291 .flex_1()
2292 .h_full()
2293 .overflow_hidden()
2294 .relative()
2295 .child(
2296 crate::virtual_list::virtual_list(
2297 view,
2298 row_ix,
2299 Axis::Horizontal,
2300 col_sizes,
2301 {
2302 move |table, visible_range: Range<usize>, window, cx| {
2303 table.update_visible_range_if_need(
2304 visible_range.clone(),
2305 columns_count.saturating_sub(left_columns_count),
2306 Axis::Horizontal,
2307 window,
2308 cx,
2309 );
2310
2311 let mut items = Vec::with_capacity(
2312 visible_range.end - visible_range.start,
2313 );
2314
2315 visible_range.for_each(|col_ix| {
2316 let col_ix = col_ix + left_columns_count;
2317 let is_cell_selected =
2318 table.selected_cell() == Some((row_ix, col_ix));
2319 let is_cell_right_clicked =
2320 table.right_clicked_cell == Some((row_ix, col_ix));
2321
2322 let el = table
2323 .render_col_wrap(Some(row_ix), col_ix, window, cx)
2324 .child(
2325 table
2326 .render_cell(
2327 Some(row_ix),
2328 col_ix,
2329 window,
2330 cx,
2331 )
2332 .id(format!(
2333 "table-cell-{}:{}",
2334 row_ix, col_ix
2335 ))
2336 .relative()
2337 .child(table.measure_render_td(
2338 row_ix, col_ix, window, cx,
2339 ))
2340 .when(is_cell_selected, |this| {
2341 this.child(
2342 div().absolute().inset_0().bg(cx
2343 .theme()
2344 .tokens
2345 .table_active),
2346 )
2347 })
2348 .when(
2349 is_cell_right_clicked
2350 && !is_cell_selected,
2351 |this| {
2352 this.child(
2353 div()
2354 .absolute()
2355 .inset_0()
2356 .border_1()
2357 .border_color(
2358 cx.theme()
2359 .table_active_border
2360 .opacity(0.5),
2361 ),
2362 )
2363 },
2364 )
2365 .when(table.cell_selectable, |this| {
2366 this.on_click(cx.listener(
2367 move |table, e, window, cx| {
2368 cx.stop_propagation();
2369 table.on_cell_click(
2370 e, row_ix, col_ix, window,
2371 cx,
2372 );
2373 },
2374 ))
2375 .on_mouse_down(
2376 MouseButton::Right,
2377 cx.listener(
2378 move |table, e, window, cx| {
2379 table.on_cell_right_click(
2380 e, row_ix, col_ix,
2381 window, cx,
2382 );
2383 },
2384 ),
2385 )
2386 }),
2387 );
2388
2389 items.push(el);
2390 });
2391
2392 items
2393 }
2394 },
2395 )
2396 .with_scroll_handle(&self.horizontal_scroll_handle),
2397 )
2398 .child(self.delegate.render_last_empty_col(window, cx)),
2399 )
2400 .when(is_selected, |this| {
2402 let bg = if cx.theme().list.active_highlight {
2403 cx.theme().tokens.table_active
2404 } else {
2405 cx.theme().tokens.accent
2406 };
2407 this.bg(bg)
2408 })
2409 .when(self.right_clicked_row == Some(row_ix), |this| {
2411 this.border_color(gpui::transparent_white()).child(
2412 div()
2413 .top(if row_ix == 0 { px(0.) } else { px(-1.) })
2414 .left(px(0.))
2415 .right(px(0.))
2416 .bottom(px(-1.))
2417 .absolute()
2418 .border_1()
2419 .border_color(cx.theme().selection),
2420 )
2421 })
2422 .on_mouse_down(
2423 MouseButton::Right,
2424 cx.listener(move |this, e, window, cx| {
2425 this.on_row_right_click(e, Some(row_ix), window, cx);
2426 }),
2427 )
2428 .on_click(cx.listener(move |this, e, window, cx| {
2429 this.on_row_left_click(e, row_ix, window, cx);
2430 }))
2431 .into_any_element()
2432 } else {
2433 let cols_width: Pixels = self
2434 .col_groups
2435 .iter()
2436 .take(columns_count)
2437 .map(|col_group| col_group.width)
2438 .sum();
2439
2440 self.delegate
2442 .render_tr(row_ix, window, cx)
2443 .h_flex()
2444 .w_full()
2445 .h(row_height)
2446 .border_b_1()
2447 .border_color(cx.theme().table_row_border)
2448 .when(is_stripe_row, |this| this.bg(cx.theme().tokens.table_even))
2449 .when(self.cell_selectable && self.row_header, |this| {
2450 this.child(
2452 div()
2453 .w(px(40.))
2454 .h_full()
2455 .flex_shrink_0()
2456 .table_cell_size(self.options.size),
2457 )
2458 })
2459 .child(div().flex_shrink_0().h_full().w(cols_width))
2463 .child(self.delegate.render_last_empty_col(window, cx))
2464 .into_any_element()
2465 }
2466 }
2467
2468 fn calculate_extra_rows_needed(
2470 &self,
2471 total_height: Pixels,
2472 actual_height: Pixels,
2473 row_height: Pixels,
2474 ) -> usize {
2475 let mut extra_rows_needed = 0;
2476
2477 let remaining_height = total_height - actual_height;
2478 if remaining_height > px(0.) {
2479 extra_rows_needed = (remaining_height / row_height).floor() as usize;
2480 }
2481
2482 extra_rows_needed
2483 }
2484
2485 #[inline]
2486 fn measure_render_td(
2487 &mut self,
2488 row_ix: usize,
2489 col_ix: usize,
2490 window: &mut Window,
2491 cx: &mut Context<Self>,
2492 ) -> impl IntoElement {
2493 if !crate::measure_enable() {
2494 return self
2495 .delegate
2496 .render_td(row_ix, col_ix, window, cx)
2497 .into_any_element();
2498 }
2499
2500 let start = std::time::Instant::now();
2501 let el = self.delegate.render_td(row_ix, col_ix, window, cx);
2502 self._measure.push(start.elapsed());
2503 el.into_any_element()
2504 }
2505
2506 fn measure(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
2507 if !crate::measure_enable() {
2508 return;
2509 }
2510
2511 if self._measure.len() > 0 {
2513 let total = self
2514 ._measure
2515 .iter()
2516 .fold(Duration::default(), |acc, d| acc + *d);
2517 let avg = total / self._measure.len() as u32;
2518 eprintln!(
2519 "last render {} cells total: {:?}, avg: {:?}",
2520 self._measure.len(),
2521 total,
2522 avg,
2523 );
2524 }
2525 self._measure.clear();
2526 }
2527
2528 fn render_vertical_scrollbar(
2529 &mut self,
2530 _: &mut Window,
2531 _: &mut Context<Self>,
2532 ) -> Option<impl IntoElement> {
2533 Some(
2534 div()
2535 .absolute()
2536 .top(self.header_band_height())
2537 .right_0()
2538 .bottom_0()
2539 .w(Scrollbar::width())
2540 .child(
2541 Scrollbar::vertical(&self.vertical_scroll_handle)
2542 .viewport_from_layout()
2543 .max_fps(60),
2544 ),
2545 )
2546 }
2547
2548 fn render_horizontal_scrollbar(
2549 &mut self,
2550 _: &mut Window,
2551 _: &mut Context<Self>,
2552 ) -> impl IntoElement {
2553 div()
2554 .absolute()
2555 .left(self.fixed_head_cols_bounds.size.width)
2556 .right_0()
2557 .bottom_0()
2558 .h(Scrollbar::width())
2559 .child(Scrollbar::horizontal(&self.horizontal_scroll_handle).viewport_from_layout())
2560 }
2561}
2562
2563impl<D> Focusable for TableState<D>
2564where
2565 D: TableDelegate,
2566{
2567 fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
2568 self.focus_handle.clone()
2569 }
2570}
2571impl<D> EventEmitter<TableEvent> for TableState<D> where D: TableDelegate {}
2572
2573impl<D> Render for TableState<D>
2574where
2575 D: TableDelegate,
2576{
2577 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2578 self.measure(window, cx);
2579
2580 let columns_count = self.delegate.columns_count(cx);
2581 let left_columns_count = self
2582 .col_groups
2583 .iter()
2584 .filter(|col| self.col_fixed && col.column.fixed == Some(ColumnFixed::Left))
2585 .count();
2586 let rows_count = self.delegate.rows_count(cx);
2587 let loading = self.delegate.loading(cx);
2588
2589 let row_height = self.options.size.table_row_height();
2590 let total_height = self
2591 .vertical_scroll_handle
2592 .0
2593 .borrow()
2594 .base_handle
2595 .bounds()
2596 .size
2597 .height;
2598 let actual_height = row_height * rows_count as f32;
2599 let extra_rows_count =
2600 self.calculate_extra_rows_needed(total_height, actual_height, row_height);
2601 let render_rows_count = if self.options.stripe {
2602 rows_count + extra_rows_count
2603 } else {
2604 rows_count
2605 };
2606 let has_right_click = self.right_clicked_row.is_some() || self.right_clicked_cell.is_some();
2607 let is_filled = total_height > Pixels::ZERO && total_height <= actual_height;
2608
2609 let loading_view = if loading {
2610 Some(
2611 self.delegate
2612 .render_loading(self.options.size, window, cx)
2613 .into_any_element(),
2614 )
2615 } else {
2616 None
2617 };
2618
2619 let empty_view = if rows_count == 0 {
2620 self.update_visible_range_if_need(0..0, 0, Axis::Vertical, window, cx);
2622 Some(
2623 div()
2624 .size_full()
2625 .child(self.delegate.render_empty(window, cx))
2626 .into_any_element(),
2627 )
2628 } else {
2629 None
2630 };
2631
2632 let inner_table = v_flex()
2633 .id("table-inner")
2634 .size_full()
2635 .overflow_hidden()
2636 .child(self.render_table_header(left_columns_count, window, cx))
2637 .context_menu({
2638 let view = cx.entity().clone();
2639 move |this, window: &mut Window, cx: &mut Context<PopupMenu>| {
2640 if let Some(row_ix) = view.read(cx).right_clicked_row {
2641 view.update(cx, |menu, cx| {
2642 menu.delegate_mut().context_menu(row_ix, this, window, cx)
2643 })
2644 } else {
2645 this
2646 }
2647 }
2648 })
2649 .map(|this| {
2650 if rows_count == 0 {
2651 this.children(empty_view)
2652 } else {
2653 this.child(
2654 h_flex().id("table-body").flex_grow_1().size_full().child(
2655 uniform_list(
2656 "table-uniform-list",
2657 render_rows_count,
2658 cx.processor(
2659 move |table, visible_range: Range<usize>, window, cx| {
2660 let col_sizes: Rc<Vec<gpui::Size<Pixels>>> = Rc::new(
2664 table
2665 .col_groups
2666 .iter()
2667 .skip(left_columns_count)
2668 .map(|col| gpui::Size {
2669 width: col.width,
2670 height: px(0.),
2671 })
2672 .collect(),
2673 );
2674
2675 table.load_more_if_need(
2676 rows_count,
2677 visible_range.end,
2678 window,
2679 cx,
2680 );
2681 table.update_visible_range_if_need(
2682 visible_range.clone(),
2683 rows_count,
2684 Axis::Vertical,
2685 window,
2686 cx,
2687 );
2688
2689 if visible_range.end > rows_count {
2690 table.scroll_to_row(
2691 std::cmp::min(
2692 visible_range.start,
2693 rows_count.saturating_sub(1),
2694 ),
2695 cx,
2696 );
2697 }
2698
2699 let mut items = Vec::with_capacity(
2700 visible_range.end.saturating_sub(visible_range.start),
2701 );
2702
2703 visible_range.for_each(|row_ix| {
2705 items.push(table.render_table_row(
2707 row_ix,
2708 rows_count,
2709 left_columns_count,
2710 col_sizes.clone(),
2711 columns_count,
2712 is_filled,
2713 window,
2714 cx,
2715 ));
2716 });
2717
2718 items
2719 },
2720 ),
2721 )
2722 .flex_grow_1()
2723 .size_full()
2724 .with_sizing_behavior(ListSizingBehavior::Auto)
2725 .track_scroll(&self.vertical_scroll_handle)
2726 .map(|rows| {
2727 gpui_base::ScrollBounce::new(
2728 "table-scroll-bounce",
2729 &self.vertical_scroll_handle,
2730 rows,
2731 )
2732 .when_some(self.options.scroll_bounce, |bounce, on| {
2733 bounce.enabled(on)
2734 })
2735 })
2736 .into_any_element(),
2737 ),
2738 )
2739 }
2740 });
2741
2742 div()
2743 .size_full()
2744 .children(loading_view)
2745 .when(!loading, |this| {
2746 this.child(inner_table)
2747 .child(ScrollableMask::new(
2748 Axis::Horizontal,
2749 &self.horizontal_scroll_handle,
2750 ))
2751 .when(rows_count > 0, |this| {
2756 this.child(ScrollableMask::new(
2757 Axis::Vertical,
2758 &self.vertical_scroll_handle.0.borrow().base_handle,
2759 ))
2760 })
2761 .when(has_right_click, |this| {
2762 this.on_mouse_down_out(cx.listener(|this, e, window, cx| {
2763 this.on_row_right_click(e, None, window, cx);
2764 cx.notify();
2765 }))
2766 })
2767 })
2768 .on_prepaint({
2769 let state = cx.entity();
2770 move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
2771 })
2772 .when(!window.is_inspector_picking(cx), |this| {
2773 this.child(
2774 div()
2775 .absolute()
2776 .top_0()
2777 .size_full()
2778 .when(self.options.scrollbar_visible.bottom, |this| {
2779 this.child(self.render_horizontal_scrollbar(window, cx))
2780 })
2781 .when(
2782 self.options.scrollbar_visible.right && rows_count > 0,
2783 |this| this.children(self.render_vertical_scrollbar(window, cx)),
2784 ),
2785 )
2786 })
2787 }
2788}
2789
2790#[cfg(test)]
2791mod tests {
2792 use gpui::{App, Entity, TestAppContext};
2793
2794 use super::*;
2795
2796 struct WidthsDelegate;
2797
2798 struct BounceDelegate;
2799
2800 impl TableDelegate for BounceDelegate {
2801 fn columns_count(&self, _: &App) -> usize {
2802 1
2803 }
2804 fn rows_count(&self, _: &App) -> usize {
2805 40
2806 }
2807 fn column(&self, _: usize, _: &App) -> Column {
2808 Column::new("value", "Value")
2809 }
2810 fn render_td(
2811 &mut self,
2812 row: usize,
2813 _: usize,
2814 _: &mut Window,
2815 _: &mut Context<TableState<Self>>,
2816 ) -> impl IntoElement {
2817 div().child(row.to_string())
2818 }
2819 }
2820
2821 struct BounceTable(Entity<TableState<BounceDelegate>>);
2822
2823 impl Render for BounceTable {
2824 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2825 div()
2826 .w(px(240.))
2827 .h(px(200.))
2828 .child(DataTable::new(&self.0).scroll_bounce(true))
2829 }
2830 }
2831
2832 #[gpui::test]
2833 fn scroll_bounce_table_keeps_headers_fixed_and_accepts_new_wheel_input(
2834 cx: &mut TestAppContext,
2835 ) {
2836 cx.update(crate::init);
2837 let (view, cx) = cx.add_window_view(|window, cx| {
2838 BounceTable(cx.new(|cx| TableState::new(BounceDelegate, window, cx)))
2839 });
2840 cx.update(|window, cx| window.draw(cx).clear(cx));
2841 let state = view.read_with(cx, |view, _| view.0.clone());
2842 let header = state.read_with(cx, |state, _| state.col_groups[0].bounds);
2843 let wheel = |cx: &mut gpui::VisualTestContext, dy| {
2844 cx.simulate_event(gpui::ScrollWheelEvent {
2845 position: gpui::point(px(100.), px(100.)),
2846 delta: gpui::ScrollDelta::Lines(gpui::point(0., dy)),
2847 ..Default::default()
2848 });
2849 };
2850 for (dy, phase) in [
2851 (-10000., gpui::TouchPhase::Started),
2852 (0., gpui::TouchPhase::Ended),
2853 ] {
2854 cx.simulate_event(gpui::ScrollWheelEvent {
2855 position: gpui::point(px(100.), px(100.)),
2856 delta: gpui::ScrollDelta::Pixels(gpui::point(px(0.), px(dy))),
2857 touch_phase: phase,
2858 ..Default::default()
2859 });
2860 }
2861 cx.update(|window, cx| window.draw(cx).clear(cx));
2862 let end = state.read_with(cx, |state, _| {
2863 state
2864 .vertical_scroll_handle
2865 .0
2866 .borrow()
2867 .base_handle
2868 .offset()
2869 .y
2870 });
2871 assert!(end < px(0.));
2872 cx.executor().advance_clock(Duration::from_millis(200));
2873 cx.update(|window, cx| window.draw(cx).clear(cx));
2874 assert_eq!(
2875 state.read_with(cx, |state, _| state.col_groups[0].bounds),
2876 header
2877 );
2878 wheel(cx, 2.);
2879 assert!(
2880 state.read_with(cx, |state, _| state
2881 .vertical_scroll_handle
2882 .0
2883 .borrow()
2884 .base_handle
2885 .offset()
2886 .y)
2887 > end
2888 );
2889 }
2890
2891 impl TableDelegate for WidthsDelegate {
2892 fn columns_count(&self, _: &App) -> usize {
2893 6
2894 }
2895
2896 fn rows_count(&self, _: &App) -> usize {
2897 0
2898 }
2899
2900 fn column(&self, col_ix: usize, _: &App) -> Column {
2901 match col_ix {
2902 0 => Column::new("narrow", "Narrow")
2903 .min_width(px(80.))
2904 .width(px(40.)),
2905 1 => Column::new("wide", "Wide")
2906 .max_width(px(120.))
2907 .width(px(300.)),
2908 2 => Column::new("tiny", "Tiny").width(px(10.)),
2910 3 => Column::new("wide_default", "Wide Default").width(px(2000.)),
2912 4 => Column::new("wide_explicit", "Wide Explicit")
2914 .max_width(px(2400.))
2915 .width(px(2000.)),
2916 _ => Column::new("inverted", "Inverted")
2918 .min_width(px(200.))
2919 .max_width(px(100.)),
2920 }
2921 }
2922
2923 fn render_td(
2924 &mut self,
2925 _: usize,
2926 _: usize,
2927 _: &mut Window,
2928 _: &mut Context<TableState<Self>>,
2929 ) -> impl IntoElement {
2930 div()
2931 }
2932 }
2933
2934 #[gpui::test]
2935 fn column_resize_bounds_preserve_initial_widths(cx: &mut TestAppContext) {
2936 cx.skip_drawing();
2937 let window = cx.add_window(|window, cx| TableState::new(WidthsDelegate, window, cx));
2938
2939 window
2940 .update(cx, |table, window, cx| {
2941 let widths = |table: &TableState<WidthsDelegate>| {
2942 table
2943 .col_groups
2944 .iter()
2945 .map(|col_group| col_group.width)
2946 .collect::<Vec<_>>()
2947 };
2948 assert_eq!(
2949 widths(table),
2950 [px(40.), px(300.), px(10.), px(2000.), px(2000.), px(100.)]
2951 );
2952
2953 for ix in 0..table.col_groups.len() {
2954 table.resize_cols(ix, px(10000.), window, cx);
2955 }
2956 assert_eq!(
2957 widths(table),
2958 [
2959 px(1200.),
2960 px(120.),
2961 px(1200.),
2962 px(1200.),
2963 px(2400.),
2964 px(100.)
2965 ]
2966 );
2967
2968 for ix in 0..table.col_groups.len() {
2969 table.resize_cols(ix, px(-100.), window, cx);
2970 }
2971 assert_eq!(
2972 widths(table),
2973 [px(80.), px(20.), px(20.), px(20.), px(20.), px(100.)]
2974 );
2975 })
2976 .unwrap();
2977 }
2978}