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
26const 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#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
64pub enum TableSelection {
65 #[default]
67 None,
68 Row(usize),
70 Column(usize),
72 Cell(usize, usize),
74}
75
76#[derive(Clone)]
78pub enum TableEvent {
79 SelectRow(usize),
81 DoubleClickedRow(usize),
83 SelectColumn(usize),
85 SelectCell(usize, usize),
92 DoubleClickedCell(usize, usize),
99 ColumnWidthsChanged(Vec<Pixels>),
103 MoveColumn(usize, usize),
108 RightClickedRow(Option<usize>),
113 RightClickedCell(usize, usize),
121 ClearSelection,
125}
126
127#[derive(Debug, Default)]
129pub struct TableVisibleRange {
130 rows: Range<usize>,
132 cols: Range<usize>,
134}
135
136impl TableVisibleRange {
137 pub fn rows(&self) -> &Range<usize> {
139 &self.rows
140 }
141
142 pub fn cols(&self) -> &Range<usize> {
144 &self.cols
145 }
146}
147
148#[derive(Clone, Copy)]
151struct ResizeOrigin {
152 x: Pixels,
153 width: Pixels,
154}
155
156#[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 bounds: Bounds<Pixels>,
221 fixed_head_cols_bounds: Bounds<Pixels>,
223 fixed_right_head_cols_bounds: Bounds<Pixels>,
225
226 col_groups: Vec<ColGroup>,
227 header_layout: Vec<Vec<HeaderCell>>,
228
229 pub loop_selection: bool,
233 pub col_selectable: bool,
235 pub row_selectable: bool,
237 select_on_modifier_click: bool,
248 pub cell_selectable: bool,
257 pub row_header: bool,
266 pub sortable: bool,
268 pub col_resizable: bool,
270 pub col_movable: bool,
272 pub col_fixed: bool,
274
275 pub vertical_scroll_handle: UniformListScrollHandle,
276 pub horizontal_scroll_handle: VirtualListScrollHandle,
277
278 selected_row: Option<usize>,
279 selection_mode: SelectionMode,
280 right_clicked_row: Option<usize>,
281 right_clicked_cell: Option<(usize, usize)>,
282 selected_col: Option<usize>,
283 selected_cell: Option<(usize, usize)>,
284
285 resizing_col: Option<usize>,
287 resize_origin: Option<ResizeOrigin>,
289 fitted_col: Option<usize>,
295
296 col_drag_gap: Option<usize>,
300
301 visible_range: TableVisibleRange,
303
304 _measure: Vec<Duration>,
305 _load_more_task: Task<()>,
306}
307
308impl<D> TableState<D>
309where
310 D: TableDelegate,
311{
312 pub fn new(delegate: D, _: &mut Window, cx: &mut Context<Self>) -> Self {
314 let mut this = Self {
315 focus_handle: cx.focus_handle().tab_stop(true),
316 options: TableOptions::default(),
317 delegate,
318 col_groups: Vec::new(),
319 header_layout: Vec::new(),
320 horizontal_scroll_handle: VirtualListScrollHandle::new(),
321 vertical_scroll_handle: UniformListScrollHandle::new(),
322 selection_mode: SelectionMode::Row,
323 selected_row: None,
324 right_clicked_row: None,
325 right_clicked_cell: None,
326 selected_col: None,
327 selected_cell: None,
328 resizing_col: None,
329 resize_origin: None,
330 fitted_col: None,
331 col_drag_gap: None,
332 bounds: Bounds::default(),
333 fixed_head_cols_bounds: Bounds::default(),
334 fixed_right_head_cols_bounds: Bounds::default(),
335 visible_range: TableVisibleRange::default(),
336 loop_selection: true,
337 col_selectable: true,
338 row_selectable: true,
339 select_on_modifier_click: true,
340 cell_selectable: false,
341 row_header: true,
342 sortable: true,
343 col_movable: true,
344 col_resizable: true,
345 col_fixed: true,
346 _load_more_task: Task::ready(()),
347 _measure: Vec::new(),
348 };
349
350 this.prepare_col_groups(cx);
351 this
352 }
353
354 pub fn delegate(&self) -> &D {
356 &self.delegate
357 }
358
359 pub fn delegate_mut(&mut self) -> &mut D {
361 &mut self.delegate
362 }
363
364 pub fn loop_selection(mut self, loop_selection: bool) -> Self {
366 self.loop_selection = loop_selection;
367 self
368 }
369
370 pub fn col_movable(mut self, col_movable: bool) -> Self {
372 self.col_movable = col_movable;
373 self
374 }
375
376 pub fn col_resizable(mut self, col_resizable: bool) -> Self {
378 self.col_resizable = col_resizable;
379 self
380 }
381
382 pub fn sortable(mut self, sortable: bool) -> Self {
384 self.sortable = sortable;
385 self
386 }
387
388 pub fn row_selectable(mut self, row_selectable: bool) -> Self {
390 self.row_selectable = row_selectable;
391 self
392 }
393
394 pub fn select_on_modifier_click(mut self, select_on_modifier_click: bool) -> Self {
397 self.select_on_modifier_click = select_on_modifier_click;
398 self
399 }
400
401 pub fn col_selectable(mut self, col_selectable: bool) -> Self {
403 self.col_selectable = col_selectable;
404 self
405 }
406
407 pub fn cell_selectable(mut self, cell_selectable: bool) -> Self {
425 self.cell_selectable = cell_selectable;
426 self
427 }
428
429 pub fn row_header(mut self, row_header: bool) -> Self {
440 self.row_header = row_header;
441 self
442 }
443
444 pub fn refresh(&mut self, cx: &mut Context<Self>) {
446 self.prepare_col_groups(cx);
447 }
448
449 pub fn scroll_to_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
451 self.vertical_scroll_handle
452 .scroll_to_item(row_ix, ScrollStrategy::Top);
453 cx.notify();
454 }
455
456 pub fn scroll_to_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
458 let scrollable_cols = self.scrollable_cols();
459 if col_ix >= scrollable_cols.end {
461 return;
462 }
463 let col_ix = col_ix.saturating_sub(scrollable_cols.start);
464
465 match self.horizontal_offset_for_col(col_ix) {
473 Some(offset_x) => {
474 let mut offset = self.horizontal_scroll_handle.offset();
475 offset.x = offset_x;
476 self.horizontal_scroll_handle.set_offset(offset);
477 }
478 None => self
481 .horizontal_scroll_handle
482 .scroll_to_item(col_ix, ScrollStrategy::Top),
483 }
484
485 cx.notify();
486 }
487
488 fn horizontal_offset_for_col(&self, col_ix: usize) -> Option<Pixels> {
500 let viewport_width = self.horizontal_scroll_handle.bounds().size.width;
501 if viewport_width <= px(0.) {
502 return None;
503 }
504
505 let cols = self.col_groups.get(self.scrollable_cols())?;
506 let col_left: Pixels = cols.get(..col_ix)?.iter().map(|col| col.width).sum();
507 let col_right = col_left + cols.get(col_ix)?.width;
508 let offset_x = self.horizontal_scroll_handle.offset().x;
509
510 Some(if col_left + offset_x < px(0.) {
511 -col_left
512 } else if col_right + offset_x > viewport_width {
513 viewport_width - col_right
514 } else {
515 offset_x
516 })
517 }
518
519 pub fn selection(&self) -> TableSelection {
524 if let Some(row_ix) = self.selected_row() {
525 TableSelection::Row(row_ix)
526 } else if let Some(col_ix) = self.selected_col() {
527 TableSelection::Column(col_ix)
528 } else if let Some((row_ix, col_ix)) = self.selected_cell() {
529 TableSelection::Cell(row_ix, col_ix)
530 } else {
531 TableSelection::None
532 }
533 }
534
535 pub fn selected_row(&self) -> Option<usize> {
540 self.selected_row.filter(|_| self.selection_mode.is_row())
541 }
542
543 pub fn set_selected_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
545 let is_down = match self.selected_row {
546 Some(selected_row) => row_ix > selected_row,
547 None => true,
548 };
549
550 self.selection_mode = SelectionMode::Row;
551 self.right_clicked_row = None;
552 self.selected_row = Some(row_ix);
553 if let Some(row_ix) = self.selected_row {
554 self.vertical_scroll_handle.scroll_to_item(
555 row_ix,
556 if is_down {
557 ScrollStrategy::Bottom
558 } else {
559 ScrollStrategy::Top
560 },
561 );
562 }
563 cx.emit(TableEvent::SelectRow(row_ix));
564 cx.emit(TableEvent::RightClickedRow(None));
565 cx.notify();
566 }
567
568 pub fn right_clicked_row(&self) -> Option<usize> {
570 self.right_clicked_row
571 }
572
573 pub fn right_clicked_cell(&self) -> Option<(usize, usize)> {
575 self.right_clicked_cell
576 }
577
578 pub fn set_right_clicked_row(&mut self, row: Option<usize>, cx: &mut Context<Self>) {
583 self.right_clicked_row = row;
584 cx.notify();
585 }
586
587 pub fn selected_col(&self) -> Option<usize> {
592 self.selected_col
593 .filter(|_| self.selection_mode.is_column())
594 }
595
596 pub fn set_selected_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
598 self.selection_mode = SelectionMode::Column;
599 self.selected_col = Some(col_ix);
600 if let Some(col_ix) = self.selected_col {
601 self.scroll_to_col(col_ix, cx);
602 }
603 cx.emit(TableEvent::SelectColumn(col_ix));
604 cx.notify();
605 }
606
607 pub fn selected_cell(&self) -> Option<(usize, usize)> {
619 self.selected_cell.filter(|_| self.selection_mode.is_cell())
620 }
621
622 pub fn set_selected_cell(&mut self, row_ix: usize, col_ix: usize, cx: &mut Context<Self>) {
638 self.selection_mode = SelectionMode::Cell;
639 self.selected_cell = Some((row_ix, col_ix));
640 self.right_clicked_cell = None;
641
642 self.vertical_scroll_handle
644 .scroll_to_item(row_ix, ScrollStrategy::Center);
645 self.scroll_to_col(col_ix, cx);
646
647 cx.emit(TableEvent::SelectCell(row_ix, col_ix));
648 cx.notify();
649 }
650
651 pub fn set_selection(&mut self, selection: TableSelection, cx: &mut Context<Self>) {
656 match selection {
657 TableSelection::None => self.clear_selection(cx),
658 TableSelection::Row(row_ix) => self.set_selected_row(row_ix, cx),
659 TableSelection::Column(col_ix) => self.set_selected_col(col_ix, cx),
660 TableSelection::Cell(row_ix, col_ix) => self.set_selected_cell(row_ix, col_ix, cx),
661 }
662 }
663
664 pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
666 self.selection_mode = SelectionMode::Row;
667 self.selected_row = None;
668 self.selected_col = None;
669 self.selected_cell = None;
670 cx.emit(TableEvent::ClearSelection);
671 cx.notify();
672 }
673
674 pub fn visible_range(&self) -> &TableVisibleRange {
678 &self.visible_range
679 }
680
681 pub fn headers(&self, cx: &App) -> Vec<String> {
686 let columns_count = self.delegate.columns_count(cx);
687 let mut headers = Vec::with_capacity(columns_count);
688 for col_ix in 0..columns_count {
689 let column = self.delegate.column(col_ix, cx);
690 headers.push(column.name.to_string());
691 }
692
693 headers
694 }
695
696 pub fn dump(&self, cx: &App) -> (Vec<String>, Vec<Vec<String>>) {
703 self.dump_range(0..self.delegate.rows_count(cx), cx)
704 }
705
706 pub fn dump_range(&self, range: Range<usize>, cx: &App) -> (Vec<String>, Vec<Vec<String>>) {
714 let columns_count = self.delegate.columns_count(cx);
715 let rows_count = self.delegate.rows_count(cx);
716 let start = range.start.min(rows_count);
717 let end = range.end.min(rows_count).max(start);
718
719 let mut rows = Vec::with_capacity(end - start);
720 for row_ix in start..end {
721 let mut row = Vec::with_capacity(columns_count);
722 for col_ix in 0..columns_count {
723 row.push(self.delegate.cell_text(row_ix, col_ix, cx));
724 }
725 rows.push(row);
726 }
727
728 (self.headers(cx), rows)
729 }
730
731 pub fn refresh_header_layout(&mut self, cx: &mut Context<Self>) {
735 self.update_header_layout(cx);
736 cx.notify();
737 }
738
739 fn prepare_col_groups(&mut self, cx: &mut Context<Self>) {
740 self.col_groups = (0..self.delegate.columns_count(cx))
741 .map(|col_ix| {
742 let column = self.delegate().column(col_ix, cx);
743 ColGroup {
744 width: column.width,
745 bounds: Bounds::default(),
746 column,
747 }
748 })
749 .collect();
750
751 self.update_header_layout(cx);
752 }
753
754 fn update_header_layout(&mut self, cx: &mut Context<Self>) {
755 let group_rows = self.delegate.group_headers(cx);
756
757 let mut layout = match group_rows.as_ref() {
758 Some(rows) => Vec::with_capacity(rows.len() + 1),
759 None => Vec::with_capacity(1),
760 };
761
762 if let Some(group_rows) = group_rows {
763 for row in group_rows {
764 let mut cell_row = Vec::with_capacity(row.len());
765 let mut current_leaf_ix = 0;
766 for group in row {
767 let mut width = px(0.);
768 let start_leaf_col_ix = current_leaf_ix;
769 for i in 0..group.span {
770 if current_leaf_ix + i < self.col_groups.len() {
771 width += self.col_groups[current_leaf_ix + i].width;
772 }
773 }
774 current_leaf_ix += group.span;
775 cell_row.push(HeaderCell {
776 label: group.label.clone(),
777 width,
778 col_span: group.span,
779 is_leaf: false,
780 leaf_col_ix: None,
781 start_leaf_col_ix,
782 });
783 }
784 layout.push(cell_row);
785 }
786 }
787
788 let mut leaf_row = Vec::with_capacity(self.col_groups.len());
789 for (ix, group) in self.col_groups.iter().enumerate() {
790 leaf_row.push(HeaderCell {
791 label: group.column.name.clone(),
792 width: group.width,
793 col_span: 1,
794 is_leaf: true,
795 leaf_col_ix: Some(ix),
796 start_leaf_col_ix: ix,
797 });
798 }
799 layout.push(leaf_row);
800
801 self.header_layout = layout;
802 }
803
804 fn fixed_cols_count(&self, side: ColumnFixed) -> usize {
805 if !self.col_fixed {
806 return 0;
807 }
808
809 self.col_groups
810 .iter()
811 .filter(|col| col.column.fixed == Some(side))
812 .count()
813 }
814
815 fn scrollable_cols(&self) -> Range<usize> {
821 let total = self.col_groups.len();
822 let left = self.fixed_cols_count(ColumnFixed::Left).min(total);
823 let right = self.fixed_cols_count(ColumnFixed::Right).min(total - left);
824 left..total - right
825 }
826
827 fn col_fixed_side(&self, col_ix: usize) -> Option<ColumnFixed> {
829 let scrollable_cols = self.scrollable_cols();
830 if col_ix < scrollable_cols.start {
831 Some(ColumnFixed::Left)
832 } else if col_ix >= scrollable_cols.end {
833 Some(ColumnFixed::Right)
834 } else {
835 None
836 }
837 }
838
839 fn header_row_height(&self) -> Pixels {
841 self.options
842 .header_row_height
843 .unwrap_or(self.options.size.table_row_height())
844 }
845
846 fn header_band_height(&self) -> Pixels {
849 self.header_row_height() * self.header_layout.len().max(1) as f32
850 }
851
852 fn page_item_count(&self) -> usize {
853 let row_height = self.options.size.table_row_height();
854 let body_height = (self.bounds.size.height - self.header_band_height()).max(px(0.));
855 let count = (body_height / row_height).floor() as usize;
856 count.max(1)
857 }
858
859 fn on_row_right_click(
860 &mut self,
861 _: &MouseDownEvent,
862 row_ix: Option<usize>,
863 _: &mut Window,
864 cx: &mut Context<Self>,
865 ) {
866 self.right_clicked_row = row_ix;
867 self.right_clicked_cell = None;
868 cx.emit(TableEvent::RightClickedRow(row_ix));
869 }
870
871 fn on_cell_right_click(
872 &mut self,
873 _: &MouseDownEvent,
874 row_ix: usize,
875 col_ix: usize,
876 _: &mut Window,
877 cx: &mut Context<Self>,
878 ) {
879 if !self.cell_selectable {
880 return;
881 }
882
883 cx.stop_propagation();
884 self.right_clicked_cell = Some((row_ix, col_ix));
885 self.right_clicked_row = None;
886 cx.emit(TableEvent::RightClickedCell(row_ix, col_ix));
887 }
888
889 fn on_row_left_click(
890 &mut self,
891 e: &ClickEvent,
892 row_ix: usize,
893 _: &mut Window,
894 cx: &mut Context<Self>,
895 ) {
896 if !self.row_selectable {
897 return;
898 }
899
900 cx.stop_propagation();
901 if self.selects_row_on_click(e, cx) {
902 self.set_selected_row(row_ix, cx);
903 }
904
905 if e.click_count() == 2 {
906 cx.emit(TableEvent::DoubleClickedRow(row_ix));
907 }
908 }
909
910 fn selects_row_on_click(&self, e: &ClickEvent, cx: &App) -> bool {
912 let modifiers = e.modifiers();
913 self.select_on_modifier_click || !(modifiers.secondary(cx) || modifiers.shift)
914 }
915
916 fn on_col_head_click(&mut self, col_ix: usize, _: &mut Window, cx: &mut Context<Self>) {
917 if !self.col_selectable {
918 return;
919 }
920
921 let Some(col_group) = self.col_groups.get(col_ix) else {
922 return;
923 };
924
925 if !col_group.column.selectable {
926 return;
927 }
928
929 self.set_selected_col(col_ix, cx)
930 }
931
932 fn on_cell_click(
933 &mut self,
934 e: &ClickEvent,
935 row_ix: usize,
936 col_ix: usize,
937 _: &mut Window,
938 cx: &mut Context<Self>,
939 ) {
940 if !self.cell_selectable {
941 return;
942 }
943
944 cx.stop_propagation();
945
946 let is_double_click = e.click_count() == 2;
947
948 let is_reselect = self.selected_cell() == Some((row_ix, col_ix));
954 let should_escalate_to_row = !self.row_header
955 && self.row_selectable
956 && is_reselect
957 && !is_double_click
958 && self.selects_row_on_click(e, cx);
959 if should_escalate_to_row {
960 self.set_selected_row(row_ix, cx);
961 return;
962 }
963
964 self.set_selected_cell(row_ix, col_ix, cx);
965
966 if is_double_click {
967 cx.emit(TableEvent::DoubleClickedCell(row_ix, col_ix));
968 }
969 }
970
971 fn has_selection(&self) -> bool {
972 self.selection() != TableSelection::None
973 }
974
975 pub(super) fn action_cancel(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
976 if self.has_selection() {
977 self.clear_selection(cx);
978 return;
979 }
980 cx.propagate();
981 }
982
983 pub(super) fn action_select_prev(
984 &mut self,
985 _: &SelectUp,
986 _: &mut Window,
987 cx: &mut Context<Self>,
988 ) {
989 let rows_count = self.delegate.rows_count(cx);
990 if rows_count < 1 {
991 return;
992 }
993
994 if self.selection_mode.is_cell() {
996 if let Some((row_ix, col_ix)) = self.selected_cell {
997 let new_row = if row_ix > 0 {
998 row_ix.saturating_sub(1)
999 } else if self.loop_selection {
1000 rows_count.saturating_sub(1)
1001 } else {
1002 row_ix
1003 };
1004 self.set_selected_cell(new_row, col_ix, cx);
1005 } else {
1006 self.set_selected_cell(0, 0, cx);
1008 }
1009 return;
1010 }
1011
1012 if !self.row_selectable {
1014 return;
1015 }
1016 let mut selected_row = self.selected_row.unwrap_or(0);
1017 if selected_row > 0 {
1018 selected_row = selected_row.saturating_sub(1);
1019 } else {
1020 if self.loop_selection {
1021 selected_row = rows_count.saturating_sub(1);
1022 }
1023 }
1024
1025 self.set_selected_row(selected_row, cx);
1026 }
1027
1028 pub(super) fn action_select_next(
1029 &mut self,
1030 _: &SelectDown,
1031 _: &mut Window,
1032 cx: &mut Context<Self>,
1033 ) {
1034 let rows_count = self.delegate.rows_count(cx);
1035 if rows_count < 1 {
1036 return;
1037 }
1038
1039 if self.selection_mode.is_cell() {
1041 if let Some((row_ix, col_ix)) = self.selected_cell {
1042 let new_row = if row_ix < rows_count.saturating_sub(1) {
1043 row_ix + 1
1044 } else if self.loop_selection {
1045 0
1046 } else {
1047 row_ix
1048 };
1049 self.set_selected_cell(new_row, col_ix, cx);
1050 } else {
1051 self.set_selected_cell(0, 0, cx);
1053 }
1054 return;
1055 }
1056
1057 if !self.row_selectable {
1059 return;
1060 }
1061 let selected_row = match self.selected_row {
1062 Some(selected_row) if selected_row < rows_count.saturating_sub(1) => selected_row + 1,
1063 Some(selected_row) => {
1064 if self.loop_selection {
1065 0
1066 } else {
1067 selected_row
1068 }
1069 }
1070 _ => 0,
1071 };
1072
1073 self.set_selected_row(selected_row, cx);
1074 }
1075
1076 pub(super) fn action_select_first_column(
1077 &mut self,
1078 _: &SelectFirst,
1079 _: &mut Window,
1080 cx: &mut Context<Self>,
1081 ) {
1082 if self.selection_mode.is_cell() {
1084 if let Some((row_ix, _)) = self.selected_cell {
1085 self.set_selected_cell(row_ix, 0, cx);
1086 } else {
1087 self.set_selected_cell(0, 0, cx);
1089 }
1090 return;
1091 }
1092
1093 if self.selection_mode.is_row() && self.row_selectable {
1095 if self.delegate.rows_count(cx) > 0 {
1096 self.set_selected_row(0, cx);
1097 }
1098 return;
1099 }
1100
1101 self.set_selected_col(0, cx);
1103 }
1104
1105 pub(super) fn action_select_last_column(
1106 &mut self,
1107 _: &SelectLast,
1108 _: &mut Window,
1109 cx: &mut Context<Self>,
1110 ) {
1111 let columns_count = self.delegate.columns_count(cx);
1112
1113 if self.selection_mode.is_cell() {
1115 if let Some((row_ix, _)) = self.selected_cell {
1116 self.set_selected_cell(row_ix, columns_count.saturating_sub(1), cx);
1117 } else {
1118 self.set_selected_cell(0, columns_count.saturating_sub(1), cx);
1120 }
1121 return;
1122 }
1123
1124 if self.selection_mode.is_row() && self.row_selectable {
1126 let rows_count = self.delegate.rows_count(cx);
1127 if rows_count > 0 {
1128 self.set_selected_row(rows_count - 1, cx);
1129 }
1130 return;
1131 }
1132
1133 self.set_selected_col(columns_count.saturating_sub(1), cx);
1135 }
1136
1137 pub(super) fn action_select_page_up(
1138 &mut self,
1139 _: &SelectPageUp,
1140 _: &mut Window,
1141 cx: &mut Context<Self>,
1142 ) {
1143 let step = self.page_item_count();
1144
1145 if self.selection_mode.is_cell() {
1147 if let Some((row_ix, col_ix)) = self.selected_cell {
1148 let target = row_ix.saturating_sub(step);
1149 self.set_selected_cell(target, col_ix, cx);
1150 } else {
1151 self.set_selected_cell(0, 0, cx);
1153 }
1154 return;
1155 }
1156
1157 if !self.row_selectable {
1159 return;
1160 }
1161 let current = self.selected_row.unwrap_or(0);
1162 let target = current.saturating_sub(step);
1163 self.set_selected_row(target, cx);
1164 }
1165
1166 pub(super) fn action_select_page_down(
1167 &mut self,
1168 _: &SelectPageDown,
1169 _: &mut Window,
1170 cx: &mut Context<Self>,
1171 ) {
1172 let rows_count = self.delegate.rows_count(cx);
1173 if rows_count == 0 {
1174 return;
1175 }
1176
1177 let step = self.page_item_count();
1178
1179 if self.selection_mode.is_cell() {
1181 if let Some((row_ix, col_ix)) = self.selected_cell {
1182 let max_row = rows_count.saturating_sub(1);
1183 let target = (row_ix + step).min(max_row);
1184 self.set_selected_cell(target, col_ix, cx);
1185 } else {
1186 self.set_selected_cell(0, 0, cx);
1188 }
1189 return;
1190 }
1191
1192 if !self.row_selectable {
1194 return;
1195 }
1196 let current = self.selected_row.unwrap_or(0);
1197 let max_row = rows_count.saturating_sub(1);
1198 let target = (current + step).min(max_row);
1199 self.set_selected_row(target, cx);
1200 }
1201
1202 pub(super) fn action_select_prev_col(
1203 &mut self,
1204 _: &SelectPrevColumn,
1205 _: &mut Window,
1206 cx: &mut Context<Self>,
1207 ) {
1208 let columns_count = self.delegate.columns_count(cx);
1209
1210 if self.selection_mode.is_cell() {
1212 if let Some((row_ix, col_ix)) = self.selected_cell {
1213 let new_col = if col_ix > 0 {
1214 col_ix.saturating_sub(1)
1215 } else if self.loop_selection {
1216 columns_count.saturating_sub(1)
1217 } else {
1218 col_ix
1219 };
1220 self.set_selected_cell(row_ix, new_col, cx);
1221 } else {
1222 self.set_selected_cell(0, 0, cx);
1224 }
1225 return;
1226 }
1227
1228 let mut selected_col = self.selected_col.unwrap_or(0);
1230 if selected_col > 0 {
1231 selected_col = selected_col.saturating_sub(1);
1232 } else {
1233 if self.loop_selection {
1234 selected_col = columns_count.saturating_sub(1);
1235 }
1236 }
1237 self.set_selected_col(selected_col, cx);
1238 }
1239
1240 pub(super) fn action_select_next_col(
1241 &mut self,
1242 _: &SelectNextColumn,
1243 _: &mut Window,
1244 cx: &mut Context<Self>,
1245 ) {
1246 let columns_count = self.delegate.columns_count(cx);
1247
1248 if self.selection_mode.is_cell() {
1250 if let Some((row_ix, col_ix)) = self.selected_cell {
1251 let new_col = if col_ix < columns_count.saturating_sub(1) {
1252 col_ix + 1
1253 } else if self.loop_selection {
1254 0
1255 } else {
1256 col_ix
1257 };
1258 self.set_selected_cell(row_ix, new_col, cx);
1259 } else {
1260 self.set_selected_cell(0, 0, cx);
1262 }
1263 return;
1264 }
1265
1266 let mut selected_col = self.selected_col.unwrap_or(0);
1268 if selected_col < columns_count.saturating_sub(1) {
1269 selected_col += 1;
1270 } else {
1271 if self.loop_selection {
1272 selected_col = 0;
1273 }
1274 }
1275
1276 self.set_selected_col(selected_col, cx);
1277 }
1278
1279 fn scroll_table_by_col_resizing(
1281 &mut self,
1282 mouse_position: Point<Pixels>,
1283 col_group: &ColGroup,
1284 ) {
1285 if mouse_position.x > self.bounds.right() {
1287 return;
1288 }
1289
1290 let mut offset = self.horizontal_scroll_handle.offset();
1291 let col_bounds = col_group.bounds;
1292
1293 if mouse_position.x < self.bounds.left()
1294 && col_bounds.right() < self.bounds.left() + px(20.)
1295 {
1296 offset.x += px(1.);
1297 } else if mouse_position.x > self.bounds.right()
1298 && col_bounds.right() > self.bounds.right() - px(20.)
1299 {
1300 offset.x -= px(1.);
1301 }
1302
1303 self.horizontal_scroll_handle.set_offset(offset);
1304 }
1305
1306 pub fn col_width(&self, col_ix: usize) -> Option<Pixels> {
1313 self.col_groups.get(col_ix).map(|group| group.width)
1314 }
1315
1316 pub fn fit_col(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> bool {
1325 if !self.is_col_resizable(col_ix) {
1326 return false;
1327 }
1328 let Some(content) = self.delegate.fit_column(col_ix, window, cx) else {
1329 return false;
1330 };
1331 let padding = self
1332 .col_groups
1333 .get(col_ix)
1334 .and_then(|group| group.column.paddings)
1335 .unwrap_or_else(|| self.options.size.table_cell_padding());
1336 let scale_factor = window.scale_factor();
1340 let device_pixels = |length: Pixels| (f32::from(length) * scale_factor).ceil();
1341 let mut content_pixels = device_pixels(content);
1342 if px(content_pixels / scale_factor) < content {
1345 content_pixels += 1.;
1346 }
1347 let width = content_pixels + device_pixels(padding.left) + device_pixels(padding.right);
1348 if self.resize_cols(col_ix, px(width / scale_factor), window, cx) {
1349 let new_widths = self.col_groups.iter().map(|g| g.width).collect();
1350 cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
1351 }
1352 true
1353 }
1354
1355 fn resize_cols(
1360 &mut self,
1361 ix: usize,
1362 size: Pixels,
1363 _: &mut Window,
1364 cx: &mut Context<Self>,
1365 ) -> bool {
1366 if !self.col_resizable {
1367 return false;
1368 }
1369
1370 let mut changed = false;
1371 if let Some(col_group) = self.col_groups.get_mut(ix) {
1372 if col_group.is_resizable() {
1373 let new_width = size
1374 .max(col_group.column.min_width)
1375 .min(col_group.column.max_width);
1376 if col_group.width != new_width {
1377 col_group.width = new_width;
1378 changed = true;
1379 }
1380 }
1381 }
1382
1383 if changed {
1384 self.update_header_layout(cx);
1385 cx.notify();
1386 }
1387 changed
1388 }
1389
1390 fn perform_sort(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
1391 if !self.sortable {
1392 return;
1393 }
1394
1395 let sort = self.col_groups.get(col_ix).and_then(|g| g.column.sort);
1396 if sort.is_none() {
1397 return;
1398 }
1399
1400 let sort = sort.unwrap();
1401 let sort = match sort {
1402 ColumnSort::Ascending => ColumnSort::Default,
1403 ColumnSort::Descending => ColumnSort::Ascending,
1404 ColumnSort::Default => ColumnSort::Descending,
1405 };
1406
1407 for (ix, col_group) in self.col_groups.iter_mut().enumerate() {
1408 if ix == col_ix {
1409 col_group.column.sort = Some(sort);
1410 } else {
1411 if col_group.column.sort.is_some() {
1412 col_group.column.sort = Some(ColumnSort::Default);
1413 }
1414 }
1415 }
1416
1417 self.delegate_mut().perform_sort(col_ix, sort, window, cx);
1418
1419 cx.notify();
1420 }
1421
1422 fn move_column(
1423 &mut self,
1424 col_ix: usize,
1425 to_ix: usize,
1426 window: &mut Window,
1427 cx: &mut Context<Self>,
1428 ) {
1429 if col_ix == to_ix {
1430 return;
1431 }
1432
1433 self.delegate.move_column(col_ix, to_ix, window, cx);
1434 let col_group = self.col_groups.remove(col_ix);
1435 self.col_groups.insert(to_ix, col_group);
1436
1437 cx.emit(TableEvent::MoveColumn(col_ix, to_ix));
1438 cx.notify();
1439 }
1440
1441 fn drag_gap_at(&self, x: Pixels, drag_col_ix: usize) -> Option<usize> {
1445 let scrollable_cols = self.scrollable_cols();
1446
1447 let pointer_side = if scrollable_cols.start > 0 && x < self.fixed_head_cols_bounds.right() {
1452 Some(ColumnFixed::Left)
1453 } else if scrollable_cols.end < self.col_groups.len()
1454 && x >= self.fixed_right_head_cols_bounds.left()
1455 {
1456 Some(ColumnFixed::Right)
1457 } else {
1458 None
1459 };
1460 if self.col_fixed_side(drag_col_ix) != pointer_side {
1461 return None;
1462 }
1463
1464 let candidates = match pointer_side {
1468 Some(ColumnFixed::Left) => 0..scrollable_cols.start,
1469 Some(ColumnFixed::Right) => scrollable_cols.end..self.col_groups.len(),
1470 None => self.calculate_visible_leaf_col_range(scrollable_cols).0,
1471 };
1472
1473 let mut gap = candidates.start;
1475 for ix in candidates {
1476 if x < self.col_groups[ix].bounds.center().x {
1477 break;
1478 }
1479 gap = ix + 1;
1480 }
1481
1482 if gap == drag_col_ix || gap == drag_col_ix + 1 {
1485 None
1486 } else {
1487 Some(gap)
1488 }
1489 }
1490
1491 fn load_more_if_need(
1493 &mut self,
1494 rows_count: usize,
1495 visible_end: usize,
1496 window: &mut Window,
1497 cx: &mut Context<Self>,
1498 ) {
1499 let threshold = self.delegate.load_more_threshold();
1500 if visible_end >= rows_count.saturating_sub(threshold) {
1502 if !self.delegate.has_more(cx) {
1503 return;
1504 }
1505
1506 self._load_more_task = cx.spawn_in(window, async move |view, window| {
1507 _ = view.update_in(window, |view, window, cx| {
1508 view.delegate.load_more(window, cx);
1509 });
1510 });
1511 }
1512 }
1513
1514 fn update_visible_range_if_need(
1515 &mut self,
1516 visible_range: Range<usize>,
1517 items_count: usize,
1518 axis: Axis,
1519 window: &mut Window,
1520 cx: &mut Context<Self>,
1521 ) {
1522 if visible_range.len() <= 1 && items_count > 1 {
1525 return;
1526 }
1527
1528 let end = visible_range.end.min(items_count);
1530 let visible_range = visible_range.start.min(end)..end;
1531 if visible_range.is_empty() && items_count > 0 {
1533 return;
1534 }
1535
1536 if axis == Axis::Vertical {
1537 if self.visible_range.rows == visible_range {
1538 return;
1539 }
1540 self.delegate_mut()
1541 .visible_rows_changed(visible_range.clone(), window, cx);
1542 self.visible_range.rows = visible_range;
1543 } else {
1544 if self.visible_range.cols == visible_range {
1545 return;
1546 }
1547 self.delegate_mut()
1548 .visible_columns_changed(visible_range.clone(), window, cx);
1549 self.visible_range.cols = visible_range;
1550 }
1551 }
1552
1553 fn render_cell(
1554 &self,
1555 _row_ix: Option<usize>,
1556 col_ix: usize,
1557 _window: &mut Window,
1558 _cx: &mut Context<Self>,
1559 ) -> Div {
1560 let Some(col_group) = self.col_groups.get(col_ix) else {
1561 return div();
1562 };
1563
1564 let col_width = col_group.width;
1565 let col_padding = col_group.column.paddings;
1566
1567 div()
1568 .w(col_width)
1569 .h_full()
1570 .flex_shrink_0()
1571 .overflow_hidden()
1572 .whitespace_nowrap()
1573 .table_cell_size(self.options.size)
1574 .map(|this| match col_group.column.align {
1577 TextAlign::Center => this.text_center(),
1578 TextAlign::Right => this.text_right(),
1579 _ => this,
1580 })
1581 .map(|this| match col_padding {
1582 Some(padding) => this
1583 .pl(padding.left)
1584 .pr(padding.right)
1585 .pt(padding.top)
1586 .pb(padding.bottom),
1587 None => this,
1588 })
1589 }
1590
1591 fn a11y_cell<E>(&self, cell: E, row_ix: usize, col_ix: usize, window: &Window, cx: &App) -> E
1597 where
1598 E: gpui::StatefulInteractiveElement + FluentBuilder,
1599 {
1600 let cell = if self.cell_selectable {
1601 let is_selected = self.selected_cell() == Some((row_ix, col_ix));
1602 cell.role(gpui::Role::GridCell)
1603 .aria_selected(is_selected)
1604 .when(
1607 is_selected && self.focus_handle.is_focused(window),
1608 |this| this.aria_active_descendant(),
1609 )
1610 } else {
1611 cell.role(gpui::Role::Cell)
1612 };
1613 let label = if window.is_a11y_active() || cfg!(feature = "test-support") {
1617 self.delegate.cell_label(row_ix, col_ix, cx)
1618 } else {
1619 None
1620 };
1621 cell.aria_column_index(col_ix)
1622 .when_some(label, |this, label| this.aria_label(label))
1623 }
1624
1625 fn render_col_wrap(
1628 &self,
1629 _row_ix: Option<usize>,
1630 col_ix: usize,
1631 _: &mut Window,
1632 cx: &mut Context<Self>,
1633 ) -> Div {
1634 let el = h_flex().h_full();
1635 let selectable = self.col_selectable
1636 && self
1637 .col_groups
1638 .get(col_ix)
1639 .map(|col_group| col_group.column.selectable)
1640 .unwrap_or(false);
1641
1642 if selectable && self.selected_col() == Some(col_ix) {
1645 el.bg(cx.theme().tokens.table_active)
1646 } else {
1647 el
1648 }
1649 }
1650
1651 fn render_leading_resize_handle(
1658 &self,
1659 ix: usize,
1660 _: &mut Window,
1661 cx: &mut Context<Self>,
1662 ) -> impl IntoElement {
1663 let is_fixed_right = |ix: usize| {
1667 self.col_fixed
1668 && self.col_groups.get(ix).and_then(|col| col.column.fixed)
1669 == Some(ColumnFixed::Right)
1670 };
1671 let Some(prev) = ix.checked_sub(1).filter(|prev| {
1672 self.is_col_resizable(*prev) && (!is_fixed_right(ix) || is_fixed_right(*prev))
1673 }) else {
1674 return div().into_any_element();
1675 };
1676
1677 self.resize_handle_band(prev, ("resizable-handle-leading", ix).into(), cx)
1678 .absolute()
1679 .top_0()
1680 .left_0()
1681 .h_full()
1682 .w(HANDLE_PADDING)
1683 .into_any_element()
1684 }
1685
1686 fn render_resize_handle(
1694 &self,
1695 ix: usize,
1696 _: &mut Window,
1697 cx: &mut Context<Self>,
1698 ) -> impl IntoElement {
1699 if !self.is_col_resizable(ix) {
1700 return div().into_any_element();
1701 }
1702
1703 let group_id = SharedString::from(format!("resizable-handle:{}", ix));
1704
1705 self.resize_handle_band(ix, ("resizable-handle", ix).into(), cx)
1706 .group(group_id.clone())
1707 .h_full()
1708 .w(HANDLE_PADDING)
1709 .ml(-HANDLE_PADDING)
1710 .justify_end()
1711 .items_center()
1712 .child(
1713 div()
1714 .h_full()
1715 .justify_center()
1716 .bg(cx.theme().table_row_border)
1717 .group_hover(&group_id, |this| this.bg(cx.theme().border).h_full())
1718 .w(px(1.)),
1719 )
1720 .into_any_element()
1721 }
1722
1723 fn is_col_resizable(&self, ix: usize) -> bool {
1724 self.col_resizable
1725 && self
1726 .col_groups
1727 .get(ix)
1728 .map(|col| col.is_resizable())
1729 .unwrap_or(false)
1730 }
1731
1732 fn resize_handle_band(
1742 &self,
1743 ix: usize,
1744 id: ElementId,
1745 cx: &mut Context<Self>,
1746 ) -> Stateful<Div> {
1747 h_flex()
1748 .id(id)
1749 .occlude()
1750 .cursor_col_resize()
1751 .on_drag_move(
1752 cx.listener(move |view, e: &DragMoveEvent<ResizeColumn>, window, cx| {
1753 match e.drag(cx) {
1754 ResizeColumn((entity_id, ix)) => {
1755 if cx.entity_id() != *entity_id {
1756 return;
1757 }
1758
1759 let ix = *ix;
1766 if view.fitted_col == Some(ix) {
1767 return;
1768 }
1769 view.resizing_col = Some(ix);
1770
1771 let col_group = view
1772 .col_groups
1773 .get(ix)
1774 .expect("BUG: invalid col index")
1775 .clone();
1776
1777 let Some(origin) = view.resize_origin else {
1783 return;
1784 };
1785 view.resize_cols(
1786 ix,
1787 origin.width + (e.event.position.x - origin.x),
1788 window,
1789 cx,
1790 );
1791
1792 view.scroll_table_by_col_resizing(e.event.position, &col_group);
1794 }
1795 };
1796 }),
1797 )
1798 .on_drag(ResizeColumn((cx.entity_id(), ix)), |drag, _, _, cx| {
1799 cx.stop_propagation();
1800 cx.new(|_| drag.clone())
1801 })
1802 .on_mouse_down(
1803 MouseButton::Left,
1804 cx.listener(move |view, e: &MouseDownEvent, window, cx| {
1805 view.resize_origin = view.col_groups.get(ix).map(|col_group| ResizeOrigin {
1806 x: e.position.x,
1807 width: col_group.width,
1808 });
1809 if e.click_count == 1 {
1810 view.fitted_col = None;
1811 } else if e.click_count == 2 && view.fit_col(ix, window, cx) {
1812 view.fitted_col = Some(ix);
1813 }
1814 }),
1815 )
1816 .on_mouse_up_out(
1817 MouseButton::Left,
1818 cx.listener(|view, _, _, cx| {
1819 view.resize_origin = None;
1820 if view.resizing_col.is_none() {
1821 return;
1822 }
1823
1824 view.resizing_col = None;
1825
1826 let new_widths = view.col_groups.iter().map(|g| g.width).collect();
1827 cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
1828 cx.notify();
1829 }),
1830 )
1831 }
1832
1833 fn render_fixed_cols_border(&self, side: ColumnFixed, cx: &App) -> Option<Div> {
1836 self.options.fixed_cols_border.then(|| {
1837 div()
1838 .absolute()
1839 .top_0()
1840 .bottom_0()
1841 .w_0()
1842 .flex_shrink_0()
1843 .border_color(cx.theme().border)
1844 .map(|this| match side {
1845 ColumnFixed::Left => this.right_0().border_r_1(),
1846 ColumnFixed::Right => this.left_0().border_l_1(),
1847 })
1848 })
1849 }
1850
1851 fn render_row_header_cell(
1853 &self,
1854 row_ix: usize,
1855 is_head: bool,
1856 cx: &mut Context<Self>,
1857 ) -> impl IntoElement {
1858 div()
1859 .id(("row-header", row_ix))
1860 .w_3()
1861 .h_full()
1862 .border_r_1()
1863 .border_color(cx.theme().table_row_border)
1864 .bg(cx.theme().tokens.table_head)
1865 .flex_shrink_0()
1866 .table_cell_size(self.options.size)
1867 .when(!is_head, |this| {
1868 this.when(self.row_selectable, |this| {
1869 this.on_click(cx.listener(move |table, e: &ClickEvent, _window, cx| {
1870 cx.stop_propagation();
1871 if table.selects_row_on_click(e, cx) {
1872 table.set_selected_row(row_ix, cx);
1873 }
1874 }))
1875 })
1876 })
1877 }
1878
1879 fn render_sort_icon(
1880 &self,
1881 col_ix: usize,
1882 col_group: &ColGroup,
1883 _: &mut Window,
1884 cx: &mut Context<Self>,
1885 ) -> Option<impl IntoElement> {
1886 if !self.sortable {
1887 return None;
1888 }
1889
1890 let Some(sort) = col_group.column.sort else {
1891 return None;
1892 };
1893
1894 let (icon, is_on) = match sort {
1895 ColumnSort::Ascending => (IconName::SortAscending, true),
1896 ColumnSort::Descending => (IconName::SortDescending, true),
1897 ColumnSort::Default => (IconName::ChevronsUpDown, false),
1898 };
1899
1900 Some(
1901 div()
1902 .id(("icon-sort", col_ix))
1903 .test_support()
1904 .role(gpui::Role::Button)
1905 .aria_label(t!("Table.SortBy", column = col_group.column.name).to_string())
1906 .p(px(2.))
1907 .rounded(cx.theme().radius / 2.)
1908 .map(|this| match is_on {
1909 true => this,
1910 false => this.opacity(0.5),
1911 })
1912 .hover(|this| this.bg(cx.theme().tokens.secondary).opacity(7.))
1913 .active(|this| this.bg(cx.theme().tokens.secondary_active).opacity(1.))
1914 .on_click(
1915 cx.listener(move |table, _, window, cx| table.perform_sort(col_ix, window, cx)),
1916 )
1917 .child(
1918 Icon::new(icon)
1919 .size_3()
1920 .text_color(cx.theme().secondary_foreground),
1921 ),
1922 )
1923 }
1924
1925 fn render_th(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> Div {
1930 let entity_id = cx.entity_id();
1931 let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index");
1932
1933 let movable = self.col_movable && col_group.column.movable;
1934 let paddings = col_group.column.paddings;
1935 let name = col_group.column.name.clone();
1936
1937 h_flex()
1938 .h_full()
1939 .child(
1940 self.render_cell(None, col_ix, window, cx)
1941 .when(
1946 self.options.header_row_height.is_some() && paddings.is_none(),
1947 |this| this.py_0(),
1948 )
1949 .id(("col-header", col_ix))
1950 .role(gpui::Role::ColumnHeader)
1951 .aria_label(name.clone())
1952 .aria_column_index(col_ix)
1953 .test_support()
1954 .on_click(cx.listener(move |this, _, window, cx| {
1955 this.on_col_head_click(col_ix, window, cx);
1956 }))
1957 .child(
1958 h_flex()
1959 .size_full()
1960 .justify_between()
1961 .items_center()
1962 .child(self.delegate.render_th(col_ix, window, cx))
1963 .when_some(paddings, |this, paddings| {
1964 let offset_pr =
1966 self.options.size.table_cell_padding().right - paddings.right;
1967 this.pr(offset_pr.max(px(0.)))
1968 })
1969 .children(self.render_sort_icon(col_ix, &col_group, window, cx)),
1970 )
1971 .when(movable, |this| {
1972 this.on_drag(
1973 DragColumn {
1974 entity_id,
1975 col_ix,
1976 name,
1977 width: col_group.width,
1978 },
1979 |drag, _, _, cx| {
1980 cx.stop_propagation();
1981 cx.new(|_| drag.clone())
1982 },
1983 )
1984 })
1985 .map(|this| {
1986 let last_gap = col_ix + 1 == self.col_groups.len();
1991 match self.col_drag_gap {
1992 Some(gap)
1993 if cx.has_active_drag()
1994 && (gap == col_ix || (last_gap && gap == col_ix + 1)) =>
1995 {
1996 let right_side = gap == col_ix + 1;
1997 this.relative().child(
1998 div()
1999 .absolute()
2000 .top_0()
2001 .bottom_0()
2002 .w(px(2.))
2003 .map(|d| if right_side { d.right_0() } else { d.left_0() })
2004 .bg(cx.theme().drag_border),
2005 )
2006 }
2007 _ => this,
2008 }
2009 }),
2010 )
2011 .child(self.render_resize_handle(col_ix, window, cx))
2013 .child(self.render_leading_resize_handle(col_ix, window, cx))
2015 .on_prepaint({
2017 let view = cx.entity().clone();
2018 move |bounds, _, cx| view.update(cx, |r, _| r.col_groups[col_ix].bounds = bounds)
2019 })
2020 }
2021
2022 fn calculate_visible_leaf_col_range(
2032 &self,
2033 scrollable_cols: Range<usize>,
2034 ) -> (Range<usize>, Pixels) {
2035 let (left_columns_count, total_cols) = (scrollable_cols.start, scrollable_cols.end);
2036
2037 if self.bounds.size.width == px(0.) {
2038 return (scrollable_cols, px(0.));
2039 }
2040
2041 let fixed_width =
2042 self.fixed_head_cols_bounds.size.width + self.fixed_right_head_cols_bounds.size.width;
2043 let available_width = (self.bounds.size.width - fixed_width).max(px(0.));
2044 let scroll_x = (-self.horizontal_scroll_handle.offset().x).max(px(0.));
2047
2048 let mut range_start = left_columns_count;
2052 let mut left_spacer = px(0.);
2053 let mut cumulative = px(0.);
2054 for i in left_columns_count..total_cols {
2055 let right_edge = cumulative + self.col_groups[i].width;
2056 if right_edge > scroll_x {
2057 range_start = i;
2058 left_spacer = cumulative;
2059 break;
2060 }
2061 cumulative = right_edge;
2062 }
2063
2064 let right_bound = scroll_x + available_width + px(200.);
2068 let mut range_end = total_cols;
2069 let mut cumulative = left_spacer; for i in range_start..total_cols {
2071 cumulative += self.col_groups[i].width;
2072 if cumulative > right_bound {
2073 range_end = (i + 1).min(total_cols);
2074 break;
2075 }
2076 }
2077
2078 (range_start..range_end, left_spacer)
2079 }
2080
2081 fn render_table_header(
2082 &mut self,
2083 scrollable_cols: Range<usize>,
2084 window: &mut Window,
2085 cx: &mut Context<Self>,
2086 ) -> impl IntoElement {
2087 let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
2088
2089 let total_cols = self.col_groups.len();
2103 let left_columns_count = scrollable_cols.start;
2104 let right_columns_start = scrollable_cols.end;
2105 let (visible_col_range, left_spacer) =
2106 self.calculate_visible_leaf_col_range(scrollable_cols.clone());
2107
2108 let layout_len = self.header_layout.len();
2109 let header_row_height = self.header_row_height();
2110
2111 if left_columns_count == 0 {
2113 self.fixed_head_cols_bounds = Bounds::default();
2114 }
2115 if right_columns_start == total_cols {
2116 self.fixed_right_head_cols_bounds = Bounds::default();
2117 }
2118
2119 let mut header = self.delegate_mut().render_header(window, cx);
2120 let style = header.style().clone();
2121 let layout = self.header_layout.clone();
2122
2123 header
2124 .role(gpui::Role::Row)
2127 .aria_row_index(0)
2128 .h_flex()
2129 .w_full()
2130 .flex_shrink_0()
2131 .bg(cx.theme().tokens.table_head)
2132 .text_color(cx.theme().table_head_foreground)
2133 .refine_style(&style)
2134 .on_drag_move(cx.listener(|table, e: &DragMoveEvent<DragColumn>, _, cx| {
2135 let drag = e.drag(cx);
2136 let (drag_entity_id, drag_col_ix) = (drag.entity_id, drag.col_ix);
2137
2138 let gap =
2139 if drag_entity_id == cx.entity_id() && e.bounds.contains(&e.event.position) {
2140 table.drag_gap_at(e.event.position.x, drag_col_ix)
2141 } else {
2142 None
2143 };
2144
2145 if table.col_drag_gap != gap {
2146 table.col_drag_gap = gap;
2147 cx.notify();
2148 }
2149 }))
2150 .on_drop(cx.listener(|table, drag: &DragColumn, window, cx| {
2151 if drag.entity_id != cx.entity_id() {
2152 return;
2153 }
2154
2155 let Some(gap) = table.col_drag_gap.take() else {
2157 return;
2158 };
2159 let to_ix = if drag.col_ix < gap { gap - 1 } else { gap };
2160 table.move_column(drag.col_ix, to_ix, window, cx);
2161 }))
2162 .when(self.cell_selectable && self.row_header, |this| {
2163 this.child(self.render_row_header_cell(0, true, cx))
2164 })
2165 .when(left_columns_count > 0, |this| {
2166 this.child(self.render_fixed_head_cols(
2167 0..left_columns_count,
2168 ColumnFixed::Left,
2169 window,
2170 cx,
2171 ))
2172 })
2173 .child(
2174 h_flex()
2176 .id("table-head")
2177 .size_full()
2178 .overflow_scroll()
2179 .relative()
2180 .track_scroll(&horizontal_scroll_handle)
2181 .bg(cx.theme().tokens.table_head)
2182 .child(v_flex().min_w_full().flex_shrink_0().children(
2183 layout.iter().enumerate().map(|(row_ix, row_cells)| {
2184 let is_leaf_row = row_ix + 1 == layout_len;
2185 h_flex()
2186 .min_w_full()
2187 .h(header_row_height)
2188 .border_b_1()
2189 .border_color(cx.theme().border)
2190 .map(|this| {
2191 if is_leaf_row {
2192 this.when(left_spacer > px(0.), |r| {
2197 r.child(div().w(left_spacer).h_full().flex_shrink_0())
2198 })
2199 .children(row_cells.iter().filter_map(|cell| {
2200 if cell.is_leaf {
2201 let ix = cell.leaf_col_ix?;
2202 if !visible_col_range.contains(&ix) {
2203 return None;
2204 }
2205 Some(
2206 self.render_th(ix, window, cx)
2207 .into_any_element(),
2208 )
2209 } else {
2210 None
2211 }
2212 }))
2213 .when(visible_col_range.end < right_columns_start, |r| {
2214 let right_spacer: Pixels = self.col_groups
2215 [visible_col_range.end..right_columns_start]
2216 .iter()
2217 .map(|g| g.width)
2218 .sum();
2219 r.child(div().w(right_spacer).h_full().flex_shrink_0())
2220 })
2221 .child(self.delegate.render_last_empty_col(window, cx))
2222 } else {
2223 this.children(row_cells.iter().filter_map(|cell| {
2226 if scrollable_cols.contains(&cell.start_leaf_col_ix) {
2227 if cell.is_leaf {
2228 if let Some(ix) = cell.leaf_col_ix {
2229 return Some(
2230 self.render_th(ix, window, cx)
2231 .into_any_element(),
2232 );
2233 }
2234 } else {
2235 return Some(
2236 self.delegate_mut()
2237 .render_group_th(
2238 &cell.label,
2239 cell.col_span,
2240 cell.width,
2241 window,
2242 cx,
2243 )
2244 .into_any_element(),
2245 );
2246 }
2247 }
2248 None
2249 }))
2250 .child(self.delegate.render_last_empty_col(window, cx))
2251 }
2252 })
2253 }),
2254 )),
2255 )
2256 .when(right_columns_start < total_cols, |this| {
2257 this.child(self.render_fixed_head_cols(
2258 right_columns_start..total_cols,
2259 ColumnFixed::Right,
2260 window,
2261 cx,
2262 ))
2263 })
2264 }
2265
2266 fn render_fixed_head_cols(
2269 &mut self,
2270 cols: Range<usize>,
2271 side: ColumnFixed,
2272 window: &mut Window,
2273 cx: &mut Context<Self>,
2274 ) -> impl IntoElement {
2275 let view = cx.entity().clone();
2276 let header_row_height = self.header_row_height();
2277 let layout = self.header_layout.clone();
2278
2279 h_flex()
2280 .relative()
2281 .h_full()
2282 .flex_shrink_0()
2283 .bg(cx.theme().tokens.table_head)
2284 .child(
2285 v_flex()
2286 .min_w_full()
2287 .flex_shrink_0()
2288 .children(layout.iter().map(|row_cells| {
2289 h_flex()
2290 .min_w_full()
2291 .h(header_row_height)
2292 .border_b_1()
2293 .border_color(cx.theme().border)
2294 .children(row_cells.iter().filter_map(|cell| {
2295 if !cols.contains(&cell.start_leaf_col_ix) {
2296 return None;
2297 }
2298
2299 if cell.is_leaf {
2300 let ix = cell.leaf_col_ix?;
2301 Some(self.render_th(ix, window, cx).into_any_element())
2302 } else {
2303 Some(
2304 self.delegate_mut()
2305 .render_group_th(
2306 &cell.label,
2307 cell.col_span,
2308 cell.width,
2309 window,
2310 cx,
2311 )
2312 .into_any_element(),
2313 )
2314 }
2315 }))
2316 })),
2317 )
2318 .children(self.render_fixed_cols_border(side, cx))
2319 .on_prepaint(move |bounds, _, cx| {
2320 view.update(cx, |table, _| match side {
2321 ColumnFixed::Left => table.fixed_head_cols_bounds = bounds,
2322 ColumnFixed::Right => table.fixed_right_head_cols_bounds = bounds,
2323 })
2324 })
2325 }
2326
2327 #[allow(clippy::too_many_arguments)]
2328 fn render_table_row(
2329 &mut self,
2330 row_ix: usize,
2331 rows_count: usize,
2332 scrollable_cols: Range<usize>,
2333 col_sizes: Rc<Vec<gpui::Size<Pixels>>>,
2334 columns_count: usize,
2335 is_filled: bool,
2336 window: &mut Window,
2337 cx: &mut Context<Self>,
2338 ) -> gpui::AnyElement {
2339 let is_stripe_row = self.options.stripe && row_ix % 2 != 0;
2340 let is_selected = self.selected_row() == Some(row_ix);
2343 let view = cx.entity().clone();
2344 let row_height = self.options.size.table_row_height();
2345
2346 if row_ix < rows_count {
2347 let is_last_row = row_ix + 1 == rows_count;
2348 let need_render_border = is_selected || !is_last_row || !is_filled;
2349
2350 let mut tr = self.delegate.render_tr(row_ix, window, cx);
2351 let style = tr.style().clone();
2352 let label = if window.is_a11y_active() || cfg!(feature = "test-support") {
2354 self.delegate.row_label(row_ix, cx)
2355 } else {
2356 None
2357 };
2358
2359 tr.test_support()
2360 .role(gpui::Role::Row)
2361 .aria_row_index(row_ix + 1)
2363 .when_some(label, |this, label| this.aria_label(label))
2364 .aria_selected(is_selected)
2365 .when(
2368 is_selected && self.focus_handle.is_focused(window),
2369 |this| this.aria_active_descendant(),
2370 )
2371 .h_flex()
2372 .w_full()
2373 .h(row_height)
2374 .when(need_render_border, |this| {
2375 this.border_b_1().border_color(cx.theme().table_row_border)
2376 })
2377 .when(is_stripe_row, |this| this.bg(cx.theme().tokens.table_even))
2378 .refine_style(&style)
2379 .hover(|this| {
2380 if is_selected || self.right_clicked_row == Some(row_ix) {
2381 this
2382 } else {
2383 this.bg(cx.theme().tokens.table_hover)
2384 }
2385 })
2386 .when(self.cell_selectable && self.row_header, |this| {
2387 this.child(self.render_row_header_cell(row_ix, false, cx))
2388 })
2389 .when(scrollable_cols.start > 0, |this| {
2390 this.child(self.render_fixed_cells(
2391 row_ix,
2392 0..scrollable_cols.start,
2393 ColumnFixed::Left,
2394 window,
2395 cx,
2396 ))
2397 })
2398 .child(
2399 h_flex()
2400 .flex_1()
2401 .h_full()
2402 .overflow_hidden()
2403 .relative()
2404 .child(
2405 crate::virtual_list::virtual_list(
2406 view,
2407 row_ix,
2408 Axis::Horizontal,
2409 col_sizes,
2410 {
2411 let scrollable_cols = scrollable_cols.clone();
2412 move |table, visible_range: Range<usize>, window, cx| {
2413 table.update_visible_range_if_need(
2414 visible_range.clone(),
2415 scrollable_cols.len(),
2416 Axis::Horizontal,
2417 window,
2418 cx,
2419 );
2420
2421 let mut items = Vec::with_capacity(
2422 visible_range.end - visible_range.start,
2423 );
2424
2425 visible_range.for_each(|col_ix| {
2426 let col_ix = col_ix + scrollable_cols.start;
2427 let is_cell_selected =
2428 table.selected_cell() == Some((row_ix, col_ix));
2429 let is_cell_right_clicked =
2430 table.right_clicked_cell == Some((row_ix, col_ix));
2431
2432 let el = table
2433 .render_col_wrap(Some(row_ix), col_ix, window, cx)
2434 .child(
2435 table
2436 .a11y_cell(
2437 table
2438 .render_cell(
2439 Some(row_ix),
2440 col_ix,
2441 window,
2442 cx,
2443 )
2444 .id(format!(
2445 "table-cell-{}:{}",
2446 row_ix, col_ix
2447 ))
2448 .test_support(),
2449 row_ix,
2450 col_ix,
2451 window,
2452 cx,
2453 )
2454 .relative()
2455 .child(table.measure_render_td(
2456 row_ix, col_ix, window, cx,
2457 ))
2458 .when(is_cell_selected, |this| {
2459 this.child(
2460 div()
2461 .absolute()
2462 .inset_0()
2463 .bg(cx
2464 .theme()
2465 .tokens
2466 .table_active)
2467 .border_1()
2468 .border_color(
2469 cx.theme()
2470 .table_active_border,
2471 ),
2472 )
2473 })
2474 .when(
2475 is_cell_right_clicked
2476 && !is_cell_selected,
2477 |this| {
2478 this.child(
2479 div()
2480 .absolute()
2481 .inset_0()
2482 .border_1()
2483 .border_color(
2484 cx.theme()
2485 .table_active_border
2486 .opacity(0.5),
2487 ),
2488 )
2489 },
2490 )
2491 .when(table.cell_selectable, |this| {
2492 this.on_click(cx.listener(
2493 move |table, e, window, cx| {
2494 cx.stop_propagation();
2495 table.on_cell_click(
2496 e, row_ix, col_ix, window,
2497 cx,
2498 );
2499 },
2500 ))
2501 .on_mouse_down(
2502 MouseButton::Right,
2503 cx.listener(
2504 move |table, e, window, cx| {
2505 table.on_cell_right_click(
2506 e, row_ix, col_ix,
2507 window, cx,
2508 );
2509 },
2510 ),
2511 )
2512 }),
2513 );
2514
2515 items.push(el);
2516 });
2517
2518 items
2519 }
2520 },
2521 )
2522 .with_scroll_handle(&self.horizontal_scroll_handle),
2523 )
2524 .child(self.delegate.render_last_empty_col(window, cx)),
2525 )
2526 .when(scrollable_cols.end < self.col_groups.len(), |this| {
2527 this.child(self.render_fixed_cells(
2528 row_ix,
2529 scrollable_cols.end..self.col_groups.len(),
2530 ColumnFixed::Right,
2531 window,
2532 cx,
2533 ))
2534 })
2535 .when(is_selected, |this| {
2537 let bg = if cx.theme().list.active_highlight {
2538 cx.theme().tokens.table_active
2539 } else {
2540 cx.theme().tokens.accent
2541 };
2542 this.bg(bg)
2543 })
2544 .when(self.right_clicked_row == Some(row_ix), |this| {
2546 this.border_color(gpui::transparent_white()).child(
2547 div()
2548 .top(if row_ix == 0 { px(0.) } else { px(-1.) })
2549 .left(px(0.))
2550 .right(px(0.))
2551 .bottom(px(-1.))
2552 .absolute()
2553 .border_1()
2554 .border_color(cx.theme().selection),
2555 )
2556 })
2557 .on_mouse_down(
2558 MouseButton::Right,
2559 cx.listener(move |this, e, window, cx| {
2560 this.on_row_right_click(e, Some(row_ix), window, cx);
2561 }),
2562 )
2563 .on_click(cx.listener(move |this, e, window, cx| {
2564 this.on_row_left_click(e, row_ix, window, cx);
2565 }))
2566 .into_any_element()
2567 } else {
2568 let cols_width: Pixels = self
2569 .col_groups
2570 .iter()
2571 .take(columns_count)
2572 .map(|col_group| col_group.width)
2573 .sum();
2574
2575 self.delegate
2577 .render_tr(row_ix, window, cx)
2578 .h_flex()
2579 .w_full()
2580 .h(row_height)
2581 .border_b_1()
2582 .border_color(cx.theme().table_row_border)
2583 .when(is_stripe_row, |this| this.bg(cx.theme().tokens.table_even))
2584 .when(self.cell_selectable && self.row_header, |this| {
2585 this.child(
2587 div()
2588 .w(px(40.))
2589 .h_full()
2590 .flex_shrink_0()
2591 .table_cell_size(self.options.size),
2592 )
2593 })
2594 .child(div().flex_shrink_0().h_full().w(cols_width))
2598 .child(self.delegate.render_last_empty_col(window, cx))
2599 .into_any_element()
2600 }
2601 }
2602
2603 fn render_fixed_cells(
2605 &mut self,
2606 row_ix: usize,
2607 cols: Range<usize>,
2608 side: ColumnFixed,
2609 window: &mut Window,
2610 cx: &mut Context<Self>,
2611 ) -> impl IntoElement {
2612 let items: Vec<_> = cols
2613 .map(|col_ix| {
2614 let is_cell_selected = self.selected_cell() == Some((row_ix, col_ix));
2615 let is_cell_right_clicked = self.right_clicked_cell == Some((row_ix, col_ix));
2616
2617 self.render_col_wrap(Some(row_ix), col_ix, window, cx)
2618 .child(
2619 self.a11y_cell(
2620 self.render_cell(Some(row_ix), col_ix, window, cx)
2621 .id(format!("table-cell:{}:{}", row_ix, col_ix))
2622 .test_support(),
2623 row_ix,
2624 col_ix,
2625 window,
2626 cx,
2627 )
2628 .relative()
2629 .child(self.measure_render_td(row_ix, col_ix, window, cx))
2630 .when(is_cell_selected, |this| {
2631 this.child(
2632 div()
2633 .absolute()
2634 .inset_0()
2635 .bg(cx.theme().tokens.table_active)
2636 .border_1()
2637 .border_color(cx.theme().table_active_border),
2638 )
2639 })
2640 .when(is_cell_right_clicked && !is_cell_selected, |this| {
2641 this.child(
2642 div()
2643 .absolute()
2644 .inset_0()
2645 .border_1()
2646 .border_color(cx.theme().table_active_border.opacity(0.5)),
2647 )
2648 })
2649 .when(self.cell_selectable, |this| {
2650 this.on_click(cx.listener(move |table, e, window, cx| {
2651 table.on_cell_click(e, row_ix, col_ix, window, cx);
2652 }))
2653 .on_mouse_down(
2654 MouseButton::Right,
2655 cx.listener(move |table, e, window, cx| {
2656 table.on_cell_right_click(e, row_ix, col_ix, window, cx);
2657 }),
2658 )
2659 }),
2660 )
2661 })
2662 .collect();
2663
2664 h_flex()
2665 .relative()
2666 .h_full()
2667 .flex_shrink_0()
2668 .children(items)
2669 .children(self.render_fixed_cols_border(side, cx))
2670 }
2671
2672 fn calculate_extra_rows_needed(
2674 &self,
2675 total_height: Pixels,
2676 actual_height: Pixels,
2677 row_height: Pixels,
2678 ) -> usize {
2679 let mut extra_rows_needed = 0;
2680
2681 let remaining_height = total_height - actual_height;
2682 if remaining_height > px(0.) {
2683 extra_rows_needed = (remaining_height / row_height).floor() as usize;
2684 }
2685
2686 extra_rows_needed
2687 }
2688
2689 #[inline]
2690 fn measure_render_td(
2691 &mut self,
2692 row_ix: usize,
2693 col_ix: usize,
2694 window: &mut Window,
2695 cx: &mut Context<Self>,
2696 ) -> impl IntoElement {
2697 if !crate::measure_enable() {
2698 return self
2699 .delegate
2700 .render_td(row_ix, col_ix, window, cx)
2701 .into_any_element();
2702 }
2703
2704 let start = std::time::Instant::now();
2705 let el = self.delegate.render_td(row_ix, col_ix, window, cx);
2706 self._measure.push(start.elapsed());
2707 el.into_any_element()
2708 }
2709
2710 fn measure(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
2711 if !crate::measure_enable() {
2712 return;
2713 }
2714
2715 if self._measure.len() > 0 {
2717 let total = self
2718 ._measure
2719 .iter()
2720 .fold(Duration::default(), |acc, d| acc + *d);
2721 let avg = total / self._measure.len() as u32;
2722 eprintln!(
2723 "last render {} cells total: {:?}, avg: {:?}",
2724 self._measure.len(),
2725 total,
2726 avg,
2727 );
2728 }
2729 self._measure.clear();
2730 }
2731
2732 fn render_vertical_scrollbar(
2733 &mut self,
2734 _: &mut Window,
2735 _: &mut Context<Self>,
2736 ) -> Option<impl IntoElement> {
2737 Some(
2738 div()
2739 .absolute()
2740 .top(self.header_band_height())
2741 .right_0()
2742 .bottom_0()
2743 .w(Scrollbar::width())
2744 .child(
2745 Scrollbar::vertical(&self.vertical_scroll_handle)
2746 .viewport_from_layout()
2747 .max_fps(60),
2748 ),
2749 )
2750 }
2751
2752 fn render_horizontal_scrollbar(
2753 &mut self,
2754 _: &mut Window,
2755 _: &mut Context<Self>,
2756 ) -> impl IntoElement {
2757 div()
2758 .absolute()
2759 .left(self.fixed_head_cols_bounds.size.width)
2760 .right(self.fixed_right_head_cols_bounds.size.width)
2761 .bottom_0()
2762 .h(Scrollbar::width())
2763 .child(Scrollbar::horizontal(&self.horizontal_scroll_handle).viewport_from_layout())
2764 }
2765}
2766
2767impl<D> Focusable for TableState<D>
2768where
2769 D: TableDelegate,
2770{
2771 fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
2772 self.focus_handle.clone()
2773 }
2774}
2775impl<D> EventEmitter<TableEvent> for TableState<D> where D: TableDelegate {}
2776
2777impl<D> Render for TableState<D>
2778where
2779 D: TableDelegate,
2780{
2781 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2782 self.measure(window, cx);
2783
2784 let columns_count = self.delegate.columns_count(cx);
2785 let scrollable_cols = self.scrollable_cols();
2786 let rows_count = self.delegate.rows_count(cx);
2787 let loading = self.delegate.loading(cx);
2788
2789 let row_height = self.options.size.table_row_height();
2790 let total_height = self
2791 .vertical_scroll_handle
2792 .0
2793 .borrow()
2794 .base_handle
2795 .bounds()
2796 .size
2797 .height;
2798 let actual_height = row_height * rows_count as f32;
2799 let extra_rows_count =
2800 self.calculate_extra_rows_needed(total_height, actual_height, row_height);
2801 let render_rows_count = if self.options.stripe {
2802 rows_count + extra_rows_count
2803 } else {
2804 rows_count
2805 };
2806 let has_right_click = self.right_clicked_row.is_some() || self.right_clicked_cell.is_some();
2807 let is_filled = total_height > Pixels::ZERO && total_height <= actual_height;
2808
2809 let loading_view = if loading {
2810 Some(
2811 self.delegate
2812 .render_loading(self.options.size, window, cx)
2813 .into_any_element(),
2814 )
2815 } else {
2816 None
2817 };
2818
2819 let empty_view = if rows_count == 0 {
2820 self.update_visible_range_if_need(0..0, 0, Axis::Vertical, window, cx);
2822 Some(
2823 div()
2824 .size_full()
2825 .child(self.delegate.render_empty(window, cx))
2826 .into_any_element(),
2827 )
2828 } else {
2829 None
2830 };
2831
2832 let inner_table = v_flex()
2833 .id("table-inner")
2834 .test_support()
2835 .size_full()
2836 .overflow_hidden()
2837 .child(self.render_table_header(scrollable_cols.clone(), window, cx))
2838 .context_menu({
2839 let view = cx.entity().clone();
2840 move |this, window: &mut Window, cx: &mut Context<PopupMenu>| {
2841 if let Some(row_ix) = view.read(cx).right_clicked_row {
2842 view.update(cx, |menu, cx| {
2843 menu.delegate_mut().context_menu(row_ix, this, window, cx)
2844 })
2845 } else {
2846 this
2847 }
2848 }
2849 })
2850 .map(|this| {
2851 if rows_count == 0 {
2852 this.children(empty_view)
2853 } else {
2854 this.child(
2855 h_flex().id("table-body").flex_grow_1().size_full().child(
2856 uniform_list(
2857 "table-uniform-list",
2858 render_rows_count,
2859 cx.processor(
2860 move |table, visible_range: Range<usize>, window, cx| {
2861 let col_sizes: Rc<Vec<gpui::Size<Pixels>>> = Rc::new(
2865 table
2866 .col_groups
2867 .get(scrollable_cols.clone())
2868 .unwrap_or_default()
2869 .iter()
2870 .map(|col| gpui::Size {
2871 width: col.width,
2872 height: px(0.),
2873 })
2874 .collect(),
2875 );
2876
2877 table.load_more_if_need(
2878 rows_count,
2879 visible_range.end,
2880 window,
2881 cx,
2882 );
2883 table.update_visible_range_if_need(
2884 visible_range.clone(),
2885 rows_count,
2886 Axis::Vertical,
2887 window,
2888 cx,
2889 );
2890
2891 if visible_range.end > rows_count {
2892 table.scroll_to_row(
2893 std::cmp::min(
2894 visible_range.start,
2895 rows_count.saturating_sub(1),
2896 ),
2897 cx,
2898 );
2899 }
2900
2901 let mut items = Vec::with_capacity(
2902 visible_range.end.saturating_sub(visible_range.start),
2903 );
2904
2905 visible_range.for_each(|row_ix| {
2907 items.push(table.render_table_row(
2909 row_ix,
2910 rows_count,
2911 scrollable_cols.clone(),
2912 col_sizes.clone(),
2913 columns_count,
2914 is_filled,
2915 window,
2916 cx,
2917 ));
2918 });
2919
2920 items
2921 },
2922 ),
2923 )
2924 .flex_grow_1()
2925 .size_full()
2926 .with_sizing_behavior(ListSizingBehavior::Auto)
2927 .track_scroll(&self.vertical_scroll_handle)
2928 .map(|rows| {
2929 gpui_base::ScrollBounce::new(
2930 "table-scroll-bounce",
2931 &self.vertical_scroll_handle,
2932 rows,
2933 )
2934 .when_some(self.options.scroll_bounce, |bounce, on| {
2935 bounce.enabled(on)
2936 })
2937 })
2938 .into_any_element(),
2939 ),
2940 )
2941 }
2942 });
2943
2944 div()
2945 .size_full()
2946 .children(loading_view)
2947 .when(!loading, |this| {
2948 this.child(inner_table)
2949 .child(ScrollableMask::new(
2950 Axis::Horizontal,
2951 &self.horizontal_scroll_handle,
2952 ))
2953 .when(rows_count > 0, |this| {
2958 this.child(ScrollableMask::new(
2959 Axis::Vertical,
2960 &self.vertical_scroll_handle.0.borrow().base_handle,
2961 ))
2962 })
2963 .when(has_right_click, |this| {
2964 this.on_mouse_down_out(cx.listener(|this, e, window, cx| {
2965 this.on_row_right_click(e, None, window, cx);
2966 cx.notify();
2967 }))
2968 })
2969 })
2970 .on_prepaint({
2971 let state = cx.entity();
2972 move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
2973 })
2974 .when(!window.is_inspector_picking(cx), |this| {
2975 this.child(
2976 div()
2977 .absolute()
2978 .top_0()
2979 .size_full()
2980 .when(self.options.scrollbar_visible.bottom, |this| {
2981 this.child(self.render_horizontal_scrollbar(window, cx))
2982 })
2983 .when(
2984 self.options.scrollbar_visible.right && rows_count > 0,
2985 |this| this.children(self.render_vertical_scrollbar(window, cx)),
2986 ),
2987 )
2988 })
2989 }
2990}
2991
2992#[cfg(test)]
2993mod tests {
2994 use gpui::{App, Entity, TestAppContext};
2995
2996 use super::*;
2997
2998 struct WidthsDelegate;
2999
3000 struct BounceDelegate;
3001
3002 impl TableDelegate for BounceDelegate {
3003 fn columns_count(&self, _: &App) -> usize {
3004 1
3005 }
3006 fn rows_count(&self, _: &App) -> usize {
3007 40
3008 }
3009 fn column(&self, _: usize, _: &App) -> Column {
3010 Column::new("value", "Value")
3011 }
3012 fn render_td(
3013 &mut self,
3014 row: usize,
3015 _: usize,
3016 _: &mut Window,
3017 _: &mut Context<TableState<Self>>,
3018 ) -> impl IntoElement {
3019 div().child(row.to_string())
3020 }
3021 }
3022
3023 struct BounceTable(Entity<TableState<BounceDelegate>>);
3024
3025 impl Render for BounceTable {
3026 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3027 div()
3028 .w(px(240.))
3029 .h(px(200.))
3030 .child(DataTable::new(&self.0).scroll_bounce(true))
3031 }
3032 }
3033
3034 #[gpui::test]
3035 fn scroll_bounce_table_keeps_headers_fixed_and_accepts_new_wheel_input(
3036 cx: &mut TestAppContext,
3037 ) {
3038 cx.update(crate::init);
3039 let (view, cx) = cx.add_window_view(|window, cx| {
3040 BounceTable(cx.new(|cx| TableState::new(BounceDelegate, window, cx)))
3041 });
3042 cx.update(|window, cx| window.draw(cx).clear(cx));
3043 let state = view.read_with(cx, |view, _| view.0.clone());
3044 let header = state.read_with(cx, |state, _| state.col_groups[0].bounds);
3045 let wheel = |cx: &mut gpui::VisualTestContext, dy| {
3046 cx.simulate_event(gpui::ScrollEvent {
3047 position: gpui::point(px(100.), px(100.)),
3048 delta: gpui::ScrollDelta::Lines(gpui::point(0., dy)),
3049 ..Default::default()
3050 });
3051 };
3052 for (dy, phase) in [
3053 (-10000., gpui::TouchPhase::Started),
3054 (0., gpui::TouchPhase::Ended),
3055 ] {
3056 cx.simulate_event(gpui::ScrollEvent {
3057 position: gpui::point(px(100.), px(100.)),
3058 delta: gpui::ScrollDelta::Pixels(gpui::point(px(0.), px(dy))),
3059 touch_phase: phase,
3060 ..Default::default()
3061 });
3062 }
3063 cx.update(|window, cx| window.draw(cx).clear(cx));
3064 let end = state.read_with(cx, |state, _| {
3065 state
3066 .vertical_scroll_handle
3067 .0
3068 .borrow()
3069 .base_handle
3070 .offset()
3071 .y
3072 });
3073 assert!(end < px(0.));
3074 cx.executor().advance_clock(Duration::from_millis(200));
3075 cx.update(|window, cx| window.draw(cx).clear(cx));
3076 assert_eq!(
3077 state.read_with(cx, |state, _| state.col_groups[0].bounds),
3078 header
3079 );
3080 wheel(cx, 2.);
3081 assert!(
3082 state.read_with(cx, |state, _| state
3083 .vertical_scroll_handle
3084 .0
3085 .borrow()
3086 .base_handle
3087 .offset()
3088 .y)
3089 > end
3090 );
3091 }
3092
3093 impl TableDelegate for WidthsDelegate {
3094 fn columns_count(&self, _: &App) -> usize {
3095 6
3096 }
3097
3098 fn rows_count(&self, _: &App) -> usize {
3099 0
3100 }
3101
3102 fn column(&self, col_ix: usize, _: &App) -> Column {
3103 match col_ix {
3104 0 => Column::new("narrow", "Narrow")
3105 .min_width(px(80.))
3106 .width(px(40.)),
3107 1 => Column::new("wide", "Wide")
3108 .max_width(px(120.))
3109 .width(px(300.)),
3110 2 => Column::new("tiny", "Tiny").width(px(10.)),
3112 3 => Column::new("wide_default", "Wide Default").width(px(2000.)),
3114 4 => Column::new("wide_explicit", "Wide Explicit")
3116 .max_width(px(2400.))
3117 .width(px(2000.)),
3118 _ => Column::new("inverted", "Inverted")
3120 .min_width(px(200.))
3121 .max_width(px(100.)),
3122 }
3123 }
3124
3125 fn render_td(
3126 &mut self,
3127 _: usize,
3128 _: usize,
3129 _: &mut Window,
3130 _: &mut Context<TableState<Self>>,
3131 ) -> impl IntoElement {
3132 div()
3133 }
3134 }
3135
3136 #[gpui::test]
3137 fn column_resize_bounds_preserve_initial_widths(cx: &mut TestAppContext) {
3138 cx.skip_drawing();
3139 let window = cx.add_window(|window, cx| TableState::new(WidthsDelegate, window, cx));
3140
3141 window
3142 .update(cx, |table, window, cx| {
3143 let widths = |table: &TableState<WidthsDelegate>| {
3144 table
3145 .col_groups
3146 .iter()
3147 .map(|col_group| col_group.width)
3148 .collect::<Vec<_>>()
3149 };
3150 assert_eq!(
3151 widths(table),
3152 [px(40.), px(300.), px(10.), px(2000.), px(2000.), px(100.)]
3153 );
3154
3155 for ix in 0..table.col_groups.len() {
3156 table.resize_cols(ix, px(10000.), window, cx);
3157 }
3158 assert_eq!(
3159 widths(table),
3160 [
3161 px(1200.),
3162 px(120.),
3163 px(1200.),
3164 px(1200.),
3165 px(2400.),
3166 px(100.)
3167 ]
3168 );
3169
3170 for ix in 0..table.col_groups.len() {
3171 table.resize_cols(ix, px(-100.), window, cx);
3172 }
3173 assert_eq!(
3174 widths(table),
3175 [px(80.), px(20.), px(20.), px(20.), px(20.), px(100.)]
3176 );
3177 })
3178 .unwrap();
3179 }
3180}