1use std::{ops::Range, rc::Rc, time::Duration};
2
3use crate::{
4 actions::{Cancel, SelectDown, SelectUp},
5 h_flex,
6 menu::{ContextMenuExt, PopupMenu},
7 scroll::{ScrollableMask, Scrollbar, ScrollbarState},
8 v_flex, ActiveTheme, Icon, IconName, StyleSized as _, StyledExt, VirtualListScrollHandle,
9};
10use gpui::{
11 canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Axis, Bounds, ClickEvent,
12 Context, Div, DragMoveEvent, EventEmitter, FocusHandle, Focusable, InteractiveElement,
13 IntoElement, ListSizingBehavior, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
14 Render, ScrollStrategy, SharedString, StatefulInteractiveElement as _, Styled, Task,
15 UniformListScrollHandle, Window,
16};
17
18use super::*;
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21enum SelectionState {
22 Column,
23 Row,
24}
25
26#[derive(Clone)]
28pub enum TableEvent {
29 SelectRow(usize),
31 DoubleClickedRow(usize),
33 SelectColumn(usize),
35 ColumnWidthsChanged(Vec<Pixels>),
39 MoveColumn(usize, usize),
44}
45
46#[derive(Debug, Default)]
48pub struct TableVisibleRange {
49 rows: Range<usize>,
51 cols: Range<usize>,
53}
54
55impl TableVisibleRange {
56 pub fn rows(&self) -> &Range<usize> {
58 &self.rows
59 }
60
61 pub fn cols(&self) -> &Range<usize> {
63 &self.cols
64 }
65}
66
67pub struct TableState<D: TableDelegate> {
69 focus_handle: FocusHandle,
70 delegate: D,
71 pub(super) options: TableOptions,
72 bounds: Bounds<Pixels>,
74 fixed_head_cols_bounds: Bounds<Pixels>,
76
77 col_groups: Vec<ColGroup>,
78
79 pub loop_selection: bool,
83 pub col_selectable: bool,
85 pub row_selectable: bool,
87 pub sortable: bool,
89 pub col_resizable: bool,
91 pub col_movable: bool,
93 pub col_fixed: bool,
95
96 pub vertical_scroll_handle: UniformListScrollHandle,
97 pub vertical_scroll_state: ScrollbarState,
98 pub horizontal_scroll_handle: VirtualListScrollHandle,
99 pub horizontal_scroll_state: ScrollbarState,
100
101 selected_row: Option<usize>,
102 selection_state: SelectionState,
103 right_clicked_row: Option<usize>,
104 selected_col: Option<usize>,
105
106 resizing_col: Option<usize>,
108
109 visible_range: TableVisibleRange,
111
112 _measure: Vec<Duration>,
113 _load_more_task: Task<()>,
114}
115
116impl<D> TableState<D>
117where
118 D: TableDelegate,
119{
120 pub fn new(delegate: D, _: &mut Window, cx: &mut Context<Self>) -> Self {
122 let mut this = Self {
123 focus_handle: cx.focus_handle(),
124 options: TableOptions::default(),
125 delegate,
126 col_groups: Vec::new(),
127 horizontal_scroll_handle: VirtualListScrollHandle::new(),
128 vertical_scroll_handle: UniformListScrollHandle::new(),
129 vertical_scroll_state: ScrollbarState::default(),
130 horizontal_scroll_state: ScrollbarState::default(),
131 selection_state: SelectionState::Row,
132 selected_row: None,
133 right_clicked_row: None,
134 selected_col: None,
135 resizing_col: None,
136 bounds: Bounds::default(),
137 fixed_head_cols_bounds: Bounds::default(),
138 visible_range: TableVisibleRange::default(),
139 loop_selection: true,
140 col_selectable: true,
141 row_selectable: true,
142 sortable: true,
143 col_movable: true,
144 col_resizable: true,
145 col_fixed: true,
146 _load_more_task: Task::ready(()),
147 _measure: Vec::new(),
148 };
149
150 this.prepare_col_groups(cx);
151 this
152 }
153
154 pub fn delegate(&self) -> &D {
156 &self.delegate
157 }
158
159 pub fn delegate_mut(&mut self) -> &mut D {
161 &mut self.delegate
162 }
163
164 pub fn loop_selection(mut self, loop_selection: bool) -> Self {
166 self.loop_selection = loop_selection;
167 self
168 }
169
170 pub fn col_movable(mut self, col_movable: bool) -> Self {
172 self.col_movable = col_movable;
173 self
174 }
175
176 pub fn col_resizable(mut self, col_resizable: bool) -> Self {
178 self.col_resizable = col_resizable;
179 self
180 }
181
182 pub fn sortable(mut self, sortable: bool) -> Self {
184 self.sortable = sortable;
185 self
186 }
187
188 pub fn row_selectable(mut self, row_selectable: bool) -> Self {
190 self.row_selectable = row_selectable;
191 self
192 }
193
194 pub fn col_selectable(mut self, col_selectable: bool) -> Self {
196 self.col_selectable = col_selectable;
197 self
198 }
199
200 pub fn refresh(&mut self, cx: &mut Context<Self>) {
202 self.prepare_col_groups(cx);
203 }
204
205 pub fn scroll_to_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
207 self.vertical_scroll_handle
208 .scroll_to_item(row_ix, ScrollStrategy::Top);
209 cx.notify();
210 }
211
212 pub fn scroll_to_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
214 let col_ix = col_ix.saturating_sub(self.fixed_left_cols_count());
215
216 self.horizontal_scroll_handle
217 .scroll_to_item(col_ix, ScrollStrategy::Top);
218 cx.notify();
219 }
220
221 pub fn selected_row(&self) -> Option<usize> {
223 self.selected_row
224 }
225
226 pub fn set_selected_row(&mut self, row_ix: usize, cx: &mut Context<Self>) {
228 let is_down = match self.selected_row {
229 Some(selected_row) => row_ix > selected_row,
230 None => true,
231 };
232
233 self.selection_state = SelectionState::Row;
234 self.right_clicked_row = None;
235 self.selected_row = Some(row_ix);
236 if let Some(row_ix) = self.selected_row {
237 self.vertical_scroll_handle.scroll_to_item(
238 row_ix,
239 if is_down {
240 ScrollStrategy::Bottom
241 } else {
242 ScrollStrategy::Top
243 },
244 );
245 }
246 cx.emit(TableEvent::SelectRow(row_ix));
247 cx.notify();
248 }
249
250 pub fn selected_col(&self) -> Option<usize> {
252 self.selected_col
253 }
254
255 pub fn set_selected_col(&mut self, col_ix: usize, cx: &mut Context<Self>) {
257 self.selection_state = SelectionState::Column;
258 self.selected_col = Some(col_ix);
259 if let Some(col_ix) = self.selected_col {
260 self.scroll_to_col(col_ix, cx);
261 }
262 cx.emit(TableEvent::SelectColumn(col_ix));
263 cx.notify();
264 }
265
266 pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
268 self.selection_state = SelectionState::Row;
269 self.selected_row = None;
270 self.selected_col = None;
271 cx.notify();
272 }
273
274 pub fn visible_range(&self) -> &TableVisibleRange {
278 &self.visible_range
279 }
280
281 fn prepare_col_groups(&mut self, cx: &mut Context<Self>) {
282 self.col_groups = (0..self.delegate.columns_count(cx))
283 .map(|col_ix| {
284 let column = self.delegate().column(col_ix, cx);
285 ColGroup {
286 width: column.width,
287 bounds: Bounds::default(),
288 column: column.clone(),
289 }
290 })
291 .collect();
292 cx.notify();
293 }
294
295 fn fixed_left_cols_count(&self) -> usize {
296 if !self.col_fixed {
297 return 0;
298 }
299
300 self.col_groups
301 .iter()
302 .filter(|col| col.column.fixed == Some(ColumnFixed::Left))
303 .count()
304 }
305
306 fn on_row_right_click(
307 &mut self,
308 _: &MouseDownEvent,
309 row_ix: usize,
310 _: &mut Window,
311 _: &mut Context<Self>,
312 ) {
313 self.right_clicked_row = Some(row_ix);
314 }
315
316 fn on_row_left_click(
317 &mut self,
318 e: &ClickEvent,
319 row_ix: usize,
320 _: &mut Window,
321 cx: &mut Context<Self>,
322 ) {
323 self.set_selected_row(row_ix, cx);
324
325 if e.click_count() == 2 {
326 cx.emit(TableEvent::DoubleClickedRow(row_ix));
327 }
328 }
329
330 fn on_col_head_click(&mut self, col_ix: usize, _: &mut Window, cx: &mut Context<Self>) {
331 if !self.col_selectable {
332 return;
333 }
334
335 let Some(col_group) = self.col_groups.get(col_ix) else {
336 return;
337 };
338
339 if !col_group.column.selectable {
340 return;
341 }
342
343 self.set_selected_col(col_ix, cx)
344 }
345
346 fn has_selection(&self) -> bool {
347 self.selected_row.is_some() || self.selected_col.is_some()
348 }
349
350 pub(super) fn action_cancel(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
351 if self.has_selection() {
352 self.clear_selection(cx);
353 return;
354 }
355 cx.propagate();
356 }
357
358 pub(super) fn action_select_prev(
359 &mut self,
360 _: &SelectUp,
361 _: &mut Window,
362 cx: &mut Context<Self>,
363 ) {
364 let rows_count = self.delegate.rows_count(cx);
365 if rows_count < 1 {
366 return;
367 }
368
369 let mut selected_row = self.selected_row.unwrap_or(0);
370 if selected_row > 0 {
371 selected_row = selected_row.saturating_sub(1);
372 } else {
373 if self.loop_selection {
374 selected_row = rows_count.saturating_sub(1);
375 }
376 }
377
378 self.set_selected_row(selected_row, cx);
379 }
380
381 pub(super) fn action_select_next(
382 &mut self,
383 _: &SelectDown,
384 _: &mut Window,
385 cx: &mut Context<Self>,
386 ) {
387 let rows_count = self.delegate.rows_count(cx);
388 if rows_count < 1 {
389 return;
390 }
391
392 let selected_row = match self.selected_row {
393 Some(selected_row) if selected_row < rows_count.saturating_sub(1) => selected_row + 1,
394 Some(selected_row) => {
395 if self.loop_selection {
396 0
397 } else {
398 selected_row
399 }
400 }
401 _ => 0,
402 };
403
404 self.set_selected_row(selected_row, cx);
405 }
406
407 pub(super) fn action_select_prev_col(
408 &mut self,
409 _: &SelectPrevColumn,
410 _: &mut Window,
411 cx: &mut Context<Self>,
412 ) {
413 let mut selected_col = self.selected_col.unwrap_or(0);
414 let columns_count = self.delegate.columns_count(cx);
415 if selected_col > 0 {
416 selected_col = selected_col.saturating_sub(1);
417 } else {
418 if self.loop_selection {
419 selected_col = columns_count.saturating_sub(1);
420 }
421 }
422 self.set_selected_col(selected_col, cx);
423 }
424
425 pub(super) fn action_select_next_col(
426 &mut self,
427 _: &SelectNextColumn,
428 _: &mut Window,
429 cx: &mut Context<Self>,
430 ) {
431 let mut selected_col = self.selected_col.unwrap_or(0);
432 if selected_col < self.delegate.columns_count(cx).saturating_sub(1) {
433 selected_col += 1;
434 } else {
435 if self.loop_selection {
436 selected_col = 0;
437 }
438 }
439
440 self.set_selected_col(selected_col, cx);
441 }
442
443 fn scroll_table_by_col_resizing(
445 &mut self,
446 mouse_position: Point<Pixels>,
447 col_group: &ColGroup,
448 ) {
449 if mouse_position.x > self.bounds.right() {
451 return;
452 }
453
454 let mut offset = self.horizontal_scroll_handle.offset();
455 let col_bounds = col_group.bounds;
456
457 if mouse_position.x < self.bounds.left()
458 && col_bounds.right() < self.bounds.left() + px(20.)
459 {
460 offset.x += px(1.);
461 } else if mouse_position.x > self.bounds.right()
462 && col_bounds.right() > self.bounds.right() - px(20.)
463 {
464 offset.x -= px(1.);
465 }
466
467 self.horizontal_scroll_handle.set_offset(offset);
468 }
469
470 fn resize_cols(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
473 if !self.col_resizable {
474 return;
475 }
476
477 const MIN_WIDTH: Pixels = px(10.0);
478 const MAX_WIDTH: Pixels = px(1200.0);
479 let Some(col_group) = self.col_groups.get_mut(ix) else {
480 return;
481 };
482
483 if !col_group.is_resizable() {
484 return;
485 }
486 let size = size.floor();
487
488 let old_width = col_group.width;
489 let new_width = size;
490 if new_width < MIN_WIDTH {
491 return;
492 }
493 let changed_width = new_width - old_width;
494 if changed_width > px(-1.0) && changed_width < px(1.0) {
496 return;
497 }
498 col_group.width = new_width.min(MAX_WIDTH);
499
500 cx.notify();
501 }
502
503 fn perform_sort(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
504 if !self.sortable {
505 return;
506 }
507
508 let sort = self.col_groups.get(col_ix).and_then(|g| g.column.sort);
509 if sort.is_none() {
510 return;
511 }
512
513 let sort = sort.unwrap();
514 let sort = match sort {
515 ColumnSort::Ascending => ColumnSort::Default,
516 ColumnSort::Descending => ColumnSort::Ascending,
517 ColumnSort::Default => ColumnSort::Descending,
518 };
519
520 for (ix, col_group) in self.col_groups.iter_mut().enumerate() {
521 if ix == col_ix {
522 col_group.column.sort = Some(sort);
523 } else {
524 if col_group.column.sort.is_some() {
525 col_group.column.sort = Some(ColumnSort::Default);
526 }
527 }
528 }
529
530 self.delegate_mut().perform_sort(col_ix, sort, window, cx);
531
532 cx.notify();
533 }
534
535 fn move_column(
536 &mut self,
537 col_ix: usize,
538 to_ix: usize,
539 window: &mut Window,
540 cx: &mut Context<Self>,
541 ) {
542 if col_ix == to_ix {
543 return;
544 }
545
546 self.delegate.move_column(col_ix, to_ix, window, cx);
547 let col_group = self.col_groups.remove(col_ix);
548 self.col_groups.insert(to_ix, col_group);
549
550 cx.emit(TableEvent::MoveColumn(col_ix, to_ix));
551 cx.notify();
552 }
553
554 fn load_more_if_need(
556 &mut self,
557 rows_count: usize,
558 visible_end: usize,
559 window: &mut Window,
560 cx: &mut Context<Self>,
561 ) {
562 let threshold = self.delegate.load_more_threshold();
563 if visible_end >= rows_count.saturating_sub(threshold) {
565 if !self.delegate.is_eof(cx) {
566 return;
567 }
568
569 self._load_more_task = cx.spawn_in(window, async move |view, window| {
570 _ = view.update_in(window, |view, window, cx| {
571 view.delegate.load_more(window, cx);
572 });
573 });
574 }
575 }
576
577 fn update_visible_range_if_need(
578 &mut self,
579 visible_range: Range<usize>,
580 axis: Axis,
581 window: &mut Window,
582 cx: &mut Context<Self>,
583 ) {
584 if visible_range.len() <= 1 {
587 return;
588 }
589
590 if axis == Axis::Vertical {
591 if self.visible_range.rows == visible_range {
592 return;
593 }
594 self.delegate_mut()
595 .visible_rows_changed(visible_range.clone(), window, cx);
596 self.visible_range.rows = visible_range;
597 } else {
598 if self.visible_range.cols == visible_range {
599 return;
600 }
601 self.delegate_mut()
602 .visible_columns_changed(visible_range.clone(), window, cx);
603 self.visible_range.cols = visible_range;
604 }
605 }
606
607 fn render_cell(&self, col_ix: usize, _window: &mut Window, _cx: &mut Context<Self>) -> Div {
608 let Some(col_group) = self.col_groups.get(col_ix) else {
609 return div();
610 };
611
612 let col_width = col_group.width;
613 let col_padding = col_group.column.paddings;
614
615 div()
616 .w(col_width)
617 .h_full()
618 .flex_shrink_0()
619 .overflow_hidden()
620 .whitespace_nowrap()
621 .table_cell_size(self.options.size)
622 .map(|this| match col_padding {
623 Some(padding) => this
624 .pl(padding.left)
625 .pr(padding.right)
626 .pt(padding.top)
627 .pb(padding.bottom),
628 None => this,
629 })
630 }
631
632 fn render_col_wrap(&self, col_ix: usize, _: &mut Window, cx: &mut Context<Self>) -> Div {
634 let el = h_flex().h_full();
635 let selectable = self.col_selectable
636 && self
637 .col_groups
638 .get(col_ix)
639 .map(|col_group| col_group.column.selectable)
640 .unwrap_or(false);
641
642 if selectable
643 && self.selected_col == Some(col_ix)
644 && self.selection_state == SelectionState::Column
645 {
646 el.bg(cx.theme().table_active)
647 } else {
648 el
649 }
650 }
651
652 fn render_resize_handle(
653 &self,
654 ix: usize,
655 _: &mut Window,
656 cx: &mut Context<Self>,
657 ) -> impl IntoElement {
658 const HANDLE_SIZE: Pixels = px(2.);
659
660 let resizable = self.col_resizable
661 && self
662 .col_groups
663 .get(ix)
664 .map(|col| col.is_resizable())
665 .unwrap_or(false);
666 if !resizable {
667 return div().into_any_element();
668 }
669
670 let group_id = SharedString::from(format!("resizable-handle:{}", ix));
671
672 h_flex()
673 .id(("resizable-handle", ix))
674 .group(group_id.clone())
675 .occlude()
676 .cursor_col_resize()
677 .h_full()
678 .w(HANDLE_SIZE)
679 .ml(-(HANDLE_SIZE))
680 .justify_end()
681 .items_center()
682 .child(
683 div()
684 .h_full()
685 .justify_center()
686 .bg(cx.theme().table_row_border)
687 .group_hover(group_id, |this| this.bg(cx.theme().border).h_full())
688 .w(px(1.)),
689 )
690 .on_drag_move(
691 cx.listener(move |view, e: &DragMoveEvent<ResizeColumn>, window, cx| {
692 match e.drag(cx) {
693 ResizeColumn((entity_id, ix)) => {
694 if cx.entity_id() != *entity_id {
695 return;
696 }
697
698 let ix = *ix;
705 view.resizing_col = Some(ix);
706
707 let col_group = view
708 .col_groups
709 .get(ix)
710 .expect("BUG: invalid col index")
711 .clone();
712
713 view.resize_cols(
714 ix,
715 e.event.position.x - HANDLE_SIZE - col_group.bounds.left(),
716 window,
717 cx,
718 );
719
720 view.scroll_table_by_col_resizing(e.event.position, &col_group);
722 }
723 };
724 }),
725 )
726 .on_drag(ResizeColumn((cx.entity_id(), ix)), |drag, _, _, cx| {
727 cx.stop_propagation();
728 cx.new(|_| drag.clone())
729 })
730 .on_mouse_up_out(
731 MouseButton::Left,
732 cx.listener(|view, _, _, cx| {
733 if view.resizing_col.is_none() {
734 return;
735 }
736
737 view.resizing_col = None;
738
739 let new_widths = view.col_groups.iter().map(|g| g.width).collect();
740 cx.emit(TableEvent::ColumnWidthsChanged(new_widths));
741 cx.notify();
742 }),
743 )
744 .into_any_element()
745 }
746
747 fn render_sort_icon(
748 &self,
749 col_ix: usize,
750 col_group: &ColGroup,
751 _: &mut Window,
752 cx: &mut Context<Self>,
753 ) -> Option<impl IntoElement> {
754 if !self.sortable {
755 return None;
756 }
757
758 let Some(sort) = col_group.column.sort else {
759 return None;
760 };
761
762 let (icon, is_on) = match sort {
763 ColumnSort::Ascending => (IconName::SortAscending, true),
764 ColumnSort::Descending => (IconName::SortDescending, true),
765 ColumnSort::Default => (IconName::ChevronsUpDown, false),
766 };
767
768 Some(
769 div()
770 .id(("icon-sort", col_ix))
771 .p(px(2.))
772 .rounded(cx.theme().radius / 2.)
773 .map(|this| match is_on {
774 true => this,
775 false => this.opacity(0.5),
776 })
777 .hover(|this| this.bg(cx.theme().secondary).opacity(7.))
778 .active(|this| this.bg(cx.theme().secondary_active).opacity(1.))
779 .on_click(
780 cx.listener(move |table, _, window, cx| table.perform_sort(col_ix, window, cx)),
781 )
782 .child(
783 Icon::new(icon)
784 .size_3()
785 .text_color(cx.theme().secondary_foreground),
786 ),
787 )
788 }
789
790 fn render_th(
795 &self,
796 col_ix: usize,
797 window: &mut Window,
798 cx: &mut Context<Self>,
799 ) -> impl IntoElement {
800 let entity_id = cx.entity_id();
801 let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index");
802
803 let movable = self.col_movable && col_group.column.movable;
804 let paddings = col_group.column.paddings;
805 let name = col_group.column.name.clone();
806
807 h_flex()
808 .h_full()
809 .child(
810 self.render_cell(col_ix, window, cx)
811 .id(("col-header", col_ix))
812 .on_click(cx.listener(move |this, _, window, cx| {
813 this.on_col_head_click(col_ix, window, cx);
814 }))
815 .child(
816 h_flex()
817 .size_full()
818 .justify_between()
819 .items_center()
820 .child(self.delegate.render_th(col_ix, window, cx))
821 .when_some(paddings, |this, paddings| {
822 let offset_pr =
824 self.options.size.table_cell_padding().right - paddings.right;
825 this.pr(offset_pr.max(px(0.)))
826 })
827 .children(self.render_sort_icon(col_ix, &col_group, window, cx)),
828 )
829 .when(movable, |this| {
830 this.on_drag(
831 DragColumn {
832 entity_id,
833 col_ix,
834 name,
835 width: col_group.width,
836 },
837 |drag, _, _, cx| {
838 cx.stop_propagation();
839 cx.new(|_| drag.clone())
840 },
841 )
842 .drag_over::<DragColumn>(|this, _, _, cx| {
843 this.rounded_l_none()
844 .border_l_2()
845 .border_r_0()
846 .border_color(cx.theme().drag_border)
847 })
848 .on_drop(cx.listener(
849 move |table, drag: &DragColumn, window, cx| {
850 if drag.entity_id != cx.entity_id() {
852 return;
853 }
854
855 table.move_column(drag.col_ix, col_ix, window, cx);
856 },
857 ))
858 }),
859 )
860 .child(self.render_resize_handle(col_ix, window, cx))
862 .child({
864 let view = cx.entity().clone();
865 canvas(
866 move |bounds, _, cx| {
867 view.update(cx, |r, _| r.col_groups[col_ix].bounds = bounds)
868 },
869 |_, _, _, _| {},
870 )
871 .absolute()
872 .size_full()
873 })
874 }
875
876 fn render_table_head(
877 &mut self,
878 left_columns_count: usize,
879 window: &mut Window,
880 cx: &mut Context<Self>,
881 ) -> impl IntoElement {
882 let view = cx.entity().clone();
883 let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
884
885 if left_columns_count == 0 {
887 self.fixed_head_cols_bounds = Bounds::default();
888 }
889
890 h_flex()
891 .w_full()
892 .h(self.options.size.table_row_height())
893 .flex_shrink_0()
894 .border_b_1()
895 .border_color(cx.theme().border)
896 .text_color(cx.theme().table_head_foreground)
897 .when(left_columns_count > 0, |this| {
898 let view = view.clone();
899 this.child(
901 h_flex()
902 .relative()
903 .h_full()
904 .bg(cx.theme().table_head)
905 .children(
906 self.col_groups
907 .iter()
908 .filter(|col| col.column.fixed == Some(ColumnFixed::Left))
909 .enumerate()
910 .map(|(col_ix, _)| self.render_th(col_ix, window, cx)),
911 )
912 .child(
913 div()
915 .absolute()
916 .top_0()
917 .right_0()
918 .bottom_0()
919 .w_0()
920 .flex_shrink_0()
921 .border_r_1()
922 .border_color(cx.theme().border),
923 )
924 .child(
925 canvas(
926 move |bounds, _, cx| {
927 view.update(cx, |r, _| r.fixed_head_cols_bounds = bounds)
928 },
929 |_, _, _, _| {},
930 )
931 .absolute()
932 .size_full(),
933 ),
934 )
935 })
936 .child(
937 h_flex()
939 .id("table-head")
940 .size_full()
941 .overflow_scroll()
942 .relative()
943 .track_scroll(&horizontal_scroll_handle)
944 .bg(cx.theme().table_head)
945 .child(
946 h_flex()
947 .relative()
948 .children(
949 self.col_groups
950 .iter()
951 .skip(left_columns_count)
952 .enumerate()
953 .map(|(col_ix, _)| {
954 self.render_th(left_columns_count + col_ix, window, cx)
955 }),
956 )
957 .child(self.delegate.render_last_empty_col(window, cx)),
958 ),
959 )
960 }
961
962 #[allow(clippy::too_many_arguments)]
963 fn render_table_row(
964 &mut self,
965 row_ix: usize,
966 rows_count: usize,
967 left_columns_count: usize,
968 col_sizes: Rc<Vec<gpui::Size<Pixels>>>,
969 columns_count: usize,
970 is_filled: bool,
971 window: &mut Window,
972 cx: &mut Context<Self>,
973 ) -> impl IntoElement {
974 let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
975 let is_stripe_row = self.options.stripe && row_ix % 2 != 0;
976 let is_selected = self.selected_row == Some(row_ix);
977 let view = cx.entity().clone();
978 let row_height = self.options.size.table_row_height();
979
980 if row_ix < rows_count {
981 let is_last_row = row_ix + 1 == rows_count;
982 let need_render_border = is_selected || !is_last_row || !is_filled;
983
984 let mut tr = self.delegate.render_tr(row_ix, window, cx);
985 let style = tr.style().clone();
986
987 tr.h_flex()
988 .w_full()
989 .h(row_height)
990 .when(need_render_border, |this| {
991 this.border_b_1().border_color(cx.theme().table_row_border)
992 })
993 .when(is_stripe_row, |this| this.bg(cx.theme().table_even))
994 .refine_style(&style)
995 .hover(|this| {
996 if is_selected || self.right_clicked_row == Some(row_ix) {
997 this
998 } else {
999 this.bg(cx.theme().table_hover)
1000 }
1001 })
1002 .when(left_columns_count > 0, |this| {
1003 this.child(
1005 h_flex()
1006 .relative()
1007 .h_full()
1008 .children({
1009 let mut items = Vec::with_capacity(left_columns_count);
1010
1011 (0..left_columns_count).for_each(|col_ix| {
1012 items.push(self.render_col_wrap(col_ix, window, cx).child(
1013 self.render_cell(col_ix, window, cx).child(
1014 self.measure_render_td(row_ix, col_ix, window, cx),
1015 ),
1016 ));
1017 });
1018
1019 items
1020 })
1021 .child(
1022 div()
1024 .absolute()
1025 .top_0()
1026 .right_0()
1027 .bottom_0()
1028 .w_0()
1029 .flex_shrink_0()
1030 .border_r_1()
1031 .border_color(cx.theme().border),
1032 ),
1033 )
1034 })
1035 .child(
1036 h_flex()
1037 .flex_1()
1038 .h_full()
1039 .overflow_hidden()
1040 .relative()
1041 .child(
1042 crate::virtual_list::virtual_list(
1043 view,
1044 row_ix,
1045 Axis::Horizontal,
1046 col_sizes,
1047 {
1048 move |table, visible_range: Range<usize>, window, cx| {
1049 table.update_visible_range_if_need(
1050 visible_range.clone(),
1051 Axis::Horizontal,
1052 window,
1053 cx,
1054 );
1055
1056 let mut items = Vec::with_capacity(
1057 visible_range.end - visible_range.start,
1058 );
1059
1060 visible_range.for_each(|col_ix| {
1061 let col_ix = col_ix + left_columns_count;
1062 let el =
1063 table.render_col_wrap(col_ix, window, cx).child(
1064 table.render_cell(col_ix, window, cx).child(
1065 table.measure_render_td(
1066 row_ix, col_ix, window, cx,
1067 ),
1068 ),
1069 );
1070
1071 items.push(el);
1072 });
1073
1074 items
1075 }
1076 },
1077 )
1078 .with_scroll_handle(&self.horizontal_scroll_handle),
1079 )
1080 .child(self.delegate.render_last_empty_col(window, cx)),
1081 )
1082 .when_some(self.selected_row, |this, _| {
1084 this.when(
1085 is_selected && self.selection_state == SelectionState::Row,
1086 |this| {
1087 this.border_color(gpui::transparent_white()).child(
1088 div()
1089 .top(if row_ix == 0 { px(0.) } else { px(-1.) })
1090 .left(px(0.))
1091 .right(px(0.))
1092 .bottom(px(-1.))
1093 .absolute()
1094 .bg(cx.theme().table_active)
1095 .border_1()
1096 .border_color(cx.theme().table_active_border),
1097 )
1098 },
1099 )
1100 })
1101 .when(self.right_clicked_row == Some(row_ix), |this| {
1103 this.border_color(gpui::transparent_white()).child(
1104 div()
1105 .top(if row_ix == 0 { px(0.) } else { px(-1.) })
1106 .left(px(0.))
1107 .right(px(0.))
1108 .bottom(px(-1.))
1109 .absolute()
1110 .border_1()
1111 .border_color(cx.theme().selection),
1112 )
1113 })
1114 .on_mouse_down(
1115 MouseButton::Right,
1116 cx.listener(move |this, e, window, cx| {
1117 this.on_row_right_click(e, row_ix, window, cx);
1118 }),
1119 )
1120 .on_click(cx.listener(move |this, e, window, cx| {
1121 this.on_row_left_click(e, row_ix, window, cx);
1122 }))
1123 } else {
1124 self.delegate
1126 .render_tr(row_ix, window, cx)
1127 .h_flex()
1128 .w_full()
1129 .h(row_height)
1130 .border_b_1()
1131 .border_color(cx.theme().table_row_border)
1132 .when(is_stripe_row, |this| this.bg(cx.theme().table_even))
1133 .children((0..columns_count).map(|col_ix| {
1134 h_flex()
1135 .left(horizontal_scroll_handle.offset().x)
1136 .child(self.render_cell(col_ix, window, cx))
1137 }))
1138 .child(self.delegate.render_last_empty_col(window, cx))
1139 }
1140 }
1141
1142 fn calculate_extra_rows_needed(
1144 &self,
1145 total_height: Pixels,
1146 actual_height: Pixels,
1147 row_height: Pixels,
1148 ) -> usize {
1149 let mut extra_rows_needed = 0;
1150
1151 let remaining_height = total_height - actual_height;
1152 if remaining_height > px(0.) {
1153 extra_rows_needed = (remaining_height / row_height).floor() as usize;
1154 }
1155
1156 extra_rows_needed
1157 }
1158
1159 #[inline]
1160 fn measure_render_td(
1161 &mut self,
1162 row_ix: usize,
1163 col_ix: usize,
1164 window: &mut Window,
1165 cx: &mut Context<Self>,
1166 ) -> impl IntoElement {
1167 if !crate::measure_enable() {
1168 return self
1169 .delegate
1170 .render_td(row_ix, col_ix, window, cx)
1171 .into_any_element();
1172 }
1173
1174 let start = std::time::Instant::now();
1175 let el = self.delegate.render_td(row_ix, col_ix, window, cx);
1176 self._measure.push(start.elapsed());
1177 el.into_any_element()
1178 }
1179
1180 fn measure(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
1181 if !crate::measure_enable() {
1182 return;
1183 }
1184
1185 if self._measure.len() > 0 {
1187 let total = self
1188 ._measure
1189 .iter()
1190 .fold(Duration::default(), |acc, d| acc + *d);
1191 let avg = total / self._measure.len() as u32;
1192 eprintln!(
1193 "last render {} cells total: {:?}, avg: {:?}",
1194 self._measure.len(),
1195 total,
1196 avg,
1197 );
1198 }
1199 self._measure.clear();
1200 }
1201
1202 fn render_vertical_scrollbar(
1203 &mut self,
1204
1205 _: &mut Window,
1206 _: &mut Context<Self>,
1207 ) -> Option<impl IntoElement> {
1208 Some(
1209 div()
1210 .occlude()
1211 .absolute()
1212 .top(self.options.size.table_row_height())
1213 .right_0()
1214 .bottom_0()
1215 .w(Scrollbar::width())
1216 .child(
1217 Scrollbar::uniform_scroll(
1218 &self.vertical_scroll_state,
1219 &self.vertical_scroll_handle,
1220 )
1221 .max_fps(60),
1222 ),
1223 )
1224 }
1225
1226 fn render_horizontal_scrollbar(
1227 &mut self,
1228 _: &mut Window,
1229 _: &mut Context<Self>,
1230 ) -> impl IntoElement {
1231 div()
1232 .occlude()
1233 .absolute()
1234 .left(self.fixed_head_cols_bounds.size.width)
1235 .right_0()
1236 .bottom_0()
1237 .h(Scrollbar::width())
1238 .child(Scrollbar::horizontal(
1239 &self.horizontal_scroll_state,
1240 &self.horizontal_scroll_handle,
1241 ))
1242 }
1243}
1244
1245impl<D> Focusable for TableState<D>
1246where
1247 D: TableDelegate,
1248{
1249 fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
1250 self.focus_handle.clone()
1251 }
1252}
1253impl<D> EventEmitter<TableEvent> for TableState<D> where D: TableDelegate {}
1254
1255impl<D> Render for TableState<D>
1256where
1257 D: TableDelegate,
1258{
1259 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1260 self.measure(window, cx);
1261
1262 let columns_count = self.delegate.columns_count(cx);
1263 let left_columns_count = self
1264 .col_groups
1265 .iter()
1266 .filter(|col| self.col_fixed && col.column.fixed == Some(ColumnFixed::Left))
1267 .count();
1268 let rows_count = self.delegate.rows_count(cx);
1269 let loading = self.delegate.loading(cx);
1270
1271 let row_height = self.options.size.table_row_height();
1272 let total_height = self
1273 .vertical_scroll_handle
1274 .0
1275 .borrow()
1276 .base_handle
1277 .bounds()
1278 .size
1279 .height;
1280 let actual_height = row_height * rows_count as f32;
1281 let extra_rows_count =
1282 self.calculate_extra_rows_needed(total_height, actual_height, row_height);
1283 let render_rows_count = if self.options.stripe {
1284 rows_count + extra_rows_count
1285 } else {
1286 rows_count
1287 };
1288 let right_clicked_row = self.right_clicked_row;
1289 let is_filled = total_height > Pixels::ZERO && total_height <= actual_height;
1290
1291 let loading_view = if loading {
1292 Some(
1293 self.delegate
1294 .render_loading(self.options.size, window, cx)
1295 .into_any_element(),
1296 )
1297 } else {
1298 None
1299 };
1300
1301 let empty_view = if rows_count == 0 {
1302 Some(
1303 div()
1304 .size_full()
1305 .child(self.delegate.render_empty(window, cx))
1306 .into_any_element(),
1307 )
1308 } else {
1309 None
1310 };
1311
1312 let inner_table = v_flex()
1313 .id("table-inner")
1314 .size_full()
1315 .overflow_hidden()
1316 .child(self.render_table_head(left_columns_count, window, cx))
1317 .context_menu({
1318 let view = cx.entity().clone();
1319 move |this, window: &mut Window, cx: &mut Context<PopupMenu>| {
1320 if let Some(row_ix) = view.read(cx).right_clicked_row {
1321 view.update(cx, |menu, cx| {
1322 menu.delegate().context_menu(row_ix, this, window, cx)
1323 })
1324 } else {
1325 this
1326 }
1327 }
1328 })
1329 .map(|this| {
1330 if rows_count == 0 {
1331 this.children(empty_view)
1332 } else {
1333 this.child(
1334 h_flex().id("table-body").flex_grow().size_full().child(
1335 uniform_list(
1336 "table-uniform-list",
1337 render_rows_count,
1338 cx.processor(
1339 move |table, visible_range: Range<usize>, window, cx| {
1340 let col_sizes: Rc<Vec<gpui::Size<Pixels>>> = Rc::new(
1343 table
1344 .col_groups
1345 .iter()
1346 .skip(left_columns_count)
1347 .map(|col| col.bounds.size)
1348 .collect(),
1349 );
1350
1351 table.load_more_if_need(
1352 rows_count,
1353 visible_range.end,
1354 window,
1355 cx,
1356 );
1357 table.update_visible_range_if_need(
1358 visible_range.clone(),
1359 Axis::Vertical,
1360 window,
1361 cx,
1362 );
1363
1364 if visible_range.end > rows_count {
1365 table.scroll_to_row(
1366 std::cmp::min(
1367 visible_range.start,
1368 rows_count.saturating_sub(1),
1369 ),
1370 cx,
1371 );
1372 }
1373
1374 let mut items = Vec::with_capacity(
1375 visible_range.end.saturating_sub(visible_range.start),
1376 );
1377
1378 visible_range.for_each(|row_ix| {
1380 items.push(table.render_table_row(
1382 row_ix,
1383 rows_count,
1384 left_columns_count,
1385 col_sizes.clone(),
1386 columns_count,
1387 is_filled,
1388 window,
1389 cx,
1390 ));
1391 });
1392
1393 items
1394 },
1395 ),
1396 )
1397 .flex_grow()
1398 .size_full()
1399 .with_sizing_behavior(ListSizingBehavior::Auto)
1400 .track_scroll(self.vertical_scroll_handle.clone())
1401 .into_any_element(),
1402 ),
1403 )
1404 }
1405 });
1406
1407 div()
1408 .size_full()
1409 .children(loading_view)
1410 .when(!loading, |this| {
1411 this.child(inner_table)
1412 .child(ScrollableMask::new(
1413 Axis::Horizontal,
1414 &self.horizontal_scroll_handle,
1415 ))
1416 .when(right_clicked_row.is_some(), |this| {
1417 this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
1418 this.right_clicked_row = None;
1419 cx.notify();
1420 }))
1421 })
1422 })
1423 .child(canvas(
1424 {
1425 let state = cx.entity();
1426 move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
1427 },
1428 |_, _, _, _| {},
1429 ))
1430 .when(!window.is_inspector_picking(cx), |this| {
1431 this.child(
1432 div()
1433 .absolute()
1434 .top_0()
1435 .size_full()
1436 .when(self.options.scrollbar_visible.bottom, |this| {
1437 this.child(self.render_horizontal_scrollbar(window, cx))
1438 })
1439 .when(
1440 self.options.scrollbar_visible.right && rows_count > 0,
1441 |this| this.children(self.render_vertical_scrollbar(window, cx)),
1442 ),
1443 )
1444 })
1445 }
1446}