1use gpui::{
12 prelude::*, px, AnyElement, App, ClickEvent, InteractiveElement, IntoElement, ParentElement,
13 Pixels, RenderOnce, SharedString, Styled, Window,
14};
15use herogpui_core::{element_id, SelectionBehavior, SelectionMode};
16use herogpui_theme::ActiveTheme;
17
18use crate::{
19 a11y::{self, A11y as _},
20 checkbox::Checkbox,
21 icons,
22};
23
24type OnRowClick = std::sync::Arc<dyn Fn(&usize, &ClickEvent, &mut Window, &mut App) + 'static>;
25type OnSelectionChange = std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>;
26type OnSortChange = std::sync::Arc<dyn Fn(&SortDescriptor, &mut Window, &mut App) + 'static>;
27type OnLoadMore = std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>;
28type OnResize = std::sync::Arc<dyn Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static>;
29type VirtualRowKey = std::sync::Arc<dyn Fn(usize) -> SharedString + 'static>;
30type VirtualRow = std::sync::Arc<dyn Fn(usize) -> TableRow + 'static>;
31type VirtualRowText = std::sync::Arc<dyn Fn(usize) -> SharedString + 'static>;
32type VirtualTree = std::sync::Arc<dyn Fn(usize) -> VirtualTreeMetadata + 'static>;
33
34const DEFAULT_COLUMN_MIN_WIDTH: f32 = 75.;
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum TableVariant {
39 #[default]
41 Primary,
42 Secondary,
44}
45
46impl TableVariant {
47 pub const ALL: [TableVariant; 2] = [TableVariant::Primary, TableVariant::Secondary];
49
50 pub fn label(self) -> &'static str {
52 match self {
53 TableVariant::Primary => "Primary",
54 TableVariant::Secondary => "Secondary",
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum SortDirection {
62 Ascending,
64 Descending,
66}
67
68impl SortDirection {
69 pub fn flipped(self) -> Self {
71 match self {
72 SortDirection::Ascending => SortDirection::Descending,
73 SortDirection::Descending => SortDirection::Ascending,
74 }
75 }
76}
77
78type Indicator = std::sync::Arc<dyn Fn(SortDirection) -> AnyElement + 'static>;
79
80#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct SortDescriptor {
83 pub column: SharedString,
85 pub direction: SortDirection,
87}
88
89impl SortDescriptor {
90 pub fn new(column: impl Into<SharedString>, direction: SortDirection) -> Self {
92 Self {
93 column: column.into(),
94 direction,
95 }
96 }
97
98 pub fn next(current: Option<&SortDescriptor>, column: impl Into<SharedString>) -> Self {
103 let column = column.into();
104 match current {
105 Some(d) if d.column == column => Self {
106 column,
107 direction: d.direction.flipped(),
108 },
109 _ => Self {
110 column,
111 direction: SortDirection::Ascending,
112 },
113 }
114 }
115}
116
117#[must_use = "builder methods return a new value; pass the column to its table"]
119#[derive(Clone, Debug)]
120pub struct TableColumn {
121 label: SharedString,
122 allows_sorting: bool,
123 is_row_header: bool,
124 allows_resizing: bool,
127 width: Option<Pixels>,
129 default_width: Option<Pixels>,
132 min_width: Option<Pixels>,
134 max_width: Option<Pixels>,
136 frozen: bool,
138}
139
140impl TableColumn {
141 pub fn new(label: impl Into<SharedString>) -> Self {
143 Self {
144 label: label.into(),
145 allows_sorting: false,
146 is_row_header: false,
147 allows_resizing: false,
148 width: None,
149 default_width: None,
150 min_width: None,
151 max_width: None,
152 frozen: false,
153 }
154 }
155
156 pub fn allows_resizing(mut self, v: bool) -> Self {
158 self.allows_resizing = v;
159 self
160 }
161
162 pub fn default_width(mut self, width: impl Into<Pixels>) -> Self {
165 self.default_width = Some(width.into());
166 self
167 }
168
169 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
171 self.width = Some(width.into());
172 self
173 }
174
175 pub fn min_width(mut self, width: impl Into<Pixels>) -> Self {
177 self.min_width = Some(width.into());
178 self
179 }
180
181 pub fn max_width(mut self, width: impl Into<Pixels>) -> Self {
183 self.max_width = Some(width.into());
184 self
185 }
186
187 pub fn allows_sorting(mut self, v: bool) -> Self {
189 self.allows_sorting = v;
190 self
191 }
192
193 pub fn is_row_header(mut self, v: bool) -> Self {
199 self.is_row_header = v;
200 self
201 }
202
203 pub fn frozen(mut self, frozen: bool) -> Self {
217 self.frozen = frozen;
218 self
219 }
220}
221
222impl From<SharedString> for TableColumn {
223 fn from(label: SharedString) -> Self {
224 TableColumn::new(label)
225 }
226}
227
228impl From<&str> for TableColumn {
229 fn from(label: &str) -> Self {
230 TableColumn::new(label.to_owned())
231 }
232}
233
234#[must_use = "builder methods return a new value; pass it on to its component"]
236pub struct TableRow {
237 key: Option<SharedString>,
238 text_value: Option<SharedString>,
239 cells: Vec<AnyElement>,
240 children: Vec<TableRow>,
243}
244
245#[derive(Clone, Debug, Default, PartialEq, Eq)]
251pub struct VirtualTreeMetadata {
252 pub depth: usize,
254 pub parent_key: Option<SharedString>,
256 pub has_children: bool,
258}
259
260impl TableRow {
261 pub fn new(cells: Vec<AnyElement>) -> Self {
263 Self {
264 key: None,
265 text_value: None,
266 cells,
267 children: Vec::new(),
268 }
269 }
270
271 pub fn children(mut self, rows: Vec<TableRow>) -> Self {
273 self.children = rows;
274 self
275 }
276
277 pub fn key(mut self, key: impl Into<SharedString>) -> Self {
279 self.key = Some(key.into());
280 self
281 }
282
283 pub fn text_value(mut self, value: impl Into<SharedString>) -> Self {
288 self.text_value = Some(value.into());
289 self
290 }
291}
292
293type OnExpandedChange = std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>;
294
295#[derive(Clone, Copy, PartialEq, Eq)]
296enum RowActivation {
297 Pointer,
298 Enter,
299 Space,
300}
301
302#[derive(Clone, Copy, PartialEq, Eq)]
303enum RowIntent {
304 Action,
305 Selection,
306 None,
307}
308
309#[derive(Clone, Debug, Default)]
310struct TableTypeahead {
311 query: String,
312 last: Option<web_time::Instant>,
313}
314
315#[derive(Clone, Debug, Default)]
316struct TableSelectionRange {
317 anchor: Option<SharedString>,
318 current: Option<SharedString>,
319 is_all: bool,
320}
321
322impl TableTypeahead {
323 fn is_active(&self, now: web_time::Instant) -> bool {
324 !self.query.is_empty()
325 && self
326 .last
327 .is_some_and(|last| now.duration_since(last) <= crate::list_nav::TYPEAHEAD_TIMEOUT)
328 }
329
330 fn push(&mut self, key: &str, now: web_time::Instant) -> String {
331 if self
332 .last
333 .is_none_or(|last| now.duration_since(last) > crate::list_nav::TYPEAHEAD_TIMEOUT)
334 {
335 self.query.clear();
336 }
337 self.last = Some(now);
338 self.query.push_str(key);
339 self.query.clone()
340 }
341
342 fn clear(&mut self) {
343 self.query.clear();
344 self.last = None;
345 }
346}
347
348#[derive(Clone)]
349struct TableTypeaheadNavigation {
350 state: gpui::Entity<TableTypeahead>,
351 labels: Vec<String>,
352 virtual_indices: Option<Vec<usize>>,
353 virtual_text: Option<VirtualRowText>,
354 stops: Vec<usize>,
355 keys: Vec<SharedString>,
356 cursor: gpui::Entity<Option<SharedString>>,
357 fixed_virtual: bool,
358 fixed_scroll: crate::VirtualListHandle,
359 variable_scroll: Option<gpui::ListState>,
360}
361
362impl TableTypeaheadNavigation {
363 fn push(
364 &self,
365 character: &str,
366 now: web_time::Instant,
367 clear_on_failure: bool,
368 cx: &mut App,
369 ) -> bool {
370 let from = self
371 .cursor
372 .read(cx)
373 .as_ref()
374 .and_then(|key| self.keys.iter().position(|row| row == key))
375 .filter(|index| self.stops.contains(index));
376 let mut query = String::new();
377 self.state.update(cx, |state, _| {
378 query = state.push(character, now);
379 });
380 let projected_labels = self
381 .virtual_text
382 .as_ref()
383 .zip(self.virtual_indices.as_ref())
384 .map(|(text_value, indices)| {
385 indices
386 .iter()
387 .map(|index| text_value(*index).to_string())
388 .collect::<Vec<_>>()
389 });
390 let searchable_labels = projected_labels.as_ref().unwrap_or(&self.labels);
391 let Some(next) =
392 crate::list_nav::typeahead(searchable_labels, &self.stops, from, &query, false)
393 else {
394 if clear_on_failure {
395 self.state.update(cx, |state, _| state.clear());
396 }
397 return false;
398 };
399 self.cursor.update(cx, |value, cx| {
400 *value = self.keys.get(next).cloned();
401 cx.notify();
402 });
403 if self.fixed_virtual {
404 self.fixed_scroll
405 .scroll_to_item(next, crate::VirtualListScroll::Center);
406 } else if let Some(state) = &self.variable_scroll {
407 state.scroll_to(gpui::ListOffset {
408 item_ix: next,
409 offset_in_item: px(0.),
410 });
411 }
412 true
413 }
414}
415
416#[derive(Clone, PartialEq, Eq)]
417enum LoadMoreCollection {
418 Rows(Vec<SharedString>),
419 Virtual {
420 count: usize,
421 identity: SharedString,
422 },
423}
424
425fn row_intent(
426 activation: RowActivation,
427 mode: SelectionMode,
428 behavior: SelectionBehavior,
429 selection_is_empty: bool,
430 has_action: bool,
431) -> RowIntent {
432 let primary_action = has_action
437 && (mode == SelectionMode::None
438 || (behavior == SelectionBehavior::Toggle && selection_is_empty));
439 match activation {
440 RowActivation::Pointer if primary_action => RowIntent::Action,
441 RowActivation::Pointer if mode != SelectionMode::None => RowIntent::Selection,
442 RowActivation::Enter if primary_action => RowIntent::Action,
443 RowActivation::Enter if !has_action && mode != SelectionMode::None => RowIntent::Selection,
444 RowActivation::Space if mode != SelectionMode::None => RowIntent::Selection,
445 _ => RowIntent::None,
446 }
447}
448
449fn select_all_flags(selectable: &[SharedString], selected: &[SharedString]) -> (bool, bool) {
450 let selected_count = selectable
451 .iter()
452 .filter(|key| selected.contains(key))
453 .count();
454 let all_selected = !selectable.is_empty() && selected_count == selectable.len();
455 let indeterminate = !selected.is_empty() && !all_selected;
456 (all_selected, indeterminate)
457}
458
459fn same_selection(left: &[SharedString], right: &[SharedString]) -> bool {
460 left.len() == right.len()
461 && left.iter().collect::<std::collections::HashSet<_>>()
462 == right.iter().collect::<std::collections::HashSet<_>>()
463}
464
465fn extend_selection_range(
466 current: &[SharedString],
467 collection: &[SharedString],
468 selectable: &[SharedString],
469 range: &TableSelectionRange,
470 target: &SharedString,
471) -> Vec<SharedString> {
472 if range.is_all {
473 return vec![target.clone()];
474 }
475 let anchor = range.anchor.as_ref().unwrap_or(target);
476 let previous = range.current.as_ref().unwrap_or(target);
477 let mut anchor_at = None;
478 let mut previous_at = None;
479 let mut target_at = None;
480 for (index, key) in collection.iter().enumerate() {
481 anchor_at = anchor_at.or_else(|| (key == anchor).then_some(index));
482 previous_at = previous_at.or_else(|| (key == previous).then_some(index));
483 target_at = target_at.or_else(|| (key == target).then_some(index));
484 }
485 let between = |from: Option<usize>, to: Option<usize>| {
486 from.zip(to)
487 .map(|(from, to)| if from <= to { from..=to } else { to..=from })
488 };
489 let mut next = current.to_vec();
490 if let Some(previous_range) = between(anchor_at, previous_at) {
491 let removed: std::collections::HashSet<&SharedString> =
492 previous_range.map(|index| &collection[index]).collect();
493 next.retain(|key| !removed.contains(key));
494 }
495 if let Some(target_range) = between(anchor_at, target_at) {
496 let selectable: std::collections::HashSet<&SharedString> = selectable.iter().collect();
497 let mut selected: std::collections::HashSet<SharedString> = next.iter().cloned().collect();
498 for index in target_range {
499 let key = &collection[index];
500 if selectable.contains(key) && selected.insert(key.clone()) {
501 next.push(key.clone());
502 }
503 }
504 }
505 next
506}
507
508fn home_end_registered(modifiers: gpui::Modifiers, macos: bool) -> bool {
521 if macos {
522 !modifiers.control && !modifiers.platform
523 } else {
524 !modifiers.alt && !modifiers.platform
525 }
526}
527
528fn shift_home_end_extends(key_name: &str, control: bool, macos: bool) -> bool {
534 !matches!(key_name, "home" | "end") || (!macos && control)
535}
536
537fn flex_cell(el: gpui::Div) -> gpui::Div {
543 el.flex_basis(px(0.)).flex_1()
544}
545
546#[derive(Clone)]
557struct FrozenColumns {
558 count: usize,
561 scroll: gpui::ScrollHandle,
562 view: gpui::EntityId,
564}
565
566impl FrozenColumns {
567 fn frozen_part(&self) -> gpui::Div {
571 let scroll = self.scroll.clone();
572 let view = self.view;
573 gpui::div()
574 .flex()
575 .flex_shrink_0()
576 .on_scroll_wheel(move |event, window, cx| {
577 let at = scroll.offset();
578 crate::util::shift_wheel_scroll_x(&scroll, event);
579 let delta = event.delta.pixel_delta(window.line_height());
580 let x_delta = if delta.x != px(0.) {
581 delta.x
582 } else {
583 scroll.offset().x - at.x
584 };
585 if x_delta == px(0.) {
586 return;
587 }
588 let x = (at.x + x_delta).clamp(-scroll.max_offset().x, px(0.));
589 if x != at.x {
590 scroll.set_offset(gpui::point(x, at.y));
591 cx.notify(view);
592 }
593 })
594 }
595
596 fn scrolling_part(&self, part: gpui::Div, id: gpui::ElementId) -> gpui::Stateful<gpui::Div> {
600 part.flex()
601 .flex_1()
602 .min_w_0()
603 .id(id)
604 .overflow_x_scroll()
605 .track_scroll(&self.scroll)
606 .restrict_scroll_to_axis()
607 }
608
609 fn reveal(&self, column: usize) {
611 if column >= self.count {
612 self.scroll.scroll_to_item(column - self.count);
613 }
614 }
615}
616
617fn clamp_to_region(from: usize, to: usize, frozen: usize) -> usize {
620 if frozen == 0 {
621 to
622 } else if from < frozen {
623 to.min(frozen - 1)
624 } else {
625 to.max(frozen)
626 }
627}
628
629fn frozen_first(order: &[usize], columns: &[TableColumn]) -> Vec<usize> {
631 let frozen = |ix: &usize| columns.get(*ix).is_some_and(|c| c.frozen);
632 order
633 .iter()
634 .copied()
635 .filter(frozen)
636 .chain(order.iter().copied().filter(|ix| !frozen(ix)))
637 .collect()
638}
639
640fn tree_column_padding(depth: usize) -> Option<Pixels> {
647 (depth > 0).then(|| px(16. * (depth as f32 + 1.)))
648}
649
650#[allow(clippy::float_cmp)] fn intrinsic_children_width(bounds: &[gpui::Bounds<Pixels>]) -> f32 {
657 let mut left = f32::MAX;
658 let mut right = f32::MIN;
659 for bound in bounds.iter().filter(|bound| bound.size.width > px(2.)) {
660 left = left.min(f32::from(bound.origin.x));
661 right = right.max(f32::from(bound.origin.x + bound.size.width));
662 }
663 if left == f32::MAX {
664 0.0
665 } else {
666 (right - left).max(0.0)
667 }
668}
669
670fn table_cell_focus_ring(cx: &App, first: bool, last: bool) -> gpui::Div {
678 let color = cx.colors().focus;
679 let radius = crate::util::key_radius(cx);
680 let mut top = gpui::div()
681 .absolute()
682 .top_0()
683 .left_0()
684 .right_0()
685 .h(px(2.))
686 .bg(color);
687 let mut bottom = gpui::div()
688 .absolute()
689 .bottom_0()
690 .left_0()
691 .right_0()
692 .h(px(2.))
693 .bg(color);
694 if first {
695 top = top.rounded_tl(radius);
696 bottom = bottom.rounded_bl(radius);
697 }
698 if last {
699 top = top.rounded_tr(radius);
700 bottom = bottom.rounded_br(radius);
701 }
702 let mut ring = gpui::div().absolute().inset_0().child(top).child(bottom);
703 if first {
704 ring = ring.child(
705 gpui::div()
706 .absolute()
707 .top_0()
708 .bottom_0()
709 .left_0()
710 .w(px(2.))
711 .rounded_tl(radius)
712 .rounded_bl(radius)
713 .bg(color),
714 );
715 }
716 if last {
717 ring = ring.child(
718 gpui::div()
719 .absolute()
720 .top_0()
721 .bottom_0()
722 .right_0()
723 .w(px(2.))
724 .rounded_tr(radius)
725 .rounded_br(radius)
726 .bg(color),
727 );
728 }
729 ring
730}
731
732#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
734#[derive(IntoElement)]
735pub struct Table {
736 id: SharedString,
742 indicator: Option<Indicator>,
745 columns: Vec<TableColumn>,
746 rows: Vec<TableRow>,
747 tree_column: usize,
749 expanded_keys: Vec<SharedString>,
751 on_expanded_change: Option<OnExpandedChange>,
752 row_height: Option<Pixels>,
756 estimated_row_height: Option<Pixels>,
759 loader_height: Option<Pixels>,
761 load_more_offset: f32,
763 max_h: Option<Pixels>,
765 gap: Option<Pixels>,
768 padding: Option<Pixels>,
769 virtual_rows: Option<(usize, SharedString, VirtualRowKey, VirtualRow)>,
773 virtual_text_value: Option<VirtualRowText>,
774 virtual_tree_metadata: Option<VirtualTree>,
775 variant: TableVariant,
776 selection_mode: SelectionMode,
777 selection_behavior: SelectionBehavior,
778 selected_keys: Vec<SharedString>,
779 default_selected_keys: Vec<SharedString>,
780 disabled_keys: Vec<SharedString>,
781 disallow_empty_selection: bool,
782 is_selection_controlled: bool,
783 sort_descriptor: Option<SortDescriptor>,
784 show_indicator: bool,
785 is_pending: bool,
786 empty_state: Option<AnyElement>,
787 footer: Option<AnyElement>,
788 on_row_click: Option<OnRowClick>,
789 on_selection_change: Option<OnSelectionChange>,
790 on_sort_change: Option<OnSortChange>,
791 on_load_more: Option<OnLoadMore>,
792 on_resize_start: Option<OnResize>,
793 on_resize: Option<OnResize>,
794 on_resize_end: Option<OnResize>,
795 row_hover_bg: Option<gpui::Hsla>,
798 radius: Option<Pixels>,
802 sx: Option<Box<gpui::StyleRefinement>>,
804 column_reorder: ColumnReorder,
806 cell_selection: CellSelection,
808}
809
810impl Table {
811 pub fn new(columns: Vec<SharedString>) -> Self {
813 Self {
814 id: SharedString::from("table"),
815 indicator: None,
816 columns: columns.into_iter().map(TableColumn::new).collect(),
817 rows: Vec::new(),
818 tree_column: 0,
819 expanded_keys: Vec::new(),
820 on_expanded_change: None,
821 row_height: None,
822 estimated_row_height: None,
823 loader_height: None,
824 load_more_offset: 1.0,
825 max_h: None,
826 gap: None,
827 padding: None,
828 virtual_rows: None,
829 virtual_text_value: None,
830 virtual_tree_metadata: None,
831 variant: TableVariant::Primary,
832 selection_mode: SelectionMode::None,
833 selection_behavior: SelectionBehavior::Toggle,
834 selected_keys: Vec::new(),
835 default_selected_keys: Vec::new(),
836 disabled_keys: Vec::new(),
837 disallow_empty_selection: false,
838 is_selection_controlled: false,
839 sort_descriptor: None,
840 show_indicator: true,
841 is_pending: false,
842 empty_state: None,
843 footer: None,
844 on_row_click: None,
845 on_selection_change: None,
846 on_sort_change: None,
847 on_load_more: None,
848 on_resize_start: None,
849 on_resize: None,
850 on_resize_end: None,
851 row_hover_bg: None,
852 radius: None,
853 sx: None,
854 column_reorder: ColumnReorder::default(),
855 cell_selection: CellSelection::default(),
856 }
857 }
858
859 pub fn id(mut self, id: impl Into<SharedString>) -> Self {
865 self.id = id.into();
866 self
867 }
868
869 pub fn indicator(mut self, render: impl Fn(SortDirection) -> AnyElement + 'static) -> Self {
875 self.indicator = Some(std::sync::Arc::new(render));
876 self
877 }
878
879 pub fn columns(mut self, columns: Vec<TableColumn>) -> Self {
881 self.columns = columns;
882 self
883 }
884
885 pub fn column(mut self, column: impl Into<TableColumn>) -> Self {
887 self.columns.push(column.into());
888 self
889 }
890
891 pub fn variant(mut self, variant: TableVariant) -> Self {
893 self.variant = variant;
894 self
895 }
896
897 pub fn row_height(mut self, height: impl Into<Pixels>) -> Self {
906 self.row_height = Some(height.into());
907 self
908 }
909
910 pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
913 self.max_h = Some(height.into());
914 self
915 }
916
917 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
919 self.gap = Some(gap.into());
920 self
921 }
922
923 pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
925 self.padding = Some(padding.into());
926 self
927 }
928
929 pub fn estimated_row_height(mut self, height: impl Into<Pixels>) -> Self {
950 self.estimated_row_height = Some(height.into());
951 self
952 }
953
954 pub fn loader_height(mut self, height: impl Into<Pixels>) -> Self {
956 self.loader_height = Some(height.into());
957 self
958 }
959
960 pub fn virtual_rows(
971 mut self,
972 count: usize,
973 identity: impl Into<SharedString>,
974 key: impl Fn(usize) -> SharedString + 'static,
975 row: impl Fn(usize) -> TableRow + 'static,
976 ) -> Self {
977 self.virtual_rows = Some((
978 count,
979 identity.into(),
980 std::sync::Arc::new(key),
981 std::sync::Arc::new(row),
982 ));
983 self
984 }
985
986 pub fn virtual_text_value(
989 mut self,
990 text_value: impl Fn(usize) -> SharedString + 'static,
991 ) -> Self {
992 self.virtual_text_value = Some(std::sync::Arc::new(text_value));
993 self
994 }
995
996 pub fn virtual_tree_metadata(
1005 mut self,
1006 metadata: impl Fn(usize) -> VirtualTreeMetadata + 'static,
1007 ) -> Self {
1008 self.virtual_tree_metadata = Some(std::sync::Arc::new(metadata));
1009 self
1010 }
1011
1012 pub fn tree_column(mut self, index: usize) -> Self {
1015 self.tree_column = index;
1016 self
1017 }
1018
1019 pub fn expanded_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
1021 self.expanded_keys = keys.into_iter().collect();
1022 self
1023 }
1024
1025 pub fn on_expanded_change(
1027 mut self,
1028 f: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
1029 ) -> Self {
1030 self.on_expanded_change = Some(std::sync::Arc::new(f));
1031 self
1032 }
1033
1034 pub fn tree_row(mut self, row: TableRow) -> Self {
1036 self.rows.push(row);
1037 self
1038 }
1039
1040 pub fn row(mut self, cells: Vec<AnyElement>) -> Self {
1042 self.rows.push(TableRow::new(cells));
1043 self
1044 }
1045
1046 pub fn keyed_row(mut self, key: impl Into<SharedString>, cells: Vec<AnyElement>) -> Self {
1048 self.rows.push(TableRow::new(cells).key(key));
1049 self
1050 }
1051
1052 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
1054 self.selection_mode = mode;
1055 self
1056 }
1057
1058 pub fn selection_behavior(mut self, behavior: SelectionBehavior) -> Self {
1063 self.selection_behavior = behavior;
1064 self
1065 }
1066
1067 pub fn row_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
1072 self.row_hover_bg = Some(color.into());
1073 self
1074 }
1075
1076 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
1083 self.radius = Some(radius.into());
1084 self
1085 }
1086
1087 pub fn selected_keys(
1089 mut self,
1090 keys: impl IntoIterator<Item = impl Into<SharedString>>,
1091 ) -> Self {
1092 self.selected_keys = keys.into_iter().map(Into::into).collect();
1093 self.is_selection_controlled = true;
1094 self
1095 }
1096
1097 pub fn default_selected_keys(
1100 mut self,
1101 keys: impl IntoIterator<Item = impl Into<SharedString>>,
1102 ) -> Self {
1103 self.default_selected_keys = keys.into_iter().map(Into::into).collect();
1104 self
1105 }
1106
1107 pub fn disallow_empty_selection(mut self, disallow: bool) -> Self {
1110 self.disallow_empty_selection = disallow;
1111 self
1112 }
1113
1114 pub fn disabled_keys(
1117 mut self,
1118 keys: impl IntoIterator<Item = impl Into<SharedString>>,
1119 ) -> Self {
1120 self.disabled_keys = keys.into_iter().map(Into::into).collect();
1121 self
1122 }
1123
1124 pub fn on_selection_change(
1126 mut self,
1127 f: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
1128 ) -> Self {
1129 self.on_selection_change = Some(std::sync::Arc::new(f));
1130 self
1131 }
1132
1133 pub fn sort_descriptor(mut self, descriptor: SortDescriptor) -> Self {
1135 self.sort_descriptor = Some(descriptor);
1136 self
1137 }
1138
1139 pub fn on_sort_change(
1144 mut self,
1145 f: impl Fn(&SortDescriptor, &mut Window, &mut App) + 'static,
1146 ) -> Self {
1147 self.on_sort_change = Some(std::sync::Arc::new(f));
1148 self
1149 }
1150
1151 pub fn show_indicator(mut self, v: bool) -> Self {
1153 self.show_indicator = v;
1154 self
1155 }
1156
1157 pub fn is_pending(mut self, v: bool) -> Self {
1159 self.is_pending = v;
1160 self
1161 }
1162
1163 pub fn scroll_offset(mut self, offset: f32) -> Self {
1167 self.load_more_offset = offset;
1168 self
1169 }
1170
1171 pub fn on_load_more(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
1173 self.on_load_more = Some(std::sync::Arc::new(f));
1174 self
1175 }
1176
1177 pub fn on_resize_start(
1180 mut self,
1181 f: impl Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static,
1182 ) -> Self {
1183 self.on_resize_start = Some(std::sync::Arc::new(f));
1184 self
1185 }
1186
1187 pub fn on_resize(
1191 mut self,
1192 f: impl Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static,
1193 ) -> Self {
1194 self.on_resize = Some(std::sync::Arc::new(f));
1195 self
1196 }
1197
1198 pub fn on_resize_end(
1201 mut self,
1202 f: impl Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static,
1203 ) -> Self {
1204 self.on_resize_end = Some(std::sync::Arc::new(f));
1205 self
1206 }
1207
1208 pub fn footer(mut self, content: impl IntoElement) -> Self {
1211 self.footer = Some(content.into_any_element());
1212 self
1213 }
1214
1215 pub fn empty_state(mut self, content: impl IntoElement) -> Self {
1217 self.empty_state = Some(content.into_any_element());
1218 self
1219 }
1220
1221 pub fn on_row_click(
1223 mut self,
1224 f: impl Fn(&usize, &ClickEvent, &mut Window, &mut App) + 'static,
1225 ) -> Self {
1226 self.on_row_click = Some(std::sync::Arc::new(f));
1227 self
1228 }
1229
1230 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
1235 crate::util::refine_sx(&mut self.sx, style);
1236 self
1237 }
1238
1239 fn row_keys(&self) -> Vec<SharedString> {
1241 fn collect(rows: &[TableRow], path: &str, out: &mut Vec<SharedString>) {
1242 for (index, row) in rows.iter().enumerate() {
1243 let key = match &row.key {
1244 Some(key) => key.clone(),
1245 None if path.is_empty() => SharedString::from(index.to_string()),
1246 None => SharedString::from(format!("{path}-{index}")),
1247 };
1248 out.push(key.clone());
1249 collect(&row.children, key.as_ref(), out);
1250 }
1251 }
1252
1253 let mut keys = Vec::new();
1254 collect(&self.rows, "", &mut keys);
1255 keys
1256 }
1257}
1258
1259#[derive(Default)]
1268struct VirtualProjection {
1269 count: usize,
1270 identity: SharedString,
1271 expanded: std::sync::Arc<std::collections::HashSet<SharedString>>,
1272 tree: bool,
1273 full_keys: std::sync::Arc<Vec<SharedString>>,
1274 visible: std::sync::Arc<Vec<(usize, SharedString, VirtualTreeMetadata)>>,
1276 selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>>,
1277 selectable_disabled_keys: Option<std::sync::Arc<Vec<SharedString>>>,
1278}
1279
1280fn filtered_selectable_keys(
1281 full_keys: &[SharedString],
1282 disabled_keys: &[SharedString],
1283) -> std::sync::Arc<Vec<SharedString>> {
1284 std::sync::Arc::new(
1285 full_keys
1286 .iter()
1287 .filter(|key| !disabled_keys.contains(key))
1288 .cloned()
1289 .collect(),
1290 )
1291}
1292
1293fn resolved_column_widths(
1294 columns: &[TableColumn],
1295 resized: &[Option<Pixels>],
1296 measured: &[Option<Pixels>],
1297 proposal: Option<(usize, Pixels)>,
1298) -> Vec<(SharedString, Pixels)> {
1299 columns
1300 .iter()
1301 .enumerate()
1302 .filter_map(|(index, column)| {
1303 let width = proposal
1304 .filter(|(proposed_index, _)| *proposed_index == index)
1305 .map(|(_, width)| width)
1306 .or(column.width)
1307 .or_else(|| resized.get(index).copied().flatten())
1308 .or(column.default_width)
1309 .or_else(|| measured.get(index).copied().flatten())?;
1310 let width = if column.allows_resizing {
1311 let min = column.min_width.map_or(DEFAULT_COLUMN_MIN_WIDTH, f32::from);
1312 let max = column.max_width.map_or(f32::MAX, f32::from);
1313 px(f32::from(width).floor().min(max).max(min))
1314 } else {
1315 width
1316 };
1317 Some((column.label.clone(), width))
1318 })
1319 .collect()
1320}
1321
1322fn final_column_widths(
1323 columns: &[TableColumn],
1324 resized: &[Option<Pixels>],
1325 measured: &[Option<Pixels>],
1326 column: usize,
1327) -> Vec<(SharedString, Pixels)> {
1328 let proposal = resized
1329 .get(column)
1330 .copied()
1331 .flatten()
1332 .map(|width| (column, width));
1333 resolved_column_widths(columns, resized, measured, proposal)
1334}
1335
1336fn clear_controlled_resize_proposal(
1337 columns: &[TableColumn],
1338 resized: &gpui::Entity<Vec<Option<Pixels>>>,
1339 column: usize,
1340 cx: &mut App,
1341) {
1342 if columns
1343 .get(column)
1344 .is_some_and(|column| column.width.is_some())
1345 {
1346 resized.update(cx, |values, cx| {
1347 if values
1348 .get_mut(column)
1349 .is_some_and(|width| width.take().is_some())
1350 {
1351 cx.notify();
1352 }
1353 });
1354 }
1355}
1356
1357struct TableState {
1362 resized: gpui::Entity<Vec<Option<Pixels>>>,
1364 resized_now: Vec<Option<Pixels>>,
1365 measured_widths: gpui::Entity<Vec<Option<Pixels>>>,
1366 measured_widths_now: Vec<Option<Pixels>>,
1367 dragging: gpui::Entity<Option<(usize, f32, f32)>>,
1368 drag_now: Option<(usize, f32, f32)>,
1369 keyboard_resizing: gpui::Entity<Option<usize>>,
1370 keyboard_resize_now: Option<usize>,
1371 load_more_state: gpui::Entity<(bool, Option<LoadMoreCollection>)>,
1372 table_focus: gpui::FocusHandle,
1374 row_cursor: gpui::Entity<Option<SharedString>>,
1375 selection_range: gpui::Entity<TableSelectionRange>,
1376 typeahead: gpui::Entity<TableTypeahead>,
1377 selection_own: Option<gpui::Entity<Vec<SharedString>>>,
1378}
1379
1380struct VirtualBody {
1383 projection: Option<std::sync::Arc<VirtualProjection>>,
1384 visible_count: usize,
1385 visible_keys: Option<Vec<SharedString>>,
1386 row_heights: Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
1387 scroll_now: crate::VirtualListHandle,
1388 list_state: Option<gpui::ListState>,
1389}
1390
1391struct HeaderFocus {
1393 header_focus: Vec<gpui::FocusHandle>,
1394 header_focused: Vec<bool>,
1395 resize_focus: Vec<Option<gpui::FocusHandle>>,
1396 resize_focused: Vec<bool>,
1397}
1398
1399struct ColumnTracks {
1401 resize_limits: Vec<(f32, f32)>,
1402 effective_widths: Vec<Option<Pixels>>,
1403 layout_width_bounds: Vec<(Option<Pixels>, Option<Pixels>)>,
1404 resize_columns: std::sync::Arc<Vec<TableColumn>>,
1405 resize_measurements: std::sync::Arc<Vec<Option<Pixels>>>,
1406}
1407
1408struct RowCollection {
1410 flat: Vec<FlatRow>,
1411 tree_rows: Vec<(bool, Option<SharedString>)>,
1412 visible_collection_keys: Vec<SharedString>,
1413 typeahead_labels: Vec<String>,
1414 virtual_typeahead_indices: Option<Vec<usize>>,
1415 cursor_at: Option<usize>,
1417 tree_column_has_children: bool,
1420}
1421
1422struct RowCtxRows {
1424 row_keys: Vec<SharedString>,
1425 cursor: Option<usize>,
1426 tree_column_has_children: bool,
1427 expanded: std::rc::Rc<Vec<SharedString>>,
1428 is_tree: bool,
1429 virtualized: bool,
1430 round_top: Option<Pixels>,
1431 round_bottom: Option<Pixels>,
1432 cell: Option<CellCtx>,
1433 frozen: Option<FrozenColumns>,
1434}
1435
1436impl RenderOnce for Table {
1437 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1438 let base_id: gpui::ElementId = self.id.clone().into();
1440 let column_order = self.apply_column_order(&base_id, window, cx);
1443 let frozen = self.frozen_columns(&base_id, window, cx);
1444 let (selected_keys, selection_own) = crate::util::controlled(
1445 window,
1446 cx,
1447 element_id::scoped(&base_id, "selected"),
1448 self.is_selection_controlled
1449 .then(|| self.selected_keys.clone()),
1450 self.default_selected_keys.clone(),
1451 );
1452 self.selected_keys = selected_keys;
1453 let state = self.table_state(&base_id, selection_own, window, cx);
1454 let mut virtual_body = self.virtual_body(&base_id, window, cx);
1455 let load_more_virtual_scroll = (self.row_height.is_some() && self.virtual_rows.is_some())
1456 .then(|| virtual_body.scroll_now.clone());
1457 let load_more_variable_scroll = virtual_body
1458 .list_state
1459 .clone()
1460 .zip(self.estimated_row_height)
1461 .map(|(list, estimate)| (list, virtual_body.visible_count, estimate));
1462 let focus = self.header_focus_state(&base_id, &state, window, cx);
1463
1464 let resizable = self.columns.iter().any(|c| c.allows_resizing);
1465 let tracks = self.column_tracks(&state, frozen.as_ref());
1466 let colors = cx.colors().clone();
1467 let muted = colors.muted;
1470 let secondary = self.variant == TableVariant::Secondary;
1471 let selectable = self.selection_mode != SelectionMode::None;
1472 let (selectable_collection_keys, load_more_collection) =
1473 self.selection_collections(&virtual_body);
1474
1475 let mut wrapper = self.wrapper_tray(&state.table_focus, &colors, secondary, cx);
1476
1477 let is_tree = self.virtual_tree_metadata.is_some()
1483 || self.rows.iter().any(|row| !row.children.is_empty());
1484 let mut table = self
1485 .grid_element(&base_id, selectable, is_tree, &virtual_body)
1486 .when(frozen.is_some(), |grid| grid.w_full());
1487
1488 let header_parts = HeaderParts {
1490 base_id: &base_id,
1491 colors: &colors,
1492 secondary,
1493 selectable,
1494 effective_widths: &tracks.effective_widths,
1495 layout_width_bounds: &tracks.layout_width_bounds,
1496 measured_widths: &state.measured_widths,
1497 measured_widths_now: &state.measured_widths_now,
1498 header_focus: &focus.header_focus,
1499 header_focused: &focus.header_focused,
1500 resize_focus: &focus.resize_focus,
1501 resize_focused: &focus.resize_focused,
1502 resized: &state.resized,
1503 dragging: &state.dragging,
1504 drag_now: state.drag_now,
1505 keyboard_resizing: &state.keyboard_resizing,
1506 keyboard_resize_now: state.keyboard_resize_now,
1507 resize_limits: &tracks.resize_limits,
1508 resize_columns: &tracks.resize_columns,
1509 resize_measurements: &tracks.resize_measurements,
1510 frozen: frozen.as_ref(),
1511 };
1512 let header = self.header_row(
1513 &header_parts,
1514 &column_order,
1515 &selectable_collection_keys,
1516 &state,
1517 window,
1518 cx,
1519 );
1520 table = table.child(header);
1521
1522 let mut body = gpui::div()
1527 .id(element_id::scoped(&base_id, "body"))
1528 .a11y(a11y::Role::RowGroup)
1529 .flex()
1530 .flex_col()
1531 .w_full();
1532 if !secondary {
1533 body = body
1534 .bg(colors.surface.background)
1535 .rounded(cx.layout().radius_2xl().min(px(32.)))
1536 .overflow_hidden();
1537 }
1538
1539 if resizable {
1540 table = self.column_resize_drag(
1541 table,
1542 state.dragging.clone(),
1543 state.resized.clone(),
1544 tracks.resize_columns.clone(),
1545 tracks.resize_measurements.clone(),
1546 );
1547 }
1548 let rows = self.row_collection(&mut virtual_body, &state, window, cx);
1550 let expanded_keys = std::rc::Rc::new(std::mem::take(&mut self.expanded_keys));
1551
1552 let table_id = self.id.clone();
1557 let cell_selection = self.cell_selection_state(
1558 &base_id,
1559 &column_order,
1560 &state.row_cursor,
1561 &state.table_focus,
1562 window,
1563 cx,
1564 );
1565 wrapper = self.cell_selection_keys(
1566 wrapper,
1567 &cell_selection,
1568 &rows.visible_collection_keys,
1569 frozen.clone(),
1570 );
1571 let (round_top, round_bottom) = self.row_edge_rounding(
1572 secondary,
1573 &virtual_body.scroll_now,
1574 virtual_body.list_state.as_ref(),
1575 virtual_body.visible_count,
1576 cx,
1577 );
1578 let RowCollection {
1579 flat,
1580 tree_rows,
1581 visible_collection_keys,
1582 typeahead_labels,
1583 virtual_typeahead_indices,
1584 cursor_at,
1585 tree_column_has_children,
1586 } = rows;
1587 let ctx = std::rc::Rc::new(self.row_ctx(
1588 &base_id,
1589 &state,
1590 &tracks,
1591 RowCtxRows {
1592 row_keys: visible_collection_keys,
1593 cursor: cursor_at,
1594 tree_column_has_children,
1595 expanded: expanded_keys.clone(),
1596 is_tree,
1597 virtualized: virtual_body.projection.is_some(),
1598 round_top,
1599 round_bottom,
1600 cell: cell_selection,
1601 frozen,
1602 },
1603 ));
1604
1605 wrapper = self.row_keyboard(
1606 wrapper,
1607 RowKeyboard {
1608 ctx: &ctx,
1609 expanded_keys,
1610 header_focus: focus.header_focus,
1611 row_cursor: state.row_cursor,
1612 selectable_collection_keys,
1613 selection_own: state.selection_own,
1614 selection_range: state.selection_range,
1615 table_focus: state.table_focus,
1616 tree_rows,
1617 typeahead_labels,
1618 typeahead: state.typeahead,
1619 virtual_list_state: &virtual_body.list_state,
1620 virtual_row_heights: &virtual_body.row_heights,
1621 virtual_scroll_now: &virtual_body.scroll_now,
1622 virtual_text_value: self.virtual_text_value.clone(),
1623 virtual_typeahead_indices,
1624 },
1625 );
1626
1627 let row_count = if self.virtual_rows.is_some()
1628 && (self.row_height.is_some() || self.estimated_row_height.is_some())
1629 {
1630 virtual_body.visible_count
1631 } else {
1632 flat.len()
1633 };
1634 let virtual_projection = virtual_body
1635 .projection
1636 .map(|projection| std::sync::Arc::clone(&projection.visible))
1637 .unwrap_or_default();
1638
1639 body = self.body_rows(
1640 body,
1641 BodyRows {
1642 base_id: &base_id,
1643 table_id: &table_id,
1644 ctx: &ctx,
1645 flat,
1646 row_count,
1647 virtual_projection,
1648 virtual_list_state: virtual_body.list_state,
1649 virtual_row_heights: virtual_body.row_heights,
1650 virtual_scroll_now: &virtual_body.scroll_now,
1651 },
1652 window,
1653 cx,
1654 );
1655
1656 if row_count == 0 {
1658 if let Some(content) = self.empty_state.take() {
1659 body = body.child(Self::empty_state_row(content, muted));
1660 }
1661 }
1662
1663 table = table.child(body);
1664
1665 if self.is_pending || self.on_load_more.is_some() {
1667 table = self.load_more(
1668 table,
1669 LoadMore {
1670 base_id: &base_id,
1671 state: state.load_more_state,
1672 collection: load_more_collection,
1673 virtual_scroll: load_more_virtual_scroll,
1674 variable_scroll: load_more_variable_scroll,
1675 muted,
1676 loading_text: crate::i18n::ui_string(crate::i18n::UiString::LoadingMore, cx),
1677 },
1678 );
1679 }
1680
1681 if let Some(footer) = self.footer.take() {
1682 table = table.child(Self::footer_row(footer));
1683 }
1684
1685 let el = wrapper.child(Self::scroll_container(&base_id, &table_id, table));
1686 crate::util::apply_sx(el, &self.sx)
1687 }
1688}
1689
1690impl Table {
1692 fn table_state(
1694 &self,
1695 base_id: &gpui::ElementId,
1696 selection_own: Option<gpui::Entity<Vec<SharedString>>>,
1697 window: &mut Window,
1698 cx: &mut App,
1699 ) -> TableState {
1700 let resized = window.use_keyed_state(element_id::scoped(base_id, "resized"), cx, |_, _| {
1703 Vec::<Option<Pixels>>::new()
1704 });
1705 let resized_now = resized.read(cx).clone();
1706 let measured_widths = window.use_keyed_state(
1707 element_id::scoped(base_id, "measured-widths"),
1708 cx,
1709 |_, _| Vec::<Option<Pixels>>::new(),
1710 );
1711 let measured_widths_now = measured_widths.read(cx).clone();
1712 let dragging =
1713 window.use_keyed_state(element_id::scoped(base_id, "resizing"), cx, |_, _| {
1714 None::<(usize, f32, f32)>
1715 });
1716 let drag_now = *dragging.read(cx);
1717 let keyboard_resizing = window.use_keyed_state(
1718 element_id::scoped(base_id, "keyboard-resizing"),
1719 cx,
1720 |_, _| None::<usize>,
1721 );
1722 let keyboard_resize_now = *keyboard_resizing.read(cx);
1723 let load_more_state = window.use_keyed_state(
1724 element_id::scoped(base_id, "load-more-state"),
1725 cx,
1726 |_, _| (false, None::<LoadMoreCollection>),
1727 );
1728 let table_focus =
1730 crate::util::tab_stop_handle(element_id::scoped(base_id, "focus"), window, cx);
1731 let row_cursor =
1732 window.use_keyed_state(element_id::scoped(base_id, "cursor"), cx, |_, _| {
1733 None::<SharedString>
1734 });
1735 let selection_range = window.use_keyed_state(
1736 element_id::scoped(base_id, "selection-range"),
1737 cx,
1738 |_, _| TableSelectionRange::default(),
1739 );
1740 let typeahead =
1741 window.use_keyed_state(element_id::scoped(base_id, "typeahead"), cx, |_, _| {
1742 TableTypeahead::default()
1743 });
1744 TableState {
1745 resized,
1746 resized_now,
1747 measured_widths,
1748 measured_widths_now,
1749 dragging,
1750 drag_now,
1751 keyboard_resizing,
1752 keyboard_resize_now,
1753 load_more_state,
1754 table_focus,
1755 row_cursor,
1756 selection_range,
1757 typeahead,
1758 selection_own,
1759 }
1760 }
1761
1762 fn frozen_columns(
1765 &self,
1766 base_id: &gpui::ElementId,
1767 window: &mut Window,
1768 cx: &mut App,
1769 ) -> Option<FrozenColumns> {
1770 let count = self.columns.iter().take_while(|c| c.frozen).count();
1771 if count == 0 || count >= self.columns.len() {
1772 return None;
1773 }
1774 let scroll = window
1775 .use_keyed_state(element_id::scoped(base_id, "frozen-scroll"), cx, |_, _| {
1776 gpui::ScrollHandle::new()
1777 })
1778 .read(cx)
1779 .clone();
1780 Some(FrozenColumns {
1781 count,
1782 scroll,
1783 view: window.current_view(),
1784 })
1785 }
1786
1787 fn virtual_body(
1789 &self,
1790 base_id: &gpui::ElementId,
1791 window: &mut Window,
1792 cx: &mut App,
1793 ) -> VirtualBody {
1794 let projection = self.virtual_projection(base_id, window, cx);
1795 let visible_count = projection
1796 .as_ref()
1797 .map_or(0, |projection| projection.visible.len());
1798 let visible_keys = projection.as_ref().map(|projection| {
1799 projection
1800 .visible
1801 .iter()
1802 .map(|(_, key, _)| key.clone())
1803 .collect::<Vec<_>>()
1804 });
1805 let row_heights = self.virtual_row_heights(base_id, visible_keys.as_ref(), window, cx);
1806 let virtual_scroll =
1807 window.use_keyed_state(element_id::scoped(base_id, "virtual-scroll"), cx, |_, _| {
1808 crate::VirtualListHandle::uniform(0)
1809 });
1810 let scroll_now = virtual_scroll.read(cx).clone();
1811 let list_state = self.virtual_list_state(base_id, visible_count, window, cx);
1812 VirtualBody {
1813 projection,
1814 visible_count,
1815 visible_keys,
1816 row_heights,
1817 scroll_now,
1818 list_state,
1819 }
1820 }
1821
1822 fn header_focus_state(
1825 &self,
1826 base_id: &gpui::ElementId,
1827 state: &TableState,
1828 window: &mut Window,
1829 cx: &mut App,
1830 ) -> HeaderFocus {
1831 let sortable = self.on_sort_change.is_some();
1837 let header_focus: Vec<gpui::FocusHandle> =
1843 self.header_focus_handles(base_id, sortable, window, cx);
1844 let ring_visible = crate::util::focus_visible(cx);
1845 let header_focused: Vec<bool> = header_focus
1846 .iter()
1847 .map(|h| h.is_focused(window) && ring_visible)
1848 .collect();
1849 let resize_focus: Vec<Option<gpui::FocusHandle>> =
1850 self.resize_focus_handles(base_id, window, cx);
1851 let resize_focused: Vec<bool> = resize_focus
1852 .iter()
1853 .map(|h| h.as_ref().is_some_and(|h| h.is_focused(window)) && ring_visible)
1854 .collect();
1855 if state.keyboard_resize_now.is_some_and(|column_index| {
1856 resize_focus
1857 .get(column_index)
1858 .and_then(Option::as_ref)
1859 .is_none()
1860 }) {
1861 state.keyboard_resizing.update(cx, |active, cx| {
1862 *active = None;
1863 cx.notify();
1864 });
1865 }
1866 HeaderFocus {
1867 header_focus,
1868 header_focused,
1869 resize_focus,
1870 resize_focused,
1871 }
1872 }
1873
1874 fn column_tracks(&self, state: &TableState, frozen: Option<&FrozenColumns>) -> ColumnTracks {
1877 let resize_limits = column_resize_limits(&self.columns);
1878 let mut effective_widths =
1879 effective_column_widths(&self.columns, &state.resized_now, &resize_limits);
1880 let layout_width_bounds = layout_width_bounds(
1892 &self.columns,
1893 &state.measured_widths_now,
1894 &effective_widths,
1895 &resize_limits,
1896 );
1897 if let Some(frozen) = frozen {
1903 for (c, column) in self.columns.iter().enumerate().take(frozen.count) {
1904 let measured = state.measured_widths_now.get(c).copied().flatten();
1905 if effective_widths[c].is_none() && (!column.allows_resizing || measured.is_some())
1906 {
1907 effective_widths[c] = layout_width_bounds[c].0;
1908 }
1909 }
1910 }
1911 ColumnTracks {
1912 resize_limits,
1913 effective_widths,
1914 layout_width_bounds,
1915 resize_columns: std::sync::Arc::new(self.columns.clone()),
1916 resize_measurements: std::sync::Arc::new(state.measured_widths_now.clone()),
1917 }
1918 }
1919
1920 fn selection_collections(
1923 &self,
1924 virtual_body: &VirtualBody,
1925 ) -> (
1926 Option<std::sync::Arc<Vec<SharedString>>>,
1927 LoadMoreCollection,
1928 ) {
1929 let needs_selectable_keys = self.selection_mode == SelectionMode::Multiple;
1930 let non_virtual_keys = virtual_body.projection.is_none().then(|| self.row_keys());
1934 let selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>> =
1935 needs_selectable_keys.then(|| match virtual_body.projection.as_ref() {
1936 Some(projection) => projection
1937 .selectable_collection_keys
1938 .as_ref()
1939 .expect("multiple virtual Tables cache selectable keys")
1940 .clone(),
1941 None => filtered_selectable_keys(
1942 non_virtual_keys
1943 .as_ref()
1944 .expect("multiple non-virtual Tables have row keys")
1945 .as_slice(),
1946 &self.disabled_keys,
1947 ),
1948 });
1949 let load_more_collection = match &self.virtual_rows {
1950 Some((count, identity, _, _)) => LoadMoreCollection::Virtual {
1951 count: *count,
1952 identity: identity.clone(),
1953 },
1954 None => LoadMoreCollection::Rows(non_virtual_keys.unwrap_or_default()),
1955 };
1956 (selectable_collection_keys, load_more_collection)
1957 }
1958
1959 fn wrapper_tray(
1962 &self,
1963 table_focus: &gpui::FocusHandle,
1964 colors: &herogpui_theme::ThemeColors,
1965 secondary: bool,
1966 cx: &App,
1967 ) -> gpui::Div {
1968 let mut wrapper = gpui::div()
1969 .w_full()
1970 .flex()
1971 .flex_col()
1972 .track_focus(table_focus)
1973 .overflow_hidden()
1974 .rounded(
1975 self.radius
1976 .unwrap_or_else(|| crate::util::container_radius(cx)),
1977 )
1978 .text_color(colors.foreground);
1979 if !secondary {
1989 let radius = cx.layout().radius_lg() * 2.5;
1990 wrapper = wrapper
1991 .bg(colors.surface_secondary)
1992 .rounded(radius.min(px(32.)))
1993 .px(px(4.))
1994 .pb(px(4.));
1995 }
1996 wrapper
1997 }
1998
1999 fn grid_element(
2002 &self,
2003 base_id: &gpui::ElementId,
2004 selectable: bool,
2005 is_tree: bool,
2006 virtual_body: &VirtualBody,
2007 ) -> gpui::Stateful<gpui::Div> {
2008 let grid_column_count = self.columns.len() + usize::from(selectable);
2031 let virtual_row_total = virtual_body
2032 .projection
2033 .as_ref()
2034 .map(|_| virtual_body.visible_count);
2035 let mut table = gpui::div()
2036 .flex()
2037 .flex_col()
2038 .min_w_full()
2039 .min_h_0()
2040 .text_size(px(14.))
2041 .line_height(px(20.))
2042 .when_some(self.gap, |el, g| el.gap(g))
2043 .when_some(self.padding, |el, p| el.p(p))
2044 .id(element_id::scoped(base_id, "grid"))
2064 .a11y(if is_tree {
2065 a11y::Role::TreeGrid
2066 } else {
2067 a11y::Role::Grid
2068 });
2069 if let Some(rows) = virtual_row_total {
2070 table = table.a11y_grid_size(rows + 1, grid_column_count);
2071 }
2072 table
2073 }
2074
2075 fn header_row(
2078 &self,
2079 hx: &HeaderParts<'_>,
2080 column_order: &std::rc::Rc<Vec<usize>>,
2081 selectable_collection_keys: &Option<std::sync::Arc<Vec<SharedString>>>,
2082 state: &TableState,
2083 window: &mut Window,
2084 cx: &mut App,
2085 ) -> gpui::Stateful<gpui::Div> {
2086 let base_id = hx.base_id;
2087 let frozen = hx.frozen;
2088 let colors = hx.colors;
2089 let secondary = hx.secondary;
2090 let measured = if frozen.is_some() {
2093 gpui::div()
2094 } else {
2095 self.column_reorder_measure(gpui::div(), base_id, window, cx)
2096 };
2097 let mut header = measured
2098 .id(element_id::scoped(base_id, "header"))
2099 .a11y(a11y::Role::Row)
2106 .flex()
2107 .border_b_1()
2108 .border_color(colors.separator.alpha(0.5))
2109 .when(!secondary, |h| h.bg(colors.surface_secondary));
2110
2111 let mut cells: Vec<AnyElement> = Vec::new();
2112 if hx.selectable {
2113 cells.push(
2114 self.select_all_header_cell(
2115 base_id,
2116 colors,
2117 secondary,
2118 selectable_collection_keys,
2119 &state.selection_own,
2120 &state.selection_range,
2121 cx,
2122 )
2123 .into_any_element(),
2124 );
2125 }
2126
2127 for (column_index, column) in self.columns.iter().enumerate() {
2128 cells.push(self.column_header_cell(hx, column_index, column, window, cx));
2129 }
2130 header = match frozen {
2131 Some(frozen) => {
2132 let (lead, rest) = self.frozen_header_parts(base_id, frozen, cells, window, cx);
2133 header.child(lead).child(rest)
2134 }
2135 None => header.children(cells),
2136 };
2137 self.column_reorder_header(
2138 header,
2139 base_id,
2140 column_order,
2141 hx.header_focus,
2142 hx.dragging,
2143 window,
2144 cx,
2145 )
2146 }
2147
2148 fn row_collection(
2151 &mut self,
2152 virtual_body: &mut VirtualBody,
2153 state: &TableState,
2154 window: &mut Window,
2155 cx: &mut App,
2156 ) -> RowCollection {
2157 let virtual_projection = &virtual_body.projection;
2158 let mut flat: Vec<FlatRow> = Vec::new();
2159 flatten_rows(
2160 std::mem::take(&mut self.rows),
2161 0,
2162 "",
2163 None,
2164 &self.expanded_keys,
2165 &mut flat,
2166 );
2167 let tree_rows: Vec<(bool, Option<SharedString>)> = virtual_projection.as_ref().map_or_else(
2168 || {
2169 flat.iter()
2170 .map(|(_, _, has_children, _, parent)| (*has_children, parent.clone()))
2171 .collect()
2172 },
2173 |projection| {
2174 projection
2175 .visible
2176 .iter()
2177 .map(|(_, _, metadata)| (metadata.has_children, metadata.parent_key.clone()))
2178 .collect()
2179 },
2180 );
2181 let visible_collection_keys: Vec<SharedString> = virtual_body
2182 .visible_keys
2183 .take()
2184 .unwrap_or_else(|| flat.iter().map(|(_, _, _, key, _)| key.clone()).collect());
2185 let typeahead_labels: Vec<String> = virtual_projection.as_ref().map_or_else(
2186 || {
2187 flat.iter()
2188 .map(|(row, _, _, _, _)| {
2189 row.text_value
2190 .as_ref()
2191 .map(ToString::to_string)
2192 .unwrap_or_default()
2193 })
2194 .collect()
2195 },
2196 |_| Vec::new(),
2197 );
2198 let virtual_typeahead_indices = virtual_projection.as_ref().map(|projection| {
2199 projection
2200 .visible
2201 .iter()
2202 .map(|(source_index, _, _)| *source_index)
2203 .collect::<Vec<_>>()
2204 });
2205 let row_cursor = &state.row_cursor;
2206 let table_focus = &state.table_focus;
2207 let cursor_key = row_cursor.read(cx).clone();
2208 let cursor_valid = cursor_key.as_ref().is_none_or(|key| {
2209 visible_collection_keys.contains(key) && !self.disabled_keys.contains(key)
2210 });
2211 if !cursor_valid {
2212 row_cursor.update(cx, |key, cx| {
2213 *key = None;
2214 cx.notify();
2215 });
2216 }
2217 let cursor_at = if window.is_window_active() && table_focus.is_focused(window) {
2218 cursor_key
2219 .as_ref()
2220 .and_then(|key| visible_collection_keys.iter().position(|row| row == key))
2221 .filter(|index| {
2222 !self
2223 .disabled_keys
2224 .contains(&visible_collection_keys[*index])
2225 })
2226 .filter(|_| crate::util::focus_visible(cx))
2227 } else {
2228 None
2229 };
2230 let tree_column_has_children = virtual_projection.as_ref().map_or_else(
2233 || flat.iter().any(|(_, _, has, _, _)| *has),
2234 |projection| {
2235 projection
2236 .visible
2237 .iter()
2238 .any(|(_, _, metadata)| metadata.has_children)
2239 },
2240 );
2241 RowCollection {
2242 flat,
2243 tree_rows,
2244 visible_collection_keys,
2245 typeahead_labels,
2246 virtual_typeahead_indices,
2247 cursor_at,
2248 tree_column_has_children,
2249 }
2250 }
2251
2252 fn row_ctx(
2255 &self,
2256 base_id: &gpui::ElementId,
2257 state: &TableState,
2258 tracks: &ColumnTracks,
2259 rows: RowCtxRows,
2260 ) -> RowCtx {
2261 RowCtx {
2262 id: base_id.clone(),
2263 measured_widths: state.measured_widths.clone(),
2264 measure_intrinsic: self
2265 .columns
2266 .iter()
2267 .map(|column| !column.allows_resizing && !cfg!(target_arch = "wasm32"))
2275 .collect(),
2276 widths: self
2277 .columns
2278 .iter()
2279 .enumerate()
2280 .map(|(c, _)| {
2281 let (layout_min, layout_max) = tracks.layout_width_bounds[c];
2282 (
2283 tracks.effective_widths.get(c).copied().flatten(),
2284 layout_min,
2285 layout_max,
2286 )
2287 })
2288 .collect(),
2289 row_header_columns: self.columns.iter().map(|c| c.is_row_header).collect(),
2290 tree_column: self.tree_column,
2291 tree_column_has_children: rows.tree_column_has_children,
2292 selectable: self.selection_mode != SelectionMode::None,
2293 selection_mode: self.selection_mode,
2294 selection_behavior: self.selection_behavior,
2295 selected_keys: self.selected_keys.clone(),
2296 row_keys: rows.row_keys,
2297 disabled_keys: self.disabled_keys.clone(),
2298 disallow_empty_selection: self.disallow_empty_selection,
2299 expanded: rows.expanded,
2300 on_expanded_change: self.on_expanded_change.clone(),
2301 on_selection_change: self.on_selection_change.clone(),
2302 selection_own: state.selection_own.clone(),
2303 selection_range: state.selection_range.clone(),
2304 on_row_click: self.on_row_click.clone(),
2305 focus: state.table_focus.clone(),
2306 cursor_own: state.row_cursor.clone(),
2307 cursor: rows.cursor,
2308 secondary: self.variant == TableVariant::Secondary,
2309 row_hover_bg: self.row_hover_bg,
2310 hover_group_prefix: self.id.clone(),
2311 virtualized: rows.virtualized,
2312 round_top: rows.round_top,
2313 round_bottom: rows.round_bottom,
2314 is_tree: rows.is_tree,
2315 cell: rows.cell,
2316 frozen: rows.frozen,
2317 }
2318 }
2319
2320 fn empty_state_row(content: AnyElement, muted: gpui::Hsla) -> gpui::Div {
2322 gpui::div()
2323 .flex()
2324 .items_center()
2325 .justify_center()
2326 .w_full()
2327 .py(px(28.))
2328 .text_color(muted)
2329 .child(content)
2330 }
2331
2332 fn footer_row(footer: AnyElement) -> gpui::Div {
2334 gpui::div()
2335 .flex()
2336 .items_center()
2337 .w_full()
2338 .px(px(16.))
2339 .py(px(10.))
2340 .child(footer)
2341 }
2342
2343 fn scroll_container(
2345 base_id: &gpui::ElementId,
2346 table_id: &SharedString,
2347 table: gpui::Stateful<gpui::Div>,
2348 ) -> gpui::Stateful<gpui::Div> {
2349 let scroll_selector = format!("{table_id}-scroll-x");
2363 gpui::div()
2364 .id(element_id::scoped(base_id, "scroll-x"))
2365 .debug_selector(move || scroll_selector)
2366 .flex()
2367 .flex_col()
2368 .items_start()
2369 .w_full()
2370 .min_h_0()
2371 .overflow_x_scroll()
2372 .restrict_scroll_to_axis()
2373 .child(table)
2374 }
2375}
2376
2377fn column_resize_limits(columns: &[TableColumn]) -> Vec<(f32, f32)> {
2379 columns
2380 .iter()
2381 .map(|column| {
2382 (
2383 column.min_width.map_or(DEFAULT_COLUMN_MIN_WIDTH, f32::from),
2384 column.max_width.map_or(f32::MAX, f32::from),
2385 )
2386 })
2387 .collect()
2388}
2389
2390fn effective_column_widths(
2394 columns: &[TableColumn],
2395 resized_now: &[Option<Pixels>],
2396 resize_limits: &[(f32, f32)],
2397) -> Vec<Option<Pixels>> {
2398 columns
2399 .iter()
2400 .enumerate()
2401 .map(|(column_index, column)| {
2402 let width = column.width.or_else(|| {
2403 resized_now
2404 .get(column_index)
2405 .copied()
2406 .flatten()
2407 .or(column.default_width)
2408 });
2409 if column.allows_resizing {
2410 let (min, max) = resize_limits[column_index];
2411 width.map(|width| px(f32::from(width).floor().min(max).max(min)))
2412 } else {
2413 width
2414 }
2415 })
2416 .collect()
2417}
2418
2419fn layout_width_bounds(
2422 columns: &[TableColumn],
2423 measured_widths_now: &[Option<Pixels>],
2424 effective_widths: &[Option<Pixels>],
2425 resize_limits: &[(f32, f32)],
2426) -> Vec<(Option<Pixels>, Option<Pixels>)> {
2427 columns
2428 .iter()
2429 .enumerate()
2430 .map(|(column_index, column)| {
2431 let measured = measured_widths_now.get(column_index).copied().flatten();
2432 if column.allows_resizing {
2433 if effective_widths[column_index].is_some() {
2434 (None, None)
2435 } else {
2436 let min = measured.map_or(resize_limits[column_index].0, |width| {
2437 resize_limits[column_index].0.max(f32::from(width))
2438 });
2439 (Some(px(min)), column.max_width)
2440 }
2441 } else {
2442 let min = match (column.min_width, measured) {
2443 (Some(configured), Some(content)) => {
2444 Some(px(f32::from(configured).max(f32::from(content))))
2445 }
2446 (Some(configured), None) => Some(configured),
2447 (None, Some(content)) => Some(content),
2448 (None, None) => None,
2449 };
2450 (min, column.max_width)
2451 }
2452 })
2453 .collect()
2454}
2455
2456#[derive(Clone, Copy)]
2459struct HeaderParts<'a> {
2460 base_id: &'a gpui::ElementId,
2461 colors: &'a herogpui_theme::ThemeColors,
2462 secondary: bool,
2463 selectable: bool,
2464 effective_widths: &'a [Option<Pixels>],
2465 layout_width_bounds: &'a [(Option<Pixels>, Option<Pixels>)],
2466 measured_widths: &'a gpui::Entity<Vec<Option<Pixels>>>,
2467 measured_widths_now: &'a [Option<Pixels>],
2468 header_focus: &'a [gpui::FocusHandle],
2469 header_focused: &'a [bool],
2470 resize_focus: &'a [Option<gpui::FocusHandle>],
2471 resize_focused: &'a [bool],
2472 resized: &'a gpui::Entity<Vec<Option<Pixels>>>,
2473 dragging: &'a gpui::Entity<Option<(usize, f32, f32)>>,
2474 drag_now: Option<(usize, f32, f32)>,
2475 keyboard_resizing: &'a gpui::Entity<Option<usize>>,
2476 keyboard_resize_now: Option<usize>,
2477 resize_limits: &'a [(f32, f32)],
2478 resize_columns: &'a std::sync::Arc<Vec<TableColumn>>,
2479 resize_measurements: &'a std::sync::Arc<Vec<Option<Pixels>>>,
2480 frozen: Option<&'a FrozenColumns>,
2481}
2482
2483type FlatRow = (TableRow, usize, bool, SharedString, Option<SharedString>);
2486
2487fn flatten_rows(
2490 rows: Vec<TableRow>,
2491 depth: usize,
2492 path: &str,
2493 parent: Option<&SharedString>,
2494 expanded: &[SharedString],
2495 out: &mut Vec<FlatRow>,
2496) {
2497 for (index, mut row) in rows.into_iter().enumerate() {
2498 let key = match &row.key {
2499 Some(k) => k.clone(),
2500 None if path.is_empty() => SharedString::from(index.to_string()),
2501 None => SharedString::from(format!("{path}-{index}")),
2502 };
2503 let children = std::mem::take(&mut row.children);
2504 let has_children = !children.is_empty();
2505 let is_open = expanded.contains(&key);
2506 out.push((row, depth, has_children, key.clone(), parent.cloned()));
2507 if has_children && is_open {
2508 flatten_rows(children, depth + 1, key.as_ref(), Some(&key), expanded, out);
2509 }
2510 }
2511}
2512
2513struct RowKeyboard<'a> {
2515 ctx: &'a RowCtx,
2516 expanded_keys: std::rc::Rc<Vec<SharedString>>,
2517 header_focus: Vec<gpui::FocusHandle>,
2518 row_cursor: gpui::Entity<Option<SharedString>>,
2519 selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>>,
2520 selection_own: Option<gpui::Entity<Vec<SharedString>>>,
2521 selection_range: gpui::Entity<TableSelectionRange>,
2522 table_focus: gpui::FocusHandle,
2523 tree_rows: Vec<(bool, Option<SharedString>)>,
2524 typeahead_labels: Vec<String>,
2525 typeahead: gpui::Entity<TableTypeahead>,
2526 virtual_list_state: &'a Option<gpui::ListState>,
2527 virtual_row_heights: &'a Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
2528 virtual_scroll_now: &'a crate::VirtualListHandle,
2529 virtual_text_value: Option<VirtualRowText>,
2530 virtual_typeahead_indices: Option<Vec<usize>>,
2531}
2532
2533struct BodyRows<'a> {
2535 base_id: &'a gpui::ElementId,
2536 table_id: &'a SharedString,
2537 ctx: &'a std::rc::Rc<RowCtx>,
2538 flat: Vec<FlatRow>,
2539 row_count: usize,
2540 virtual_projection: std::sync::Arc<Vec<(usize, SharedString, VirtualTreeMetadata)>>,
2541 virtual_list_state: Option<gpui::ListState>,
2542 virtual_row_heights: Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
2543 virtual_scroll_now: &'a crate::VirtualListHandle,
2544}
2545
2546struct LoadMore<'a> {
2548 base_id: &'a gpui::ElementId,
2549 state: gpui::Entity<(bool, Option<LoadMoreCollection>)>,
2550 collection: LoadMoreCollection,
2551 virtual_scroll: Option<crate::VirtualListHandle>,
2552 variable_scroll: Option<(gpui::ListState, usize, Pixels)>,
2553 muted: gpui::Hsla,
2554 loading_text: SharedString,
2556}
2557
2558struct RowKeyNav {
2561 stops: Vec<usize>,
2563 held: gpui::Entity<Option<SharedString>>,
2565 keys: Vec<SharedString>,
2566 table_focus: gpui::FocusHandle,
2567 typeahead: std::rc::Rc<TableTypeaheadNavigation>,
2568 on_row_click: Option<OnRowClick>,
2569 selection: Option<OnSelectionChange>,
2570 selection_own: Option<gpui::Entity<Vec<SharedString>>>,
2571 selection_range: gpui::Entity<TableSelectionRange>,
2572 selected_now: Vec<SharedString>,
2573 selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>>,
2574 mode: SelectionMode,
2575 selection_behavior: SelectionBehavior,
2576 disallow_empty_selection: bool,
2577 select_on_focus: bool,
2578 plain_rows: bool,
2579 fixed_virtual: bool,
2580 fixed_row_height: Option<Pixels>,
2581 fixed_scroll: crate::VirtualListHandle,
2582 variable_scroll: Option<gpui::ListState>,
2583 variable_heights: Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
2584 variable_estimate: Option<Pixels>,
2585 expanded: std::rc::Rc<Vec<SharedString>>,
2586 on_expanded: Option<OnExpandedChange>,
2587 tree_rows: Vec<(bool, Option<SharedString>)>,
2588 page_up_header: Option<gpui::FocusHandle>,
2589 headers: Vec<gpui::FocusHandle>,
2590}
2591
2592impl RowKeyNav {
2593 fn key_down(&self, event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App) {
2595 if !self.table_focus.contains_focused(window, cx) {
2596 return;
2597 }
2598 let stops = &self.stops;
2599 let keys = &self.keys;
2600 let from = self
2601 .held
2602 .read(cx)
2603 .as_ref()
2604 .and_then(|key| keys.iter().position(|row| row == key))
2605 .filter(|index| stops.contains(index));
2606 let key_name = event.keystroke.key.as_str();
2610 let typed = event.keystroke.key_char.as_deref().unwrap_or(key_name);
2611 let modifiers = &event.keystroke.modifiers;
2612 let now = web_time::Instant::now();
2613 let is_space = key_name == "space" || typed == " ";
2614 let is_character = {
2615 let mut chars = key_name.chars();
2616 chars.next().is_some() && chars.next().is_none() && !is_space
2617 };
2618 let is_typeahead =
2619 is_character && !modifiers.control && !modifiers.platform && !modifiers.alt;
2620 if is_typeahead {
2621 if self.typeahead.push(typed, now, true, cx) {
2622 cx.stop_propagation();
2623 }
2624 return;
2625 }
2626 if key_name == "a"
2632 && event.keystroke.modifiers.secondary()
2633 && !event.keystroke.modifiers.shift
2634 && !event.keystroke.modifiers.alt
2635 && !event.keystroke.modifiers.function
2636 && if cfg!(target_os = "macos") {
2637 !event.keystroke.modifiers.control
2638 } else {
2639 !event.keystroke.modifiers.platform
2640 }
2641 && self.mode == SelectionMode::Multiple
2642 {
2643 self.select_all(window, cx);
2644 cx.stop_propagation();
2645 return;
2646 }
2647 if self.headers.iter().any(|header| header.is_focused(window)) {
2654 self.enter_from_header(key_name, window, cx);
2655 return;
2656 }
2657 if !self.table_focus.is_focused(window) {
2658 return;
2659 }
2660 if key_name == "escape"
2661 && self.mode != SelectionMode::None
2662 && !self.disallow_empty_selection
2663 && !self.selected_now.is_empty()
2664 {
2665 self.commit(&[], window, cx);
2666 self.selection_range.update(cx, |range, _| {
2667 *range = TableSelectionRange::default();
2668 });
2669 cx.stop_propagation();
2670 return;
2671 }
2672 if let Some(index) = from {
2673 if self.tree_key(index, key_name, window, cx) {
2674 return;
2675 }
2676 }
2677 let (fixed_page_move, variable_page_move) = self.page_moves(from, key_name, cx);
2678 let is_variable_page = fixed_page_move.is_none() && variable_page_move.is_some();
2679 if self.page_up_to_header(
2680 from,
2681 key_name,
2682 fixed_page_move,
2683 variable_page_move,
2684 window,
2685 cx,
2686 ) {
2687 return;
2688 }
2689 let plain_page_move = from
2690 .filter(|_| self.plain_rows)
2691 .and_then(|_| match key_name {
2692 "pagedown" => stops.last().copied(),
2693 _ => None,
2694 });
2695 let page_move = fixed_page_move
2696 .or(variable_page_move)
2697 .or(plain_page_move)
2698 .filter(|next| Some(*next) != from);
2699 if matches!(key_name, "home" | "end")
2705 && !home_end_registered(*modifiers, cfg!(target_os = "macos"))
2706 {
2707 return;
2708 }
2709 let initial_home_end_extends =
2710 shift_home_end_extends("home", modifiers.control, cfg!(target_os = "macos"));
2711 let initial_shift_settle = from.is_none()
2712 && modifiers.shift
2713 && (key_name == "up"
2714 || (matches!(key_name, "home" | "end") && !initial_home_end_extends)
2715 || (key_name == "down" && stops.len() < 2));
2716 let navigation = if from.is_none() && modifiers.shift && key_name == "down" {
2717 stops
2718 .get(1)
2719 .or_else(|| stops.first())
2720 .copied()
2721 .map_or(crate::list_nav::Move::Ignore, crate::list_nav::Move::To)
2722 } else if initial_shift_settle {
2723 (if key_name == "end" {
2724 stops.last()
2725 } else {
2726 stops.first()
2727 })
2728 .copied()
2729 .map_or(crate::list_nav::Move::Ignore, crate::list_nav::Move::To)
2730 } else {
2731 page_move.map_or_else(
2732 || crate::list_nav::resolve(stops, from, key_name, false),
2733 crate::list_nav::Move::To,
2734 )
2735 };
2736 match navigation {
2737 crate::list_nav::Move::To(next) => {
2738 self.move_cursor(
2739 next,
2740 from,
2741 key_name,
2742 *modifiers,
2743 initial_shift_settle,
2744 is_variable_page,
2745 window,
2746 cx,
2747 );
2748 }
2749 crate::list_nav::Move::Activate => {
2750 let Some(index) = from else { return };
2751 self.activate(index, event, *modifiers, window, cx);
2752 }
2753 crate::list_nav::Move::Ignore => {}
2754 }
2755 }
2756
2757 fn commit(&self, next: &[SharedString], window: &mut Window, cx: &mut App) {
2759 if let Some(held) = &self.selection_own {
2760 held.update(cx, |value, cx| {
2761 *value = next.to_vec();
2762 cx.notify();
2763 });
2764 }
2765 if let Some(cb) = &self.selection {
2766 cb(next, window, cx);
2767 }
2768 }
2769
2770 fn selectable_keys(&self) -> &[SharedString] {
2771 self.selectable_collection_keys
2772 .as_ref()
2773 .expect("multiple Tables have selectable keys")
2774 .as_slice()
2775 }
2776
2777 fn select_all(&self, window: &mut Window, cx: &mut App) {
2779 let selectable_collection_keys = self
2780 .selectable_collection_keys
2781 .as_ref()
2782 .expect("multiple Tables have selectable keys");
2783 let selectable_collection_keys = selectable_collection_keys.clone();
2784 let selectable_collection_keys = selectable_collection_keys.as_slice();
2785 let (all_selected, _) = select_all_flags(selectable_collection_keys, &self.selected_now);
2786 let materializes_all = same_selection(selectable_collection_keys, &self.selected_now);
2787 let already_all =
2788 all_selected || (self.selection_range.read(cx).is_all && materializes_all);
2789 if !already_all {
2792 let next = selectable_collection_keys.to_vec();
2793 self.commit(&next, window, cx);
2794 self.selection_range.update(cx, |range, _| {
2795 *range = TableSelectionRange {
2796 is_all: true,
2797 ..TableSelectionRange::default()
2798 };
2799 });
2800 }
2801 }
2802
2803 fn enter_from_header(&self, key_name: &str, window: &mut Window, cx: &mut App) {
2805 let next = match key_name {
2806 "down" => self.stops.first(),
2807 "pagedown" => self.stops.last(),
2808 _ => None,
2809 };
2810 if let Some(next) = next {
2811 self.held.update(cx, |value, cx| {
2812 *value = Some(self.keys[*next].clone());
2813 cx.notify();
2814 });
2815 window.focus(&self.table_focus, cx);
2816 cx.stop_propagation();
2817 }
2818 }
2819
2820 fn tree_key(&self, index: usize, key_name: &str, window: &mut Window, cx: &mut App) -> bool {
2822 let keys = &self.keys;
2823 let expanded = &self.expanded;
2824 let focused_key = &keys[index];
2825 let Some((has_children, parent)) = self.tree_rows.get(index) else {
2826 return false;
2827 };
2828 if key_name == "right" && *has_children && !expanded.contains(focused_key) {
2829 if let Some(cb) = &self.on_expanded {
2830 let mut next = expanded.as_ref().clone();
2831 next.push(focused_key.clone());
2832 cb(&next, window, cx);
2833 }
2834 crate::util::set_focus_visible(true, cx);
2835 cx.stop_propagation();
2836 return true;
2837 } else if key_name == "left" {
2838 if *has_children && expanded.contains(focused_key) {
2839 if let Some(cb) = &self.on_expanded {
2840 let mut next = expanded.as_ref().clone();
2841 next.retain(|key| key != focused_key);
2842 cb(&next, window, cx);
2843 }
2844 crate::util::set_focus_visible(true, cx);
2845 cx.stop_propagation();
2846 return true;
2847 } else if let Some(parent) = parent {
2848 if let Some(parent_index) = keys.iter().position(|key| key == parent) {
2849 self.held.update(cx, |value, cx| {
2850 *value = Some(parent.clone());
2851 cx.notify();
2852 });
2853 if self.fixed_virtual {
2854 self.fixed_scroll
2855 .scroll_to_item(parent_index, crate::VirtualListScroll::Center);
2856 } else if let Some(state) = &self.variable_scroll {
2857 state.scroll_to(gpui::ListOffset {
2858 item_ix: parent_index,
2859 offset_in_item: px(0.),
2860 });
2861 }
2862 crate::util::set_focus_visible(true, cx);
2863 cx.stop_propagation();
2864 return true;
2865 }
2866 }
2867 }
2868 false
2869 }
2870
2871 fn page_moves(
2874 &self,
2875 from: Option<usize>,
2876 key_name: &str,
2877 cx: &App,
2878 ) -> (Option<usize>, Option<usize>) {
2879 let stops = &self.stops;
2880 let keys = &self.keys;
2881 let page_by_step = |from: usize, step: usize| match key_name {
2882 "pagedown" => {
2883 let boundary = from.saturating_add(step).min(keys.len() - 1);
2884 stops
2885 .iter()
2886 .copied()
2887 .find(|stop| *stop >= boundary)
2888 .or_else(|| stops.last().copied())
2889 }
2890 "pageup" => {
2891 let boundary = from.saturating_sub(step);
2892 stops
2893 .iter()
2894 .rev()
2895 .copied()
2896 .find(|stop| *stop <= boundary)
2897 .or_else(|| stops.first().copied())
2898 }
2899 _ => None,
2900 };
2901 let fixed_page_move = from.and_then(|from| {
2902 let row_height = self.fixed_row_height?;
2903 let viewport_height = f32::from(self.fixed_scroll.viewport_bounds().size.height);
2904 if viewport_height <= 0. {
2905 return None;
2906 }
2907 let step =
2908 ((viewport_height / f32::from(row_height)).ceil() as usize).saturating_sub(1);
2909 page_by_step(from, step)
2910 });
2911 let variable_page_move = from.and_then(|from| {
2912 let viewport_height = self.variable_scroll.as_ref()?.viewport_bounds().size.height;
2913 let heights = self.variable_heights.as_ref()?.read(cx);
2914 let estimate = self.variable_estimate?;
2915 let height_at =
2916 |index: usize| heights.1.get(index).copied().flatten().unwrap_or(estimate);
2917 let mut distance = height_at(from);
2918 let mut target = from;
2919 match key_name {
2920 "pagedown" => {
2921 if distance >= viewport_height {
2922 return Some(target);
2923 }
2924 for next in stops.iter().copied().filter(|next| *next > from) {
2925 for index in target + 1..=next {
2926 distance += height_at(index);
2927 }
2928 target = next;
2929 if distance >= viewport_height {
2930 break;
2931 }
2932 }
2933 Some(target)
2934 }
2935 "pageup" => {
2936 if distance >= viewport_height {
2937 return Some(target);
2938 }
2939 for previous in stops
2940 .iter()
2941 .rev()
2942 .copied()
2943 .filter(|previous| *previous < from)
2944 {
2945 for index in previous..target {
2946 distance += height_at(index);
2947 }
2948 target = previous;
2949 if distance >= viewport_height {
2950 break;
2951 }
2952 }
2953 Some(target)
2954 }
2955 _ => None,
2956 }
2957 });
2958 (fixed_page_move, variable_page_move)
2959 }
2960
2961 fn page_up_to_header(
2964 &self,
2965 from: Option<usize>,
2966 key_name: &str,
2967 fixed_page_move: Option<usize>,
2968 variable_page_move: Option<usize>,
2969 window: &mut Window,
2970 cx: &mut App,
2971 ) -> bool {
2972 let plain_rows = self.plain_rows;
2983 if plain_rows && key_name == "pageup" {
2984 if let Some(header) = &self.page_up_header {
2985 window.focus(header, cx);
2986 cx.stop_propagation();
2987 return true;
2988 }
2989 } else if !plain_rows && key_name == "pageup" {
2990 let at_top = match from {
2991 Some(position) => {
2992 self.stops.first().is_some_and(|first| *first == position)
2993 || fixed_page_move.is_some_and(|target| Some(target) == from)
2994 || variable_page_move.is_some_and(|target| Some(target) == from)
2995 }
2996 None => false,
2997 };
2998 if at_top {
2999 if let Some(header) = &self.page_up_header {
3000 window.focus(header, cx);
3001 cx.stop_propagation();
3002 return true;
3003 }
3004 }
3005 }
3006 false
3007 }
3008
3009 #[allow(clippy::too_many_arguments)]
3012 fn move_cursor(
3013 &self,
3014 next: usize,
3015 from: Option<usize>,
3016 key_name: &str,
3017 modifiers: gpui::Modifiers,
3018 initial_shift_settle: bool,
3019 is_variable_page: bool,
3020 window: &mut Window,
3021 cx: &mut App,
3022 ) {
3023 let keys = &self.keys;
3024 let exact_shift_navigation = if cfg!(target_os = "macos") {
3028 !modifiers.control && !modifiers.platform && !modifiers.function
3029 } else {
3030 !modifiers.alt && !modifiers.platform && !modifiers.function
3031 };
3032 let extends_selection = modifiers.shift
3033 && self.mode == SelectionMode::Multiple
3034 && exact_shift_navigation
3035 && !initial_shift_settle
3036 && Some(next) != from
3037 && shift_home_end_extends(key_name, modifiers.control, cfg!(target_os = "macos"));
3038 if extends_selection {
3039 if let Some(target) = keys.get(next) {
3040 let range = self.selection_range.read(cx).clone();
3041 let next_selection = extend_selection_range(
3042 &self.selected_now,
3043 keys,
3044 self.selectable_keys(),
3045 &range,
3046 target,
3047 );
3048 self.selection_range.update(cx, |range, _| {
3049 if range.anchor.is_none() {
3050 range.anchor = Some(target.clone());
3051 }
3052 range.current = Some(target.clone());
3053 range.is_all = false;
3054 });
3055 if !same_selection(&next_selection, &self.selected_now) {
3056 self.commit(&next_selection, window, cx);
3057 }
3058 }
3059 } else if self.select_on_focus
3060 && !modifiers.shift
3061 && !((cfg!(target_os = "macos") && modifiers.alt)
3062 || (!cfg!(target_os = "macos") && modifiers.control))
3063 {
3064 if let Some(target) = keys.get(next) {
3065 let next_selection = vec![target.clone()];
3066 if !same_selection(&next_selection, &self.selected_now) {
3067 self.commit(&next_selection, window, cx);
3068 }
3069 self.selection_range.update(cx, |range, _| {
3070 *range = TableSelectionRange {
3071 anchor: Some(target.clone()),
3072 current: Some(target.clone()),
3073 ..TableSelectionRange::default()
3074 };
3075 });
3076 }
3077 }
3078 self.held.update(cx, |v, cx| {
3079 *v = keys.get(next).cloned();
3080 cx.notify();
3081 });
3082 if self.fixed_virtual {
3083 self.fixed_scroll
3084 .scroll_to_item(next, crate::VirtualListScroll::Center);
3085 } else if let Some(state) = &self.variable_scroll {
3086 if is_variable_page {
3087 state.scroll_to_reveal_item(next);
3088 } else {
3089 state.scroll_to(gpui::ListOffset {
3094 item_ix: next,
3095 offset_in_item: px(0.),
3096 });
3097 }
3098 }
3099 }
3100
3101 fn activate(
3103 &self,
3104 index: usize,
3105 event: &gpui::KeyDownEvent,
3106 modifiers: gpui::Modifiers,
3107 window: &mut Window,
3108 cx: &mut App,
3109 ) {
3110 let activation = if event.keystroke.key == "enter" {
3111 RowActivation::Enter
3112 } else {
3113 RowActivation::Space
3114 };
3115 match row_intent(
3116 activation,
3117 self.mode,
3118 self.selection_behavior,
3119 self.selected_now.is_empty(),
3120 self.on_row_click.is_some(),
3121 ) {
3122 RowIntent::Action => {
3123 if let Some(cb) = &self.on_row_click {
3124 cb(&index, &ClickEvent::default(), window, cx);
3125 }
3126 }
3127 RowIntent::Selection => {
3128 if let Some(key) = self.keys.get(index) {
3129 self.select_row(key, modifiers, window, cx);
3130 }
3131 }
3132 RowIntent::None => {}
3133 }
3134 }
3135
3136 fn select_row(
3138 &self,
3139 key: &SharedString,
3140 modifiers: gpui::Modifiers,
3141 window: &mut Window,
3142 cx: &mut App,
3143 ) {
3144 let mode = self.mode;
3145 let selected_now = &self.selected_now;
3146 let was_selected = selected_now.contains(key);
3147 let extends_selection = modifiers.shift && mode == SelectionMode::Multiple;
3148 let next = if extends_selection {
3149 let range = self.selection_range.read(cx).clone();
3150 extend_selection_range(
3151 selected_now,
3152 &self.keys,
3153 self.selectable_keys(),
3154 &range,
3155 key,
3156 )
3157 } else {
3158 let non_contiguous = if cfg!(target_os = "macos") {
3159 modifiers.alt
3160 } else {
3161 modifiers.control
3162 };
3163 crate::selection::next_selection_with_behavior(
3164 selected_now,
3165 key,
3166 mode,
3167 if non_contiguous {
3168 SelectionBehavior::Toggle
3169 } else {
3170 self.selection_behavior
3171 },
3172 self.disallow_empty_selection,
3173 )
3174 };
3175 let changed = !same_selection(&next, selected_now);
3176 if changed {
3177 self.commit(&next, window, cx);
3178 }
3179 if extends_selection && changed {
3180 self.selection_range.update(cx, |range, _| {
3181 if range.anchor.is_none() {
3182 range.anchor = Some(key.clone());
3183 }
3184 range.current = Some(key.clone());
3185 range.is_all = false;
3186 });
3187 } else if !extends_selection && mode == SelectionMode::Multiple && !was_selected {
3188 self.selection_range.update(cx, |range, _| {
3189 range.anchor = Some(key.clone());
3190 range.current = Some(key.clone());
3191 range.is_all = false;
3192 });
3193 } else if !extends_selection && mode == SelectionMode::Multiple {
3194 self.selection_range.update(cx, |range, _| {
3195 if range.is_all {
3196 *range = TableSelectionRange::default();
3197 }
3198 });
3199 }
3200 }
3201}
3202
3203fn resize_width_probe(
3207 measured: gpui::Entity<Vec<Option<Pixels>>>,
3208 column_index: usize,
3209) -> impl IntoElement {
3210 gpui::canvas(
3211 move |bounds: gpui::Bounds<Pixels>, _, cx| {
3212 let width = px(f32::from(bounds.size.width).floor());
3213 measured.update(cx, |values, cx| {
3214 if values.len() <= column_index {
3215 values.resize(column_index + 1, None);
3216 }
3217 if values[column_index] != Some(width) {
3218 values[column_index] = Some(width);
3219 cx.notify();
3220 }
3221 });
3222 bounds
3223 },
3224 |_, _, _, _| {},
3225 )
3226 .absolute()
3227 .inset_0()
3228}
3229
3230struct ResizePointerStart {
3233 column_index: usize,
3234 start_width: Pixels,
3237 held: gpui::Entity<Option<(usize, f32, f32)>>,
3239 keyboard: gpui::Entity<Option<usize>>,
3241 widths: gpui::Entity<Vec<Option<Pixels>>>,
3242 columns: std::sync::Arc<Vec<TableColumn>>,
3243 measurements: std::sync::Arc<Vec<Option<Pixels>>>,
3244 focus: gpui::FocusHandle,
3245 on_resize_start: Option<OnResize>,
3246 on_resize_end: Option<OnResize>,
3247}
3248
3249impl ResizePointerStart {
3250 fn press(&self, ev: &gpui::MouseDownEvent, window: &mut Window, cx: &mut App) {
3253 let column_index = self.column_index;
3254 window.focus(&self.focus, cx);
3255 if let Some(active_column) = *self.keyboard.read(cx) {
3256 let current = final_column_widths(
3257 &self.columns,
3258 self.widths.read(cx),
3259 &self.measurements,
3260 active_column,
3261 );
3262 self.keyboard.update(cx, |active, _| *active = None);
3263 clear_controlled_resize_proposal(&self.columns, &self.widths, active_column, cx);
3264 if let Some(callback) = &self.on_resize_end {
3265 callback(¤t, window, cx);
3266 }
3267 }
3268 clear_controlled_resize_proposal(&self.columns, &self.widths, column_index, cx);
3269 if let Some(callback) = &self.on_resize_start {
3270 let current = resolved_column_widths(
3271 &self.columns,
3272 self.widths.read(cx),
3273 &self.measurements,
3274 None,
3275 );
3276 callback(¤t, window, cx);
3277 }
3278 let x = f32::from(ev.position.x);
3279 let start_width = self.start_width;
3280 self.held.update(cx, |v, _| {
3281 *v = Some((column_index, x, f32::from(start_width)));
3282 });
3283 }
3284}
3285
3286impl Table {
3288 fn virtual_projection(
3293 &self,
3294 base_id: &gpui::ElementId,
3295 window: &mut Window,
3296 cx: &mut App,
3297 ) -> Option<std::sync::Arc<VirtualProjection>> {
3298 let needs_selectable_keys = self.selection_mode == SelectionMode::Multiple;
3299 self.virtual_rows
3300 .as_ref()
3301 .map(|(count, identity, key_for_row, _)| {
3302 let cache = window.use_keyed_state(
3303 element_id::scoped(base_id, "virtual-projection"),
3304 cx,
3305 |_, _| None::<std::sync::Arc<VirtualProjection>>,
3306 );
3307 let cached = cache.read(cx).clone();
3308 let tree = self.virtual_tree_metadata.is_some();
3309 match cached {
3310 Some(cached)
3311 if cached.count == *count
3312 && cached.identity == *identity
3313 && cached.tree == tree
3314 && (!tree
3315 || (cached.expanded.len() == self.expanded_keys.len()
3316 && self
3317 .expanded_keys
3318 .iter()
3319 .all(|key| cached.expanded.contains(key)))) =>
3320 {
3321 if !needs_selectable_keys
3322 || cached
3323 .selectable_disabled_keys
3324 .as_ref()
3325 .is_some_and(|disabled| {
3326 disabled.as_slice() == self.disabled_keys.as_slice()
3327 })
3328 {
3329 cached
3330 } else {
3331 let projection = std::sync::Arc::new(VirtualProjection {
3332 count: cached.count,
3333 identity: cached.identity.clone(),
3334 expanded: std::sync::Arc::clone(&cached.expanded),
3335 tree: cached.tree,
3336 full_keys: std::sync::Arc::clone(&cached.full_keys),
3337 visible: std::sync::Arc::clone(&cached.visible),
3338 selectable_collection_keys: Some(filtered_selectable_keys(
3339 cached.full_keys.as_slice(),
3340 &self.disabled_keys,
3341 )),
3342 selectable_disabled_keys: Some(std::sync::Arc::new(
3343 self.disabled_keys.clone(),
3344 )),
3345 });
3346 cache.update(cx, |slot, _| {
3347 *slot = Some(std::sync::Arc::clone(&projection));
3348 });
3349 projection
3350 }
3351 }
3352 _ => {
3353 let mut full_keys = Vec::with_capacity(*count);
3354 let mut visible = Vec::with_capacity(*count);
3355 if let Some(project) = &self.virtual_tree_metadata {
3356 let mut visible_by_key =
3357 std::collections::HashMap::with_capacity(*count);
3358 for source_index in 0..*count {
3359 let key = key_for_row(source_index);
3360 let metadata = project(source_index);
3361 let is_visible =
3362 metadata.parent_key.as_ref().is_none_or(|parent| {
3363 visible_by_key.get(parent).copied().unwrap_or(false)
3364 && self.expanded_keys.contains(parent)
3365 });
3366 visible_by_key.insert(key.clone(), is_visible);
3367 full_keys.push(key.clone());
3368 if is_visible {
3369 visible.push((source_index, key, metadata));
3370 }
3371 }
3372 } else {
3373 for source_index in 0..*count {
3374 let key = key_for_row(source_index);
3375 full_keys.push(key.clone());
3376 visible.push((source_index, key, VirtualTreeMetadata::default()));
3377 }
3378 }
3379 let full_keys = std::sync::Arc::new(full_keys);
3380 let selectable_collection_keys = needs_selectable_keys.then(|| {
3381 filtered_selectable_keys(full_keys.as_slice(), &self.disabled_keys)
3382 });
3383 let selectable_disabled_keys = needs_selectable_keys
3384 .then(|| std::sync::Arc::new(self.disabled_keys.clone()));
3385 let projection = std::sync::Arc::new(VirtualProjection {
3386 count: *count,
3387 identity: identity.clone(),
3388 expanded: if tree {
3389 std::sync::Arc::new(self.expanded_keys.iter().cloned().collect())
3390 } else {
3391 std::sync::Arc::default()
3392 },
3393 tree,
3394 full_keys,
3395 visible: std::sync::Arc::new(visible),
3396 selectable_collection_keys,
3397 selectable_disabled_keys,
3398 });
3399 cache.update(cx, |slot, _| {
3400 *slot = Some(std::sync::Arc::clone(&projection));
3401 });
3402 projection
3403 }
3404 }
3405 })
3406 }
3407
3408 fn virtual_row_heights(
3411 &self,
3412 base_id: &gpui::ElementId,
3413 virtual_visible_keys: Option<&Vec<SharedString>>,
3414 window: &mut Window,
3415 cx: &mut App,
3416 ) -> Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>> {
3417 match (
3418 &self.virtual_rows,
3419 self.estimated_row_height,
3420 virtual_visible_keys,
3421 ) {
3422 (Some((_, identity, _, _)), Some(_), Some(keys)) if self.row_height.is_none() => {
3423 let count = keys.len();
3424 let state = window.use_keyed_state(
3425 element_id::scoped(
3426 &element_id::scoped(base_id, "row-heights"),
3427 identity.clone(),
3428 ),
3429 cx,
3430 |_, _| (Vec::<SharedString>::new(), Vec::<Option<Pixels>>::new()),
3431 );
3432 if state.read(cx).0.as_slice() != keys.as_slice() {
3433 let stored_keys = keys.clone();
3434 state.update(cx, |stored, _| {
3435 *stored = (stored_keys, vec![None; count]);
3436 });
3437 }
3438 Some(state)
3439 }
3440 _ => None,
3441 }
3442 }
3443
3444 fn virtual_list_state(
3447 &self,
3448 base_id: &gpui::ElementId,
3449 virtual_visible_count: usize,
3450 window: &mut Window,
3451 cx: &mut App,
3452 ) -> Option<gpui::ListState> {
3453 match (
3454 self.row_height,
3455 self.estimated_row_height,
3456 &self.virtual_rows,
3457 ) {
3458 (None, Some(estimate), Some((_, identity, _, _))) => {
3459 let count = virtual_visible_count;
3460 let overdraw = self.max_h.unwrap_or(px(400.)).max(estimate * 3.);
3461 let state = window
3462 .use_keyed_state(
3463 element_id::scoped(
3464 &element_id::scoped(base_id, "list-state"),
3465 identity.clone(),
3466 ),
3467 cx,
3468 move |_, _| crate::VirtualListHandle::with_overdraw(count, overdraw),
3469 )
3470 .read(cx)
3471 .list_state()
3472 .clone();
3473 if state.item_count() != count {
3474 state.reset(count);
3475 }
3476 Some(state)
3477 }
3478 _ => None,
3479 }
3480 }
3481
3482 fn header_focus_handles(
3486 &self,
3487 base_id: &gpui::ElementId,
3488 sortable: bool,
3489 window: &mut Window,
3490 cx: &mut App,
3491 ) -> Vec<gpui::FocusHandle> {
3492 self.columns
3493 .iter()
3494 .enumerate()
3495 .map(|(i, column)| {
3496 let id = element_id::scoped(&element_id::indexed(base_id, "sort", i), "focus");
3497 if column.allows_sorting && sortable {
3498 crate::util::tab_stop_handle(id, window, cx)
3499 } else {
3500 window
3501 .use_keyed_state(id, cx, |_, cx| cx.focus_handle())
3502 .read(cx)
3503 .clone()
3504 }
3505 })
3506 .collect()
3507 }
3508
3509 fn resize_focus_handles(
3512 &self,
3513 base_id: &gpui::ElementId,
3514 window: &mut Window,
3515 cx: &mut App,
3516 ) -> Vec<Option<gpui::FocusHandle>> {
3517 self.columns
3518 .iter()
3519 .enumerate()
3520 .map(|(i, column)| {
3521 column.allows_resizing.then(|| {
3522 crate::util::tab_stop_handle(
3523 element_id::scoped(&element_id::indexed(base_id, "resize", i), "focus"),
3524 window,
3525 cx,
3526 )
3527 })
3528 })
3529 .collect()
3530 }
3531
3532 #[allow(clippy::too_many_arguments)]
3536 fn select_all_header_cell(
3537 &self,
3538 base_id: &gpui::ElementId,
3539 colors: &herogpui_theme::ThemeColors,
3540 secondary: bool,
3541 selectable_collection_keys: &Option<std::sync::Arc<Vec<SharedString>>>,
3542 selection_own: &Option<gpui::Entity<Vec<SharedString>>>,
3543 selection_range: &gpui::Entity<TableSelectionRange>,
3544 cx: &mut App,
3545 ) -> gpui::Stateful<gpui::Div> {
3546 let mut cell = gpui::div()
3549 .id(element_id::scoped(base_id, "select-all-cell"))
3550 .a11y(a11y::Role::ColumnHeader)
3555 .a11y_column_index(0)
3556 .flex()
3557 .items_center()
3558 .justify_center()
3559 .relative()
3560 .w(px(44.))
3561 .py(px(10.));
3562 if secondary {
3563 let radius = cx.layout().radius_2xl().min(px(32.));
3564 cell = cell
3565 .bg(colors.surface_secondary)
3566 .rounded_tl(radius)
3567 .rounded_bl(radius);
3568 }
3569 if self.selection_mode == SelectionMode::Multiple {
3570 let all = selectable_collection_keys
3573 .as_ref()
3574 .expect("multiple Tables have selectable keys")
3575 .clone();
3576 let (all_selected, indeterminate) =
3577 select_all_flags(all.as_slice(), &self.selected_keys);
3578 let mut box_el = Checkbox::new(element_id::scoped(base_id, "select-all"))
3579 .is_selected(all_selected)
3580 .is_indeterminate(indeterminate);
3581 let cb = self.on_selection_change.clone();
3582 if cb.is_some() || selection_own.is_some() {
3583 let selection_own = selection_own.clone();
3584 let selection_range = selection_range.clone();
3585 let disallow_empty_selection = self.disallow_empty_selection;
3586 box_el = box_el.on_change(move |_next, window, cx| {
3587 if all_selected && disallow_empty_selection {
3588 return;
3589 }
3590 let next: Vec<SharedString> = if all_selected {
3592 Vec::new()
3593 } else {
3594 all.as_slice().to_vec()
3595 };
3596 if let Some(held) = &selection_own {
3597 held.update(cx, |value, cx| {
3598 *value = next.clone();
3599 cx.notify();
3600 });
3601 }
3602 if let Some(cb) = &cb {
3603 cb(&next, window, cx);
3604 }
3605 selection_range.update(cx, |range, _| {
3606 *range = if all_selected {
3607 TableSelectionRange::default()
3608 } else {
3609 TableSelectionRange {
3610 is_all: true,
3611 ..TableSelectionRange::default()
3612 }
3613 };
3614 });
3615 });
3616 }
3617 cell = cell.child(box_el);
3618 }
3619 cell
3620 }
3621
3622 fn column_header_cell(
3626 &self,
3627 hx: &HeaderParts<'_>,
3628 column_index: usize,
3629 column: &TableColumn,
3630 window: &mut Window,
3631 cx: &mut App,
3632 ) -> AnyElement {
3633 let HeaderParts {
3634 base_id,
3635 colors,
3636 secondary,
3637 selectable,
3638 effective_widths,
3639 layout_width_bounds,
3640 measured_widths,
3641 header_focus,
3642 header_focused,
3643 ..
3644 } = *hx;
3645 let sorted = self
3646 .sort_descriptor
3647 .as_ref()
3648 .filter(|d| d.column == column.label);
3649 let effective = effective_widths[column_index];
3651 let (layout_min, layout_max) = layout_width_bounds[column_index];
3652 let first_header = column_index == 0 && !selectable;
3653 let last_header = column_index + 1 == self.columns.len();
3654 let mut cell = gpui::div()
3655 .when(effective.is_none(), flex_cell)
3656 .when_some(effective, |c, w| c.w(w))
3657 .when_some(layout_min, |c, w| c.min_w(w))
3658 .when_some(layout_max, |c, w| c.max_w(w))
3659 .flex()
3660 .items_center()
3661 .gap(px(4.))
3662 .px(px(16.))
3664 .py(px(10.))
3665 .text_size(px(12.))
3666 .line_height(px(16.))
3667 .font_weight(gpui::FontWeight::MEDIUM)
3668 .when(column.allows_sorting, |c| c.w_full().justify_between())
3669 .text_color(if sorted.is_some() {
3670 colors.foreground
3671 } else {
3672 colors.muted
3673 })
3674 .relative();
3675 if !column.allows_resizing && !cfg!(target_arch = "wasm32") {
3676 let measurements = measured_widths.clone();
3677 let content_children = 1 + usize::from(sorted.is_some() && self.show_indicator);
3680 cell = cell.on_children_prepainted(move |bounds, _, cx| {
3681 let content =
3682 intrinsic_children_width(&bounds[..content_children.min(bounds.len())]);
3683 if content <= 0. {
3684 return;
3685 }
3686 let next = px(content + 32.);
3687 measurements.update(cx, |values, cx| {
3688 if values.len() <= column_index {
3689 values.resize(column_index + 1, None);
3690 }
3691 if values[column_index].is_none_or(|current| next > current) {
3692 values[column_index] = Some(next);
3693 cx.notify();
3694 }
3695 });
3696 });
3697 }
3698 cell = cell.child(column.label.clone());
3699 let header_selector = format!("table-header-track-{column_index}");
3700 cell = cell.debug_selector(move || header_selector);
3701
3702 if secondary {
3703 let radius = cx.layout().radius_2xl().min(px(32.));
3704 cell = cell.bg(colors.surface_secondary);
3705 if first_header {
3706 cell = cell.rounded_tl(radius).rounded_bl(radius);
3707 }
3708 if last_header {
3709 cell = cell.rounded_tr(radius).rounded_br(radius);
3710 }
3711 }
3712
3713 if let Some(descriptor) = sorted {
3714 if self.show_indicator {
3715 cell = cell.child(match &self.indicator {
3719 Some(render) => render(descriptor.direction),
3720 None => {
3721 let indicator_id = element_id::indexed(
3722 &element_id::scoped(base_id, "sort-indicator"),
3723 "column",
3724 column_index,
3725 );
3726 crate::anim::rotating_indicator_with_duration(
3727 &indicator_id,
3728 descriptor.direction == SortDirection::Descending,
3729 gpui::svg()
3730 .size(px(12.))
3731 .path(icons::CHEVRON_UP)
3732 .text_color(colors.foreground),
3734 100,
3735 window,
3736 cx,
3737 )
3738 }
3739 });
3740 }
3741 }
3742
3743 if !last_header && !column.allows_resizing {
3744 cell = cell.child(
3745 gpui::div()
3746 .absolute()
3747 .right_0()
3748 .top(px(10.))
3749 .h(px(16.))
3750 .w(px(1.))
3751 .rounded(crate::util::hairline_radius(cx))
3752 .bg(colors.separator),
3753 );
3754 }
3755
3756 let cell = match (column.allows_sorting, self.on_sort_change.clone()) {
3759 (true, Some(cb)) => {
3760 let next =
3761 SortDescriptor::next(self.sort_descriptor.as_ref(), column.label.clone());
3762 let sort_group: SharedString =
3768 format!("{}-sort-hover-{}", self.id, column.label).into();
3769 let header_cell = gpui::div()
3770 .id(element_id::scoped(
3771 &element_id::scoped(base_id, "sort"),
3772 column.label.clone(),
3773 ))
3774 .a11y_named(
3782 a11y::Role::ColumnHeader,
3783 &a11y::Name::labelled(column.label.clone()),
3784 )
3785 .a11y_column_index(column_index + usize::from(selectable))
3786 .group(sort_group.clone())
3787 .when(effective.is_none(), |wrapper| {
3794 wrapper.flex_basis(px(0.)).flex_grow(1.).flex_shrink(1.)
3795 })
3796 .when_some(effective, |wrapper, w| wrapper.w(w))
3797 .when_some(layout_min, |wrapper, w| wrapper.min_w(w))
3798 .when_some(layout_max, |wrapper, w| wrapper.max_w(w))
3799 .flex()
3800 .cursor(crate::util::interactive_cursor(cx))
3801 .track_focus(&header_focus[column_index])
3804 .on_click(move |_, window, cx| cb(&next.clone(), window, cx))
3805 .child(cell.group_hover(sort_group, |s| s.text_color(colors.foreground)));
3806 let header_cell = header_cell
3810 .relative()
3811 .when(header_focused[column_index], |c| {
3812 c.child(crate::util::inset_focus_ring(cx))
3813 });
3814 crate::util::record_focus_bounds(
3815 header_cell,
3816 &header_focus[column_index],
3817 window,
3818 cx,
3819 )
3820 .into_any_element()
3821 }
3822 _ => {
3825 let header_cell = cell
3826 .id(element_id::indexed(base_id, "header", column_index))
3827 .a11y_named(
3831 a11y::Role::ColumnHeader,
3832 &a11y::Name::labelled(column.label.clone()),
3833 )
3834 .a11y_column_index(column_index + usize::from(selectable))
3835 .track_focus(&header_focus[column_index])
3836 .relative()
3837 .when(header_focused[column_index], |c| {
3838 c.child(crate::util::inset_focus_ring(cx))
3839 });
3840 crate::util::record_focus_bounds(
3841 header_cell,
3842 &header_focus[column_index],
3843 window,
3844 cx,
3845 )
3846 .into_any_element()
3847 }
3848 };
3849
3850 if column.allows_resizing {
3855 self.resizable_column_header(hx, column_index, column, cell, cx)
3856 } else {
3857 cell
3858 }
3859 }
3860
3861 fn resizable_column_header(
3865 &self,
3866 hx: &HeaderParts<'_>,
3867 column_index: usize,
3868 column: &TableColumn,
3869 cell: AnyElement,
3870 cx: &mut App,
3871 ) -> AnyElement {
3872 let HeaderParts {
3873 base_id,
3874 colors,
3875 effective_widths,
3876 layout_width_bounds,
3877 measured_widths,
3878 measured_widths_now,
3879 resize_focus,
3880 resize_focused,
3881 resized,
3882 dragging,
3883 drag_now,
3884 keyboard_resizing,
3885 keyboard_resize_now,
3886 resize_limits,
3887 resize_columns,
3888 resize_measurements,
3889 ..
3890 } = *hx;
3891 let effective = effective_widths[column_index];
3892 let (layout_min, layout_max) = layout_width_bounds[column_index];
3893 let start_width = effective
3894 .or_else(|| measured_widths_now.get(column_index).copied().flatten())
3895 .unwrap_or(px(160.));
3896 let keyboard = keyboard_resizing.clone();
3897 let keyboard_out = keyboard.clone();
3898 let widths = resized.clone();
3899 let widths_for_outside = widths.clone();
3900 let (min_width, max_width) = resize_limits[column_index];
3901 let controlled = column.width.is_some();
3902 let resizer_group: SharedString = format!("{}-resizer-{column_index}", self.id).into();
3904 let accent_color = colors.accent.color;
3905 let focus_color = colors.focus;
3906 let is_resizing = drag_now.is_some_and(|(index, _, _)| index == column_index)
3907 || keyboard_resize_now == Some(column_index);
3908 let measured = measured_widths.clone();
3909 let columns_for_outside = resize_columns.clone();
3910 let columns_for_keys = resize_columns.clone();
3911 let measurements_for_outside = resize_measurements.clone();
3912 let measurements_for_keys = resize_measurements.clone();
3913 let resize_start_for_keys = self.on_resize_start.clone();
3914 let resize_for_keys = self.on_resize.clone();
3915 let resize_end_for_outside = self.on_resize_end.clone();
3916 let resize_end_for_keys = self.on_resize_end.clone();
3917 let pointer_start = ResizePointerStart {
3918 column_index,
3919 start_width,
3920 held: dragging.clone(),
3921 keyboard: keyboard.clone(),
3922 widths: widths.clone(),
3923 columns: resize_columns.clone(),
3924 measurements: resize_measurements.clone(),
3925 focus: resize_focus[column_index]
3926 .as_ref()
3927 .expect("resizable columns have a focus handle")
3928 .clone(),
3929 on_resize_start: self.on_resize_start.clone(),
3930 on_resize_end: self.on_resize_end.clone(),
3931 };
3932 gpui::div()
3933 .relative()
3934 .when(effective.is_none(), flex_cell)
3935 .when_some(effective, |c, w| c.w(w))
3936 .when_some(layout_min, |wrapper, w| wrapper.min_w(w))
3940 .when_some(layout_max, |wrapper, w| wrapper.max_w(w))
3941 .when(effective.is_none(), |wrapper| {
3942 wrapper.child(resize_width_probe(measured, column_index))
3943 })
3944 .child(cell)
3945 .child(
3946 gpui::div()
3947 .id(element_id::indexed(base_id, "resize", column_index))
3948 .track_focus(
3949 resize_focus[column_index]
3950 .as_ref()
3951 .expect("resizable columns have a focus handle"),
3952 )
3953 .group(resizer_group.clone())
3954 .absolute()
3955 .top(px(0.))
3956 .right(px(-8.))
3959 .w(px(17.))
3960 .h_full()
3961 .flex()
3962 .items_center()
3963 .justify_center()
3964 .child(
3965 gpui::div()
3968 .w(px(1.))
3969 .h(px(16.))
3970 .rounded(crate::util::hairline_radius(cx))
3971 .bg(colors.separator)
3972 .group_hover(resizer_group, |s| {
3973 s.w(px(2.)).h_full().bg(accent_color)
3974 })
3975 .when(is_resizing, |s| {
3976 s.w(px(2.)).h_full().bg(accent_color)
3977 })
3978 .when(resize_focused[column_index], |s| {
3979 s.w(px(2.)).h_full().bg(focus_color)
3980 }),
3981 )
3982 .cursor(gpui::CursorStyle::ResizeLeftRight)
3983 .on_mouse_down(gpui::MouseButton::Left, move |ev, window, cx| {
3984 pointer_start.press(ev, window, cx);
3985 })
3986 .on_mouse_down_out(move |_, window, cx| {
3987 if *keyboard_out.read(cx) == Some(column_index) {
3988 let current = final_column_widths(
3989 &columns_for_outside,
3990 widths_for_outside.read(cx),
3991 &measurements_for_outside,
3992 column_index,
3993 );
3994 keyboard_out.update(cx, |active, cx| {
3995 *active = None;
3996 cx.notify();
3997 });
3998 clear_controlled_resize_proposal(
3999 &columns_for_outside,
4000 &widths_for_outside,
4001 column_index,
4002 cx,
4003 );
4004 if let Some(callback) = &resize_end_for_outside {
4005 callback(¤t, window, cx);
4006 }
4007 }
4008 })
4009 .on_key_down(move |event, window, cx| {
4010 let key = event.keystroke.key.as_str();
4011 let editing = *keyboard.read(cx) == Some(column_index);
4012 match key {
4013 "enter" => {
4014 if editing {
4015 let current = final_column_widths(
4016 &columns_for_keys,
4017 widths.read(cx),
4018 &measurements_for_keys,
4019 column_index,
4020 );
4021 keyboard.update(cx, |active, cx| {
4022 *active = None;
4023 cx.notify();
4024 });
4025 clear_controlled_resize_proposal(
4026 &columns_for_keys,
4027 &widths,
4028 column_index,
4029 cx,
4030 );
4031 if let Some(callback) = &resize_end_for_keys {
4032 callback(¤t, window, cx);
4033 }
4034 } else {
4035 clear_controlled_resize_proposal(
4036 &columns_for_keys,
4037 &widths,
4038 column_index,
4039 cx,
4040 );
4041 let current = resolved_column_widths(
4042 &columns_for_keys,
4043 widths.read(cx),
4044 &measurements_for_keys,
4045 None,
4046 );
4047 keyboard.update(cx, |active, cx| {
4048 *active = Some(column_index);
4049 cx.notify();
4050 });
4051 if let Some(callback) = &resize_start_for_keys {
4052 callback(¤t, window, cx);
4053 }
4054 }
4055 cx.stop_propagation();
4056 }
4057 "escape" | "space" | "tab" if editing => {
4058 let current = final_column_widths(
4059 &columns_for_keys,
4060 widths.read(cx),
4061 &measurements_for_keys,
4062 column_index,
4063 );
4064 keyboard.update(cx, |active, cx| {
4065 *active = None;
4066 cx.notify();
4067 });
4068 clear_controlled_resize_proposal(
4069 &columns_for_keys,
4070 &widths,
4071 column_index,
4072 cx,
4073 );
4074 if let Some(callback) = &resize_end_for_keys {
4075 callback(¤t, window, cx);
4076 }
4077 cx.stop_propagation();
4078 }
4079 "right" | "up" | "left" | "down" if editing => {
4080 let delta = if matches!(key, "right" | "up") {
4081 10.
4082 } else {
4083 -10.
4084 };
4085 let mut proposed = start_width;
4086 widths.update(cx, |values, cx| {
4087 if values.len() <= column_index {
4088 values.resize(column_index + 1, None);
4089 }
4090 let current = if controlled {
4091 start_width
4092 } else {
4093 values[column_index].unwrap_or(start_width)
4094 };
4095 let next = (f32::from(current) + delta)
4096 .floor()
4097 .min(max_width)
4098 .max(min_width);
4099 proposed = px(next);
4100 values[column_index] = Some(proposed);
4101 cx.notify();
4102 });
4103 if let Some(callback) = &resize_for_keys {
4104 let current = resolved_column_widths(
4105 &columns_for_keys,
4106 widths.read(cx),
4107 &measurements_for_keys,
4108 Some((column_index, proposed)),
4109 );
4110 callback(¤t, window, cx);
4111 }
4112 cx.stop_propagation();
4113 }
4114 _ => {}
4115 }
4116 }),
4117 )
4118 .into_any_element()
4119 }
4120
4121 fn column_resize_drag(
4124 &self,
4125 mut table: gpui::Stateful<gpui::Div>,
4126 dragging: gpui::Entity<Option<(usize, f32, f32)>>,
4127 resized: gpui::Entity<Vec<Option<Pixels>>>,
4128 resize_columns: std::sync::Arc<Vec<TableColumn>>,
4129 resize_measurements: std::sync::Arc<Vec<Option<Pixels>>>,
4130 ) -> gpui::Stateful<gpui::Div> {
4131 let held = dragging.clone();
4132 let held_up = dragging;
4133 let widths = resized;
4134 let widths_up = widths.clone();
4135 let columns_for_move = resize_columns.clone();
4136 let columns_for_up = resize_columns;
4137 let measurements_for_move = resize_measurements.clone();
4138 let measurements_for_up = resize_measurements;
4139 let resize_callback = self.on_resize.clone();
4140 let resize_end_callback = self.on_resize_end.clone();
4141 table = table.relative().child(
4142 gpui::canvas(
4143 |bounds, _, _| bounds,
4144 move |_, _, window, _| {
4145 let held = held.clone();
4146 let widths = widths.clone();
4147 let columns_for_move = columns_for_move.clone();
4148 let measurements_for_move = measurements_for_move.clone();
4149 let resize_callback = resize_callback.clone();
4150 window.on_mouse_event(
4151 move |event: &gpui::MouseMoveEvent, phase, window, cx| {
4152 if phase != gpui::DispatchPhase::Capture
4153 || event.pressed_button != Some(gpui::MouseButton::Left)
4154 {
4155 return;
4156 }
4157 let Some((column, from_x, from_w)) = *held.read(cx) else {
4158 return;
4159 };
4160 let raw = (from_w + f32::from(event.position.x) - from_x).floor();
4161 let min = columns_for_move[column]
4162 .min_width
4163 .map_or(DEFAULT_COLUMN_MIN_WIDTH, f32::from);
4164 let max = columns_for_move[column]
4165 .max_width
4166 .map_or(f32::MAX, f32::from);
4167 let proposed = px(raw.min(max).max(min));
4168 widths.update(cx, |values, cx| {
4169 if values.len() <= column {
4170 values.resize(column + 1, None);
4171 }
4172 values[column] = Some(proposed);
4173 cx.notify();
4174 });
4175 if let Some(callback) = &resize_callback {
4176 let current = resolved_column_widths(
4177 &columns_for_move,
4178 widths.read(cx),
4179 &measurements_for_move,
4180 Some((column, proposed)),
4181 );
4182 callback(¤t, window, cx);
4183 }
4184 },
4185 );
4186
4187 let held_up = held_up.clone();
4188 let widths_up = widths_up.clone();
4189 let columns_for_up = columns_for_up.clone();
4190 let measurements_for_up = measurements_for_up.clone();
4191 let resize_end_callback = resize_end_callback.clone();
4192 window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
4193 if phase != gpui::DispatchPhase::Capture
4194 || event.button != gpui::MouseButton::Left
4195 {
4196 return;
4197 }
4198 let drag = *held_up.read(cx);
4199 if let Some((column, _, _)) = drag {
4200 let current = final_column_widths(
4201 &columns_for_up,
4202 widths_up.read(cx),
4203 &measurements_for_up,
4204 column,
4205 );
4206 held_up.update(cx, |value, cx| {
4207 *value = None;
4208 cx.notify();
4209 });
4210 clear_controlled_resize_proposal(
4211 &columns_for_up,
4212 &widths_up,
4213 column,
4214 cx,
4215 );
4216 if let Some(callback) = &resize_end_callback {
4217 callback(¤t, window, cx);
4218 }
4219 }
4220 });
4221 },
4222 )
4223 .absolute()
4224 .inset_0(),
4225 );
4226 table
4227 }
4228
4229 fn row_keyboard(&self, mut wrapper: gpui::Div, keys: RowKeyboard<'_>) -> gpui::Div {
4235 let RowKeyboard {
4236 ctx,
4237 expanded_keys,
4238 header_focus,
4239 row_cursor,
4240 selectable_collection_keys,
4241 selection_own,
4242 selection_range,
4243 table_focus,
4244 tree_rows,
4245 typeahead_labels,
4246 typeahead,
4247 virtual_list_state,
4248 virtual_row_heights,
4249 virtual_scroll_now,
4250 virtual_text_value,
4251 virtual_typeahead_indices,
4252 } = keys;
4253 let stops: Vec<usize> = ctx
4254 .row_keys
4255 .iter()
4256 .enumerate()
4257 .filter_map(|(index, key)| (!self.disabled_keys.contains(key)).then_some(index))
4258 .collect();
4259 let keys = ctx.row_keys.clone();
4260 if keys.is_empty() {
4261 return wrapper;
4262 }
4263 let mode = self.selection_mode;
4264 let selection_behavior = self.selection_behavior;
4265 let fixed_virtual = self.row_height.is_some() && self.virtual_rows.is_some();
4266 let fixed_scroll = virtual_scroll_now.clone();
4267 let variable_scroll = virtual_list_state.clone();
4268 let typeahead_navigation = std::rc::Rc::new(TableTypeaheadNavigation {
4269 state: typeahead,
4270 labels: typeahead_labels,
4271 virtual_indices: virtual_typeahead_indices,
4272 virtual_text: virtual_text_value,
4273 stops: stops.clone(),
4274 keys: keys.clone(),
4275 cursor: row_cursor.clone(),
4276 fixed_virtual,
4277 fixed_scroll: fixed_scroll.clone(),
4278 variable_scroll: variable_scroll.clone(),
4279 });
4280 let capture_typeahead = typeahead_navigation.clone();
4281 let capture_typeahead_up = typeahead_navigation.clone();
4282 let capture_focus = table_focus.clone();
4283 let capture_focus_up = table_focus.clone();
4284 wrapper = wrapper.capture_key_down(move |event, window, cx| {
4285 if !capture_focus.contains_focused(window, cx) {
4286 return;
4287 }
4288 let key_name = event.keystroke.key.as_str();
4289 let typed = event.keystroke.key_char.as_deref().unwrap_or(key_name);
4290 let modifiers = &event.keystroke.modifiers;
4291 let now = web_time::Instant::now();
4292 let is_space = key_name == "space" || typed == " ";
4293 if is_space
4294 && capture_typeahead.state.read(cx).is_active(now)
4295 && !modifiers.control
4296 && !modifiers.platform
4297 && !modifiers.alt
4298 {
4299 cx.stop_propagation();
4300 capture_typeahead.push(" ", now, false, cx);
4301 }
4302 });
4303 wrapper = wrapper.capture_key_up(move |event, window, cx| {
4304 if !capture_focus_up.contains_focused(window, cx) {
4305 return;
4306 }
4307 let key_name = event.keystroke.key.as_str();
4308 let typed = event.keystroke.key_char.as_deref().unwrap_or(key_name);
4309 let modifiers = &event.keystroke.modifiers;
4310 let is_space = key_name == "space" || typed == " ";
4311 if is_space
4312 && capture_typeahead_up
4313 .state
4314 .read(cx)
4315 .is_active(web_time::Instant::now())
4316 && !modifiers.control
4317 && !modifiers.platform
4318 && !modifiers.alt
4319 {
4320 cx.stop_propagation();
4321 }
4322 });
4323 let nav = RowKeyNav {
4324 stops,
4325 held: row_cursor,
4326 keys,
4327 table_focus,
4328 typeahead: typeahead_navigation,
4329 on_row_click: self.on_row_click.clone(),
4330 selection: self.on_selection_change.clone(),
4331 selection_own,
4332 selection_range,
4333 selected_now: self.selected_keys.clone(),
4334 selectable_collection_keys,
4335 mode,
4336 selection_behavior,
4337 disallow_empty_selection: self.disallow_empty_selection,
4338 select_on_focus: selection_behavior == SelectionBehavior::Replace
4341 && mode != SelectionMode::None,
4342 plain_rows: self.virtual_rows.is_none(),
4343 fixed_virtual,
4344 fixed_row_height: self.row_height.filter(|_| fixed_virtual),
4351 fixed_scroll,
4352 variable_scroll,
4353 variable_heights: virtual_row_heights.clone(),
4354 variable_estimate: self.estimated_row_height,
4355 expanded: expanded_keys,
4356 on_expanded: self.on_expanded_change.clone(),
4357 tree_rows,
4358 page_up_header: header_focus.first().cloned(),
4361 headers: header_focus,
4362 };
4363 wrapper.on_key_down(move |event, window, cx| nav.key_down(event, window, cx))
4364 }
4365
4366 fn body_rows(
4370 &self,
4371 mut body: gpui::Stateful<gpui::Div>,
4372 rows_in: BodyRows<'_>,
4373 window: &Window,
4374 cx: &mut App,
4375 ) -> gpui::Stateful<gpui::Div> {
4376 let BodyRows {
4377 base_id,
4378 table_id,
4379 ctx,
4380 flat,
4381 row_count,
4382 virtual_projection,
4383 virtual_list_state,
4384 virtual_row_heights,
4385 virtual_scroll_now,
4386 } = rows_in;
4387 if let (Some(row_height), Some((_, _, _, factory))) =
4391 (self.row_height, self.virtual_rows.clone())
4392 {
4393 let height = self.max_h.unwrap_or(px(400.));
4403 let rows = ctx.clone();
4404 let projection = virtual_projection;
4405 let rows_selector = format!("{table_id}-virtual-rows");
4407 let handle = virtual_scroll_now;
4408 if handle.item_count() != row_count {
4409 handle.splice(0..handle.item_count(), row_count);
4410 }
4411 body = body.child(
4412 crate::VirtualList::new(
4413 element_id::scoped(base_id, "virtual-rows"),
4414 handle,
4415 move |i, _window, cx| {
4416 let (source_index, key, metadata) = &projection[i];
4417 let row_data = factory(*source_index);
4418 rows.row(
4419 i,
4420 row_data,
4421 metadata.depth,
4422 metadata.has_children,
4423 key,
4424 Some(row_height),
4425 _window,
4426 cx,
4427 )
4428 },
4429 )
4430 .height(height)
4431 .debug_selector(rows_selector)
4432 .when(
4433 ctx.frozen.is_some(),
4434 crate::VirtualList::restrict_scroll_to_axis,
4435 ),
4436 );
4437 } else if let (Some(state), Some((_, _, _, factory))) =
4438 (virtual_list_state, self.virtual_rows.clone())
4439 {
4440 let height = self.max_h.unwrap_or(px(400.));
4441 let rows = ctx.clone();
4442 let projection = virtual_projection;
4443 let measured_heights =
4444 virtual_row_heights.expect("estimated row height creates a measurement store");
4445 body = body.child(
4446 crate::VirtualList::new(
4447 element_id::scoped(base_id, "variable-rows"),
4448 &crate::VirtualListHandle::from_list_state(state),
4449 move |i, _window, cx| {
4450 let (source_index, key, metadata) = &projection[i];
4451 let row_data = factory(*source_index);
4452 let row = rows.row(
4453 i,
4454 row_data,
4455 metadata.depth,
4456 metadata.has_children,
4457 key,
4458 None,
4459 _window,
4460 cx,
4461 );
4462 let measured = measured_heights.clone();
4463 gpui::div()
4464 .relative()
4465 .w_full()
4466 .child(row)
4467 .child(
4468 gpui::canvas(
4469 move |bounds: gpui::Bounds<Pixels>, _, cx| {
4470 measured.update(cx, |(_, heights), cx| {
4471 if heights.get(i).copied().flatten()
4472 != Some(bounds.size.height)
4473 {
4474 heights[i] = Some(bounds.size.height);
4475 cx.notify();
4476 }
4477 });
4478 bounds
4479 },
4480 |_, _, _, _| {},
4481 )
4482 .absolute()
4483 .inset_0(),
4484 )
4485 .into_any_element()
4486 },
4487 )
4488 .height(height),
4489 );
4490 } else {
4491 for (i, (row_data, depth, has_children, tree_key, _)) in flat.into_iter().enumerate() {
4492 body = body.child(ctx.row(
4493 i,
4494 row_data,
4495 depth,
4496 has_children,
4497 &tree_key,
4498 None,
4499 window,
4500 cx,
4501 ));
4502 }
4503 }
4504
4505 body
4506 }
4507
4508 fn load_more(
4512 &self,
4513 mut table: gpui::Stateful<gpui::Div>,
4514 load_more: LoadMore<'_>,
4515 ) -> gpui::Stateful<gpui::Div> {
4516 let LoadMore {
4517 base_id,
4518 state,
4519 collection: load_more_collection,
4520 virtual_scroll,
4521 variable_scroll,
4522 muted,
4523 loading_text,
4524 } = load_more;
4525 if let Some(cb) = self.on_load_more.clone() {
4526 let scroll_offset = self.load_more_offset;
4527 table = table.child(
4528 gpui::canvas(
4529 move |bounds, window, cx| {
4530 let mask = window.content_mask().bounds;
4535 let in_view = bounds.right() >= mask.left()
4536 && bounds.left() <= mask.right()
4537 && bounds.bottom() >= mask.top()
4538 && bounds.top() <= mask.bottom() + mask.size.height * scroll_offset;
4539 let virtual_end_is_near = if let Some(handle) = &virtual_scroll {
4540 let viewport_height = handle.viewport_bounds().size.height;
4544 handle.remaining_below().is_some_and(|remaining| {
4545 remaining <= viewport_height * scroll_offset
4546 })
4547 } else if let Some((state, count, estimate)) = &variable_scroll {
4548 let viewport = state.viewport_bounds();
4549 let viewport_height = viewport.size.height;
4550 let top = state.logical_scroll_top();
4551 let remaining = *estimate * count.saturating_sub(top.item_ix)
4555 - top.offset_in_item
4556 - viewport_height;
4557 let margin = viewport_height * scroll_offset;
4558 let measured_end_is_near = *count == 0
4559 || state.bounds_for_item(count.saturating_sub(1)).is_some_and(
4560 |bounds| bounds.bottom() <= viewport.bottom() + margin,
4561 );
4562 measured_end_is_near || (scroll_offset > 0. && remaining <= margin)
4563 } else {
4564 true
4565 };
4566 let visible = if virtual_scroll.is_some() || variable_scroll.is_some() {
4567 virtual_end_is_near
4568 } else {
4569 in_view
4570 };
4571 let previous = state.read(cx).clone();
4572 let collection_changed = previous
4573 .1
4574 .as_ref()
4575 .is_none_or(|identity| identity != &load_more_collection);
4576 let should_load = visible && (!previous.0 || collection_changed);
4577 if previous.0 != visible || should_load {
4578 state.update(cx, |value, cx| {
4579 value.0 = visible;
4580 if should_load {
4581 value.1 = Some(load_more_collection.clone());
4582 }
4583 cx.notify();
4584 });
4585 }
4586 if should_load {
4587 cb(window, cx);
4588 }
4589 },
4590 |_, _, _, _| {},
4591 )
4592 .size(px(1.)),
4593 );
4594 }
4595
4596 if self.is_pending {
4597 let sentinel = gpui::div()
4598 .id(element_id::scoped(base_id, "load-more"))
4599 .flex()
4600 .items_center()
4601 .justify_center()
4602 .gap(px(8.))
4603 .w_full()
4604 .py(px(8.))
4607 .when_some(self.loader_height, |el, h| el.h(h))
4608 .text_size(px(13.))
4609 .text_color(muted)
4610 .child(
4611 crate::spinner::Spinner::new(element_id::scoped(base_id, "load-spinner"))
4612 .size(herogpui_core::Size::Sm),
4613 )
4614 .child(loading_text);
4615 table = table.child(sentinel);
4616 }
4617 table
4618 }
4619
4620 fn row_edge_rounding(
4625 &self,
4626 secondary: bool,
4627 virtual_scroll_now: &crate::VirtualListHandle,
4628 virtual_list_state: Option<&gpui::ListState>,
4629 virtual_visible_count: usize,
4630 cx: &App,
4631 ) -> (Option<Pixels>, Option<Pixels>) {
4632 let (touches_top, touches_bottom) =
4633 if self.row_height.is_some() && self.virtual_rows.is_some() {
4634 (
4637 virtual_scroll_now.is_scrolled_to_top(),
4638 virtual_scroll_now.is_scrolled_to_end(),
4639 )
4640 } else if let Some(state) = virtual_list_state {
4641 let top = state.logical_scroll_top();
4642 let viewport = state.viewport_bounds();
4643 (
4644 top.item_ix == 0 && top.offset_in_item <= px(0.5),
4645 virtual_visible_count == 0
4646 || state
4647 .bounds_for_item(virtual_visible_count - 1)
4648 .is_some_and(|bounds| bounds.bottom() <= viewport.bottom() + px(0.5)),
4649 )
4650 } else {
4651 (true, true)
4654 };
4655 edge_corner_rounding(
4656 secondary,
4657 self.footer.is_some(),
4658 touches_top,
4659 touches_bottom,
4660 cx.layout().radius_2xl().min(px(32.)),
4661 self.radius
4662 .unwrap_or_else(|| crate::util::container_radius(cx)),
4663 )
4664 }
4665}
4666
4667fn edge_corner_rounding(
4680 secondary: bool,
4681 has_footer: bool,
4682 touches_top: bool,
4683 touches_bottom: bool,
4684 body_radius: Pixels,
4685 wrapper_radius: Pixels,
4686) -> (Option<Pixels>, Option<Pixels>) {
4687 if secondary {
4688 (
4689 None,
4690 (touches_bottom && !has_footer).then_some(wrapper_radius),
4691 )
4692 } else {
4693 (
4694 touches_top.then_some(body_radius),
4695 touches_bottom.then_some(body_radius),
4696 )
4697 }
4698}
4699
4700struct RowCtx {
4701 id: gpui::ElementId,
4704 measured_widths: gpui::Entity<Vec<Option<Pixels>>>,
4708 measure_intrinsic: Vec<bool>,
4709 widths: Vec<(Option<Pixels>, Option<Pixels>, Option<Pixels>)>,
4711 row_header_columns: Vec<bool>,
4712 tree_column: usize,
4713 tree_column_has_children: bool,
4714 selectable: bool,
4715 selection_mode: SelectionMode,
4716 selection_behavior: SelectionBehavior,
4717 selected_keys: Vec<SharedString>,
4718 row_keys: Vec<SharedString>,
4719 disabled_keys: Vec<SharedString>,
4720 disallow_empty_selection: bool,
4721 expanded: std::rc::Rc<Vec<SharedString>>,
4722 on_expanded_change: Option<OnExpandedChange>,
4723 on_selection_change: Option<OnSelectionChange>,
4724 selection_own: Option<gpui::Entity<Vec<SharedString>>>,
4725 selection_range: gpui::Entity<TableSelectionRange>,
4726 on_row_click: Option<OnRowClick>,
4727 focus: gpui::FocusHandle,
4728 cursor_own: gpui::Entity<Option<SharedString>>,
4729 cursor: Option<usize>,
4731 secondary: bool,
4734 row_hover_bg: Option<gpui::Hsla>,
4737 hover_group_prefix: SharedString,
4741 virtualized: bool,
4744 round_top: Option<Pixels>,
4748 round_bottom: Option<Pixels>,
4750 is_tree: bool,
4753 cell: Option<CellCtx>,
4755 frozen: Option<FrozenColumns>,
4757}
4758
4759struct RowPaint {
4761 i: usize,
4762 key: SharedString,
4763 depth: usize,
4764 has_children: bool,
4765 is_selected: bool,
4766 is_disabled: bool,
4767 is_expanded: bool,
4768 selected_bg: Option<gpui::Hsla>,
4769 hover_bg: Option<gpui::Hsla>,
4770 hover_group: SharedString,
4771 row_focused: bool,
4773 data_count: usize,
4774 round_top: Option<Pixels>,
4775 round_bottom: Option<Pixels>,
4776}
4777
4778impl RowCtx {
4779 #[allow(clippy::too_many_arguments)]
4785 fn row(
4789 &self,
4790 i: usize,
4791 row_data: TableRow,
4792 depth: usize,
4793 has_children: bool,
4794 tree_key: &SharedString,
4795 fixed_h: Option<Pixels>,
4796 window: &Window,
4797 cx: &mut App,
4798 ) -> AnyElement {
4799 let colors = cx.colors();
4800 let key = tree_key.clone();
4801 let is_selected = self.selected_keys.contains(&key);
4802 let is_disabled = self.disabled_keys.contains(&key);
4803 let selected_bg = is_selected.then(|| colors.surface.background.alpha(0.1));
4804 let hover_bg = (!is_selected && !is_disabled).then(|| {
4805 self.row_hover_bg.unwrap_or(if self.secondary {
4806 colors.default.color.alpha(0.5)
4807 } else {
4808 colors.surface.background.alpha(0.4)
4809 })
4810 });
4811 let hover_group: SharedString = format!("{}-row-hover-{i}", self.hover_group_prefix).into();
4812 let row_focused = self.cursor == Some(i);
4813 let row_anchor = self.cursor_own.read(cx).as_ref() == Some(tree_key);
4814
4815 let mut row = gpui::div()
4816 .id(element_id::indexed(&self.id, "row", i))
4817 .a11y(a11y::Role::Row)
4831 .when(self.selection_mode != SelectionMode::None, |row| {
4832 row.a11y_selected(is_selected)
4833 })
4834 .when(self.virtualized, |row| row.a11y_row_index(i))
4835 .when(self.is_tree, |row| row.a11y_level(depth))
4836 .when(self.is_tree && has_children, |row| {
4837 row.a11y_expanded(self.expanded.iter().any(|k| k == tree_key))
4838 })
4839 .flex()
4840 .group(hover_group.clone())
4841 .w_full()
4846 .when_some(fixed_h, |e, h| e.h(h));
4847 if !self.secondary && i + 1 < self.row_keys.len() {
4848 row = row.border_b_1().border_color(colors.separator.alpha(0.5));
4849 }
4850
4851 let data_count = row_data.cells.len();
4852 let round_top = (i == 0).then_some(self.round_top).flatten();
4855 let round_bottom = (i + 1 == self.row_keys.len())
4856 .then_some(self.round_bottom)
4857 .flatten();
4858 let paint = RowPaint {
4859 i,
4860 key,
4861 depth,
4862 has_children,
4863 is_selected,
4864 is_disabled,
4865 is_expanded: self.expanded.iter().any(|k| k == tree_key),
4866 selected_bg,
4867 hover_bg,
4868 hover_group,
4869 row_focused,
4870 data_count,
4871 round_top,
4872 round_bottom,
4873 };
4874
4875 if let Some(frozen) = &self.frozen {
4876 row = self.frozen_row_parts(row, frozen, &paint, row_data, cx);
4877 } else {
4878 if self.selectable {
4879 row = row.child(self.select_cell(&paint, cx));
4880 }
4881
4882 let cx: &App = cx;
4885 row = row.children(
4886 row_data
4887 .cells
4888 .into_iter()
4889 .enumerate()
4890 .map(|(c, cell)| self.data_cell(&paint, c, cell, cx)),
4891 );
4892 }
4893 row = self.row_press(row, &paint, cx);
4894
4895 row = row.when(is_disabled, |row| row.opacity(cx.layout().disabled_opacity));
4898 if row_anchor && self.focus.is_focused(window) {
4899 row = crate::util::record_focus_bounds(row, &self.focus, window, cx);
4900 }
4901 row.into_any_element()
4902 }
4903
4904 fn frozen_row_parts(
4909 &self,
4910 row: gpui::Stateful<gpui::Div>,
4911 frozen: &FrozenColumns,
4912 paint: &RowPaint,
4913 row_data: TableRow,
4914 cx: &App,
4915 ) -> gpui::Stateful<gpui::Div> {
4916 let data_count = row_data.cells.len();
4917 let mut lead = frozen.frozen_part();
4918 if self.selectable {
4919 lead = lead.child(self.select_cell(paint, cx));
4920 }
4921 let mut cells = row_data.cells.into_iter().enumerate();
4922 let lead = lead.children(
4923 cells
4924 .by_ref()
4925 .take(frozen.count)
4926 .map(|(c, cell)| self.data_cell(paint, c, cell, cx)),
4927 );
4928 let lead =
4929 lead.children((data_count..frozen.count).map(|c| self.empty_frozen_track(paint.i, c)));
4930 let rest = frozen
4931 .scrolling_part(
4932 gpui::div(),
4933 element_id::indexed(&self.id, "row-scroll", paint.i),
4934 )
4935 .children(cells.map(|(c, cell)| self.data_cell(paint, c, cell, cx)))
4936 .children(
4937 (data_count.max(frozen.count)..self.widths.len())
4938 .map(|c| self.empty_frozen_track(paint.i, c)),
4939 );
4940 row.child(lead).child(rest)
4941 }
4942
4943 fn empty_frozen_track(&self, row: usize, column: usize) -> gpui::Stateful<gpui::Div> {
4947 let (width, min_width, max_width) = self.widths[column];
4948 gpui::div()
4949 .when(width.is_none(), flex_cell)
4950 .when_some(width, |cell, width| cell.w(width))
4951 .when_some(min_width, |cell, width| cell.min_w(width))
4952 .when_some(max_width, |cell, width| cell.max_w(width))
4953 .id(element_id::indexed(
4954 &element_id::indexed(&self.id, "row", row),
4955 "empty-cell",
4956 column,
4957 ))
4958 }
4959
4960 fn select_cell(&self, paint: &RowPaint, cx: &App) -> gpui::Stateful<gpui::Div> {
4962 let RowPaint {
4963 i,
4964 is_selected,
4965 is_disabled,
4966 selected_bg,
4967 hover_bg,
4968 row_focused,
4969 data_count,
4970 round_top,
4971 round_bottom,
4972 ..
4973 } = *paint;
4974 let hover_group = &paint.hover_group;
4975 let mut cell = gpui::div()
4976 .id(element_id::indexed(&self.id, "select-cell", i))
4977 .a11y(a11y::Role::GridCell)
4981 .a11y_column_index(0)
4982 .flex()
4983 .items_center()
4984 .justify_center()
4985 .relative()
4986 .w(px(44.))
4987 .py(px(10.))
4988 .when_some(selected_bg, |cell, bg| cell.bg(bg))
4989 .when_some(hover_bg, |cell, bg| {
4990 cell.group_hover(hover_group.clone(), move |s| s.bg(bg))
4991 })
4992 .when_some(round_top, |cell, r| cell.rounded_tl(r))
4993 .when_some(round_bottom, |cell, r| cell.rounded_bl(r))
4994 .when(data_count == 0, |cell| {
4995 cell.when_some(round_top, |cell, r| cell.rounded_tr(r))
4996 .when_some(round_bottom, |cell, r| cell.rounded_br(r))
4997 })
4998 .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
4999 cx.stop_propagation();
5000 });
5001 let mut box_el = Checkbox::new(element_id::indexed(&self.id, "select", i))
5002 .is_selected(is_selected)
5003 .is_disabled(is_disabled);
5004 let cb = self.on_selection_change.clone();
5005 if !is_disabled && (cb.is_some() || self.selection_own.is_some()) {
5006 let current = self.selected_keys.clone();
5007 let key2 = paint.key.clone();
5008 let mode = self.selection_mode;
5009 let disallow_empty_selection = self.disallow_empty_selection;
5010 let selection_own = self.selection_own.clone();
5011 let selection_range = self.selection_range.clone();
5012 box_el = box_el.on_change(move |_next, window, cx| {
5013 cx.stop_propagation();
5014 let next = crate::selection::next_selection(
5018 ¤t,
5019 &key2,
5020 mode,
5021 disallow_empty_selection,
5022 );
5023 if let Some(held) = &selection_own {
5024 held.update(cx, |value, cx| {
5025 *value = next.clone();
5026 cx.notify();
5027 });
5028 }
5029 if let Some(cb) = &cb {
5030 cb(&next, window, cx);
5031 }
5032 if mode == SelectionMode::Multiple && !is_selected {
5033 selection_range.update(cx, |range, _| {
5034 range.anchor = Some(key2.clone());
5035 range.current = Some(key2.clone());
5036 range.is_all = false;
5037 });
5038 } else if mode == SelectionMode::Multiple {
5039 selection_range.update(cx, |range, _| {
5040 if range.is_all {
5041 *range = TableSelectionRange::default();
5042 }
5043 });
5044 }
5045 });
5046 }
5047 cell = cell.child(box_el);
5048 if row_focused {
5049 cell = cell.child(table_cell_focus_ring(cx, true, data_count == 0));
5050 }
5051 cell
5052 }
5053
5054 fn data_cell(
5057 &self,
5058 paint: &RowPaint,
5059 c: usize,
5060 cell: AnyElement,
5061 cx: &App,
5062 ) -> gpui::Stateful<gpui::Div> {
5063 let colors = cx.colors();
5064 let RowPaint {
5065 i,
5066 depth,
5067 has_children,
5068 is_disabled,
5069 selected_bg,
5070 hover_bg,
5071 row_focused,
5072 data_count,
5073 round_top,
5074 round_bottom,
5075 ..
5076 } = *paint;
5077 let hover_group = &paint.hover_group;
5078 let key = &paint.key;
5079 let tree_column = self.tree_column;
5080 let row_header_columns = &self.row_header_columns;
5081 let (width, min_width, max_width) =
5082 self.widths.get(c).copied().unwrap_or((None, None, None));
5083 let mut cell_el = gpui::div()
5084 .when(width.is_none(), flex_cell)
5085 .when_some(width, |e, w| e.w(w))
5086 .when_some(min_width, |e, w| e.min_w(w))
5087 .when_some(max_width, |e, w| e.max_w(w))
5088 .flex()
5089 .items_center()
5090 .gap(px(6.))
5091 .relative()
5092 .px(px(16.))
5094 .py(px(12.))
5095 .when(self.secondary, |e| {
5096 e.border_b_1()
5097 .border_color(colors.separator_tertiary().alpha(0.5))
5098 })
5099 .when(row_header_columns.get(c).copied().unwrap_or(false), |e| {
5100 e.font_weight(gpui::FontWeight::MEDIUM)
5101 })
5102 .when_some(selected_bg, |cell, bg| cell.bg(bg))
5103 .when_some(hover_bg, |cell, bg| {
5104 cell.group_hover(hover_group.clone(), move |s| s.bg(bg))
5105 })
5106 .when(!self.selectable && c == 0, |cell| {
5107 cell.when_some(round_top, |cell, r| cell.rounded_tl(r))
5108 .when_some(round_bottom, |cell, r| cell.rounded_bl(r))
5109 })
5110 .when(c + 1 == data_count, |cell| {
5111 cell.when_some(round_top, |cell, r| cell.rounded_tr(r))
5112 .when_some(round_bottom, |cell, r| cell.rounded_br(r))
5113 });
5114 if self.measure_intrinsic.get(c).copied().unwrap_or(false) {
5115 let measurements = self.measured_widths.clone();
5116 let tree_indent = if c == tree_column {
5117 tree_column_padding(depth).map_or(0., f32::from)
5118 } else {
5119 0.
5120 };
5121 let content_children = 1 + usize::from(
5126 c == tree_column && (has_children || depth > 0 || self.tree_column_has_children),
5127 );
5128 cell_el = cell_el.on_children_prepainted(move |bounds, _, cx| {
5129 let content =
5130 intrinsic_children_width(&bounds[..content_children.min(bounds.len())]);
5131 if content <= 0. {
5132 return;
5133 }
5134 let next = px(content + 32. + tree_indent);
5135 measurements.update(cx, |values, cx| {
5136 if values.len() <= c {
5137 values.resize(c + 1, None);
5138 }
5139 if values[c].is_none_or(|current| next > current) {
5140 values[c] = Some(next);
5141 cx.notify();
5142 }
5143 });
5144 });
5145 }
5146 let mut cell_el = cell_el
5159 .debug_selector(move || format!("table-row-track-{i}-{c}"))
5160 .id(element_id::indexed(
5161 &element_id::indexed(&self.id, "row", i),
5162 "cell",
5163 c,
5164 ))
5165 .a11y(if row_header_columns.get(c).copied().unwrap_or(false) {
5166 a11y::Role::RowHeader
5167 } else {
5168 a11y::Role::GridCell
5169 })
5170 .a11y_column_index(c + usize::from(self.selectable));
5171 if c == tree_column {
5174 if let Some(padding) = tree_column_padding(depth) {
5175 cell_el = cell_el.pl(padding);
5176 }
5177 if has_children {
5178 cell_el = cell_el.child(self.tree_chevron(paint, cx));
5179 } else if depth > 0 || self.tree_column_has_children {
5180 cell_el = cell_el.child(gpui::div().size(px(18.)).flex_shrink_0());
5183 }
5184 }
5185 cell_el = cell_el.child(cell);
5186 if let Some(cell_ctx) = &self.cell {
5187 cell_el = cell_ctx.decorate(cell_el, key, c, is_disabled, cx);
5188 }
5189 if row_focused && self.cell.is_none() {
5190 let cell_index = c + usize::from(self.selectable);
5191 let total_cells = data_count + usize::from(self.selectable);
5192 cell_el = cell_el.child(table_cell_focus_ring(
5193 cx,
5194 cell_index == 0,
5195 cell_index + 1 == total_cells,
5196 ));
5197 }
5198 cell_el
5199 }
5200
5201 fn tree_chevron(&self, paint: &RowPaint, cx: &App) -> gpui::Stateful<gpui::Div> {
5203 let colors = cx.colors();
5204 let is_disabled = paint.is_disabled;
5205 let is_expanded = paint.is_expanded;
5206 let toggle_key = paint.key.clone();
5207 let mut chevron = gpui::div()
5208 .id(element_id::scoped(
5209 &element_id::scoped(&self.id, "expand"),
5210 toggle_key.clone(),
5211 ))
5212 .flex()
5213 .items_center()
5214 .justify_center()
5215 .size(px(18.))
5216 .flex_shrink_0()
5217 .when(!is_disabled, |chevron| {
5218 chevron
5219 .cursor(crate::util::interactive_cursor(cx))
5220 .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
5221 cx.stop_propagation();
5222 })
5223 })
5224 .child(
5225 gpui::svg()
5226 .size(px(12.))
5227 .path(if is_expanded {
5228 icons::CHEVRON_DOWN
5229 } else {
5230 icons::CHEVRON_RIGHT
5231 })
5232 .text_color(colors.muted),
5233 );
5234 if !is_disabled {
5235 if let Some(cb) = self.on_expanded_change.clone() {
5236 let key = toggle_key;
5237 let before = self.expanded.clone();
5238 let focus = self.focus.clone();
5239 let cursor = self.cursor_own.clone();
5240 chevron = chevron.on_click(move |_, window, cx| {
5241 cx.stop_propagation();
5242 let mut next = before.as_ref().clone();
5243 if let Some(at) = next.iter().position(|k| *k == key) {
5244 next.remove(at);
5245 } else {
5246 next.push(key.clone());
5247 }
5248 cb(&next, window, cx);
5249 window.focus(&focus, cx);
5250 cursor.update(cx, |value, cx| {
5251 *value = Some(key.clone());
5252 cx.notify();
5253 });
5254 });
5255 }
5256 }
5257 chevron
5258 }
5259
5260 fn row_press(
5263 &self,
5264 row: gpui::Stateful<gpui::Div>,
5265 paint: &RowPaint,
5266 cx: &App,
5267 ) -> gpui::Stateful<gpui::Div> {
5268 let i = paint.i;
5269 let key = paint.key.clone();
5270 let row_action = self.on_row_click.clone();
5271 let row_selection = self.on_selection_change.clone();
5272 let selection_own = self.selection_own.clone();
5273 let selection_behavior = self.selection_behavior;
5274 let disallow_empty_selection = self.disallow_empty_selection;
5275 if paint.is_disabled
5276 || !(row_action.is_some()
5277 || (self.selectable && (row_selection.is_some() || selection_own.is_some())))
5278 {
5279 return row;
5280 }
5281 let current = self.selected_keys.clone();
5282 let mode = self.selection_mode;
5283 let selection_range = self.selection_range.clone();
5284 let range_collection = self.row_keys.clone();
5285 let range_selectable: Vec<SharedString> = self
5286 .row_keys
5287 .iter()
5288 .filter(|key| !self.disabled_keys.contains(key))
5289 .cloned()
5290 .collect();
5291 let moved = self.cursor_own.clone();
5292 let focus = self.focus.clone();
5293 let focus_for_click = self.focus.clone();
5294 let key_for_cursor = key.clone();
5295 row.cursor(crate::util::interactive_cursor(cx))
5296 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
5297 if window.default_prevented() {
5298 return;
5299 }
5300 window.focus(&focus, cx);
5301 moved.update(cx, |value, cx| {
5302 *value = Some(key_for_cursor.clone());
5303 cx.notify();
5304 });
5305 })
5306 .on_click(move |ev, w, cx| {
5307 if !focus_for_click.is_focused(w) {
5308 return;
5309 }
5310 match row_intent(
5311 RowActivation::Pointer,
5312 mode,
5313 selection_behavior,
5314 current.is_empty(),
5315 row_action.is_some(),
5316 ) {
5317 RowIntent::Action => {
5318 if let Some(cb) = &row_action {
5319 cb(&i, ev, w, cx);
5320 }
5321 }
5322 RowIntent::Selection => {
5323 let was_selected = current.contains(&key);
5324 let extends_selection =
5325 ev.modifiers().shift && mode == SelectionMode::Multiple;
5326 let next = if extends_selection {
5327 let range = selection_range.read(cx).clone();
5328 extend_selection_range(
5329 ¤t,
5330 &range_collection,
5331 &range_selectable,
5332 &range,
5333 &key,
5334 )
5335 } else {
5336 let modifiers = ev.modifiers();
5337 let non_contiguous = if cfg!(target_os = "macos") {
5338 modifiers.alt
5339 } else {
5340 modifiers.control
5341 };
5342 crate::selection::next_selection_with_behavior(
5343 ¤t,
5344 &key,
5345 mode,
5346 if non_contiguous {
5347 SelectionBehavior::Toggle
5348 } else {
5349 selection_behavior
5350 },
5351 disallow_empty_selection,
5352 )
5353 };
5354 let changed = !same_selection(&next, ¤t);
5355 if changed {
5356 if let Some(held) = &selection_own {
5357 held.update(cx, |value, cx| {
5358 *value = next.clone();
5359 cx.notify();
5360 });
5361 }
5362 if let Some(cb) = &row_selection {
5363 cb(&next, w, cx);
5364 }
5365 }
5366 if extends_selection && changed {
5367 selection_range.update(cx, |range, _| {
5368 if range.anchor.is_none() {
5369 range.anchor = Some(key.clone());
5370 }
5371 range.current = Some(key.clone());
5372 range.is_all = false;
5373 });
5374 } else if !extends_selection
5375 && mode == SelectionMode::Multiple
5376 && !was_selected
5377 {
5378 selection_range.update(cx, |range, _| {
5379 range.anchor = Some(key.clone());
5380 range.current = Some(key.clone());
5381 range.is_all = false;
5382 });
5383 } else if !extends_selection && mode == SelectionMode::Multiple {
5384 selection_range.update(cx, |range, _| {
5385 if range.is_all {
5386 *range = TableSelectionRange::default();
5387 }
5388 });
5389 }
5390 }
5391 RowIntent::None => {}
5392 }
5393 })
5394 }
5395}
5396
5397type OnColumnMove = std::sync::Arc<dyn Fn(&ColumnMove, &mut Window, &mut App) + 'static>;
5405type OnCellSelect = std::sync::Arc<dyn Fn(&TableCell, &mut Window, &mut App) + 'static>;
5406
5407#[derive(Clone, Debug, PartialEq, Eq)]
5411#[non_exhaustive]
5412pub struct ColumnMove {
5413 pub from: usize,
5415 pub to: usize,
5417 pub order: Vec<usize>,
5419}
5420
5421#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5425pub struct TableCell {
5426 pub row: SharedString,
5428 pub column: usize,
5430}
5431
5432impl TableCell {
5433 pub fn new(row: impl Into<SharedString>, column: usize) -> Self {
5436 Self {
5437 row: row.into(),
5438 column,
5439 }
5440 }
5441}
5442
5443#[derive(Default)]
5445struct ColumnReorder {
5446 allowed: bool,
5447 order: Option<Vec<usize>>,
5448 default_order: Option<Vec<usize>>,
5449 on_move: Option<OnColumnMove>,
5450}
5451
5452#[derive(Default)]
5454struct CellSelection {
5455 enabled: bool,
5456 selected: Option<Option<TableCell>>,
5458 default: Option<TableCell>,
5459 on_select: Option<OnCellSelect>,
5460}
5461
5462fn sanitize_order(order: &[usize], n: usize) -> Vec<usize> {
5465 let mut seen = vec![false; n];
5466 let mut out = Vec::with_capacity(n);
5467 for &ix in order {
5468 if ix < n && !seen[ix] {
5469 seen[ix] = true;
5470 out.push(ix);
5471 }
5472 }
5473 out.extend((0..n).filter(|ix| !seen[*ix]));
5474 out
5475}
5476
5477fn moved_order(order: &[usize], from: usize, to: usize) -> Vec<usize> {
5479 let mut next = order.to_vec();
5480 if from < next.len() && to < next.len() && from != to {
5481 let column = next.remove(from);
5482 next.insert(to, column);
5483 }
5484 next
5485}
5486
5487fn permute_row(row: &mut TableRow, order: &[usize]) {
5490 let mut cells: Vec<Option<AnyElement>> = std::mem::take(&mut row.cells)
5491 .into_iter()
5492 .map(Some)
5493 .collect();
5494 row.cells = order
5495 .iter()
5496 .filter_map(|&ix| cells.get_mut(ix).and_then(Option::take))
5497 .collect();
5498 for child in &mut row.children {
5499 permute_row(child, order);
5500 }
5501}
5502
5503fn permute_positions<T: Clone>(
5506 values: &[Option<T>],
5507 from: &[usize],
5508 to: &[usize],
5509) -> Vec<Option<T>> {
5510 to.iter()
5511 .map(|column| {
5512 from.iter()
5513 .position(|c| c == column)
5514 .and_then(|at| values.get(at).cloned().flatten())
5515 })
5516 .collect()
5517}
5518
5519type HeaderBounds = std::rc::Rc<std::cell::RefCell<(Pixels, Vec<gpui::Bounds<Pixels>>)>>;
5521
5522#[derive(Clone, Copy, Debug, PartialEq)]
5524struct ColumnDrag {
5525 from: usize,
5526 start_x: f32,
5527 active: bool,
5528 target: usize,
5529}
5530
5531fn header_at(bounds: &[gpui::Bounds<Pixels>], x: Pixels) -> Option<usize> {
5533 let first = bounds.first()?;
5534 if x < first.left() {
5535 return Some(0);
5536 }
5537 bounds
5538 .iter()
5539 .position(|b| x >= b.left() && x < b.right())
5540 .or(Some(bounds.len() - 1))
5541}
5542
5543impl Table {
5544 pub fn allows_column_reorder(mut self, allowed: bool) -> Self {
5548 self.column_reorder.allowed = allowed;
5549 self
5550 }
5551
5552 pub fn column_order(mut self, order: impl IntoIterator<Item = usize>) -> Self {
5556 self.column_reorder.order = Some(order.into_iter().collect());
5557 self
5558 }
5559
5560 pub fn default_column_order(mut self, order: impl IntoIterator<Item = usize>) -> Self {
5563 self.column_reorder.default_order = Some(order.into_iter().collect());
5564 self
5565 }
5566
5567 pub fn on_column_move(
5569 mut self,
5570 f: impl Fn(&ColumnMove, &mut Window, &mut App) + 'static,
5571 ) -> Self {
5572 self.column_reorder.on_move = Some(std::sync::Arc::new(f));
5573 self
5574 }
5575
5576 pub fn cell_selectable(mut self, selectable: bool) -> Self {
5583 self.cell_selection.enabled = selectable;
5584 self
5585 }
5586
5587 pub fn selected_cell(mut self, cell: Option<TableCell>) -> Self {
5589 self.cell_selection.selected = Some(cell);
5590 self
5591 }
5592
5593 pub fn default_selected_cell(mut self, cell: TableCell) -> Self {
5596 self.cell_selection.default = Some(cell);
5597 self
5598 }
5599
5600 pub fn on_cell_select(
5602 mut self,
5603 f: impl Fn(&TableCell, &mut Window, &mut App) + 'static,
5604 ) -> Self {
5605 self.cell_selection.on_select = Some(std::sync::Arc::new(f));
5606 self
5607 }
5608
5609 fn apply_column_order(
5613 &mut self,
5614 base_id: &gpui::ElementId,
5615 window: &mut Window,
5616 cx: &mut App,
5617 ) -> std::rc::Rc<Vec<usize>> {
5618 let n = self.columns.len();
5619 let reorder = &self.column_reorder;
5620 let configured =
5621 reorder.allowed || reorder.order.is_some() || reorder.default_order.is_some();
5622 if !configured && !self.columns.iter().any(|c| c.frozen) {
5623 return std::rc::Rc::new((0..n).collect());
5624 }
5625 let order = if configured {
5626 let seed = sanitize_order(reorder.default_order.as_deref().unwrap_or(&[]), n);
5627 let (order, _) = crate::util::controlled(
5628 window,
5629 cx,
5630 element_id::scoped(base_id, "column-order"),
5631 reorder.order.as_ref().map(|order| sanitize_order(order, n)),
5632 seed,
5633 );
5634 sanitize_order(&order, n)
5635 } else {
5636 (0..n).collect()
5637 };
5638 let order = frozen_first(&order, &self.columns);
5640 let rendered = window.use_keyed_state(
5643 element_id::scoped(base_id, "rendered-column-order"),
5644 cx,
5645 |_, _| (0..n).collect::<Vec<usize>>(),
5646 );
5647 let previous = rendered.read(cx).clone();
5648 if previous != order {
5649 for part in ["resized", "measured-widths"] {
5650 let widths =
5651 window.use_keyed_state(element_id::scoped(base_id, part), cx, |_, _| {
5652 Vec::<Option<Pixels>>::new()
5653 });
5654 widths.update(cx, |values, _| {
5655 *values = permute_positions(values, &previous, &order);
5656 });
5657 }
5658 rendered.update(cx, |value, _| value.clone_from(&order));
5659 }
5660 if order.iter().enumerate().all(|(at, ix)| at == *ix) {
5661 return std::rc::Rc::new(order);
5662 }
5663 let mut given: Vec<Option<TableColumn>> = std::mem::take(&mut self.columns)
5664 .into_iter()
5665 .map(Some)
5666 .collect();
5667 self.columns = order
5668 .iter()
5669 .filter_map(|&ix| given.get_mut(ix).and_then(Option::take))
5670 .collect();
5671 for row in &mut self.rows {
5672 permute_row(row, &order);
5673 }
5674 if let Some((_, _, _, row)) = &mut self.virtual_rows {
5675 let inner = row.clone();
5676 let order = order.clone();
5677 *row = crate::util::shared(move |ix| {
5678 let mut built = inner(ix);
5679 permute_row(&mut built, &order);
5680 built
5681 });
5682 }
5683 if let Some(at) = order.iter().position(|ix| *ix == self.tree_column) {
5684 self.tree_column = at;
5685 }
5686 std::rc::Rc::new(order)
5687 }
5688
5689 fn header_bounds(base_id: &gpui::ElementId, window: &mut Window, cx: &mut App) -> HeaderBounds {
5692 window
5693 .use_keyed_state(element_id::scoped(base_id, "header-bounds"), cx, |_, _| {
5694 std::rc::Rc::new(std::cell::RefCell::new((
5695 px(0.),
5696 Vec::<gpui::Bounds<Pixels>>::new(),
5697 )))
5698 })
5699 .read(cx)
5700 .clone()
5701 }
5702
5703 fn column_reorder_measure(
5706 &self,
5707 header: gpui::Div,
5708 base_id: &gpui::ElementId,
5709 window: &mut Window,
5710 cx: &mut App,
5711 ) -> gpui::Div {
5712 if !self.column_reorder.allowed || self.columns.len() < 2 {
5713 return header;
5714 }
5715 let bounds = Self::header_bounds(base_id, window, cx);
5716 let skip = usize::from(self.selection_mode != SelectionMode::None);
5717 let columns = self.columns.len();
5718 header.on_children_prepainted(move |children, _, _| {
5719 let origin = children.first().map_or(px(0.), |b| b.left());
5720 let headers = children.iter().skip(skip).take(columns).copied().collect();
5721 *bounds.borrow_mut() = (origin, headers);
5722 })
5723 }
5724
5725 fn frozen_header_parts(
5730 &self,
5731 base_id: &gpui::ElementId,
5732 frozen: &FrozenColumns,
5733 mut cells: Vec<AnyElement>,
5734 window: &mut Window,
5735 cx: &mut App,
5736 ) -> (gpui::Div, gpui::Stateful<gpui::Div>) {
5737 let skip = usize::from(self.selection_mode != SelectionMode::None);
5738 let rest = cells.split_off((skip + frozen.count).min(cells.len()));
5739 let mut lead = frozen.frozen_part();
5740 let mut part = gpui::div();
5741 if self.column_reorder.allowed && self.columns.len() >= 2 {
5742 let bounds = Self::header_bounds(base_id, window, cx);
5743 let scrolled = bounds.clone();
5744 let count = frozen.count;
5745 lead = lead.on_children_prepainted(move |children, _, _| {
5746 let origin = children.first().map_or(px(0.), |b| b.left());
5747 let mut measured = bounds.borrow_mut();
5748 measured.0 = origin;
5749 measured.1.clear();
5750 measured.1.extend(children.iter().skip(skip).copied());
5751 });
5752 part = part.on_children_prepainted(move |children, _, _| {
5753 let mut measured = scrolled.borrow_mut();
5754 measured.1.truncate(count);
5755 measured.1.extend(children.iter().copied());
5756 });
5757 }
5758 (
5759 lead.children(cells),
5760 frozen
5761 .scrolling_part(part, element_id::scoped(base_id, "header-scroll"))
5762 .children(rest),
5763 )
5764 }
5765
5766 #[allow(clippy::too_many_arguments)]
5769 fn column_reorder_header(
5770 &self,
5771 header: gpui::Stateful<gpui::Div>,
5772 base_id: &gpui::ElementId,
5773 order: &std::rc::Rc<Vec<usize>>,
5774 header_focus: &[gpui::FocusHandle],
5775 resizing: &gpui::Entity<Option<(usize, f32, f32)>>,
5776 window: &mut Window,
5777 cx: &mut App,
5778 ) -> gpui::Stateful<gpui::Div> {
5779 if !self.column_reorder.allowed || self.columns.len() < 2 {
5780 return header;
5781 }
5782 let bounds = Self::header_bounds(base_id, window, cx);
5783 let drag =
5784 window.use_keyed_state(element_id::scoped(base_id, "column-drag"), cx, |_, _| {
5785 None::<ColumnDrag>
5786 });
5787 let own_order = if self.column_reorder.order.is_none() {
5788 Some(
5789 window.use_keyed_state(element_id::scoped(base_id, "column-order"), cx, |_, _| {
5790 order.as_ref().clone()
5791 }),
5792 )
5793 } else {
5794 None
5795 };
5796 let on_move = self.column_reorder.on_move.clone();
5797 let frozen = self.columns.iter().take_while(|c| c.frozen).count();
5800 let frozen = if frozen < self.columns.len() {
5801 frozen
5802 } else {
5803 0
5804 };
5805 let commit = crate::util::shared({
5806 let order = order.clone();
5807 move |from: usize, to: usize, window: &mut Window, cx: &mut App| {
5808 let to = clamp_to_region(from, to, frozen);
5809 if from == to || to >= order.len() {
5810 return;
5811 }
5812 let next = moved_order(&order, from, to);
5813 if let Some(own) = &own_order {
5814 own.update(cx, |value, cx| {
5815 value.clone_from(&next);
5816 cx.notify();
5817 });
5818 }
5819 if let Some(cb) = &on_move {
5820 cb(
5821 &ColumnMove {
5822 from,
5823 to,
5824 order: next,
5825 },
5826 window,
5827 cx,
5828 );
5829 }
5830 }
5831 });
5832
5833 let accent = cx.colors().accent.color;
5834 let drag_now = *drag.read(cx);
5835 let mut header = header.relative();
5836 if let Some(active) = drag_now.filter(|d| d.active) {
5837 let measured = bounds.borrow();
5838 if let Some(target) = measured.1.get(active.target) {
5839 let x = if active.target > active.from {
5842 target.right()
5843 } else {
5844 target.left()
5845 };
5846 header = header.cursor(gpui::CursorStyle::ClosedHand).child(
5847 gpui::div()
5848 .absolute()
5849 .top_0()
5850 .bottom_0()
5851 .left(x - measured.0 - px(1.))
5852 .w(px(2.))
5853 .bg(accent)
5854 .debug_selector(|| "table-column-drop-indicator".into()),
5855 );
5856 }
5857 }
5858 let down_bounds = bounds.clone();
5859 let move_bounds = bounds;
5860 let down_drag = drag.clone();
5861 let move_drag = drag.clone();
5862 let up_drag = drag.clone();
5863 let out_drag = drag;
5864 let up_commit = commit.clone();
5865 let out_commit = commit.clone();
5866 let key_commit = commit;
5867 let resizing = resizing.clone();
5868 let focus = header_focus.to_vec();
5869 header
5870 .on_mouse_down(gpui::MouseButton::Left, move |event, _, cx| {
5871 if resizing.read(cx).is_some() {
5873 return;
5874 }
5875 let Some(from) = header_at(&down_bounds.borrow().1, event.position.x) else {
5876 return;
5877 };
5878 down_drag.update(cx, |d, _| {
5879 *d = Some(ColumnDrag {
5880 from,
5881 start_x: f32::from(event.position.x),
5882 active: false,
5883 target: from,
5884 });
5885 });
5886 })
5887 .on_mouse_move(move |event, _, cx| {
5888 let Some(mut current) = *move_drag.read(cx) else {
5889 return;
5890 };
5891 if event.pressed_button != Some(gpui::MouseButton::Left) {
5892 move_drag.update(cx, |d, cx| {
5893 *d = None;
5894 cx.notify();
5895 });
5896 return;
5897 }
5898 let x = f32::from(event.position.x);
5899 if !current.active && (x - current.start_x).abs() < 6. {
5900 return;
5901 }
5902 current.active = true;
5903 if let Some(target) = header_at(&move_bounds.borrow().1, event.position.x) {
5904 current.target = clamp_to_region(current.from, target, frozen);
5905 }
5906 move_drag.update(cx, |d, cx| {
5907 if *d != Some(current) {
5908 *d = Some(current);
5909 cx.notify();
5910 }
5911 });
5912 })
5913 .on_mouse_up(gpui::MouseButton::Left, move |_, window, cx| {
5914 let done = up_drag.update(cx, |d, cx| {
5915 cx.notify();
5916 d.take()
5917 });
5918 if let Some(d) = done.filter(|d| d.active) {
5919 up_commit(d.from, d.target, window, cx);
5920 }
5921 })
5922 .on_mouse_up_out(gpui::MouseButton::Left, move |_, window, cx| {
5923 let done = out_drag.update(cx, |d, cx| {
5924 cx.notify();
5925 d.take()
5926 });
5927 if let Some(d) = done.filter(|d| d.active) {
5928 out_commit(d.from, d.target, window, cx);
5929 }
5930 })
5931 .on_key_down(move |event, window, cx| {
5932 let m = &event.keystroke.modifiers;
5933 if !m.alt || m.control || m.platform || m.shift || m.function {
5934 return;
5935 }
5936 let forward = match event.keystroke.key.as_str() {
5937 "right" => true,
5938 "left" => false,
5939 _ => return,
5940 };
5941 let Some(from) = focus.iter().position(|h| h.is_focused(window)) else {
5942 return;
5943 };
5944 let to = if forward {
5945 (from + 1).min(focus.len() - 1)
5946 } else {
5947 from.saturating_sub(1)
5948 };
5949 let to = clamp_to_region(from, to, frozen);
5950 cx.stop_propagation();
5951 crate::util::set_focus_visible(true, cx);
5952 if to != from {
5953 key_commit(from, to, window, cx);
5954 window.focus(&focus[to], cx);
5956 }
5957 })
5958 }
5959}
5960
5961#[derive(Clone)]
5963struct CellCtx {
5964 selected: Option<(SharedString, usize)>,
5966 column: gpui::Entity<Option<usize>>,
5968 cursor: gpui::Entity<Option<SharedString>>,
5970 focus: gpui::FocusHandle,
5971}
5972
5973impl CellCtx {
5974 fn decorate(
5976 &self,
5977 el: gpui::Stateful<gpui::Div>,
5978 row: &SharedString,
5979 column: usize,
5980 disabled: bool,
5981 cx: &App,
5982 ) -> gpui::Stateful<gpui::Div> {
5983 let is_selected = self
5984 .selected
5985 .as_ref()
5986 .is_some_and(|(r, c)| r == row && *c == column);
5987 let mut el = el.a11y_selected(is_selected);
5988 if is_selected {
5989 el = el.child(
5990 crate::util::inset_focus_ring(cx).debug_selector(|| "table-selected-cell".into()),
5991 );
5992 }
5993 if disabled {
5994 return el;
5995 }
5996 let (cursor, column_state, focus, row) = (
5997 self.cursor.clone(),
5998 self.column.clone(),
5999 self.focus.clone(),
6000 row.clone(),
6001 );
6002 el.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
6003 window.focus(&focus, cx);
6004 crate::util::set_focus_visible(false, cx);
6005 column_state.update(cx, |c, cx| {
6006 *c = Some(column);
6007 cx.notify();
6008 });
6009 cursor.update(cx, |c, cx| {
6010 *c = Some(row.clone());
6011 cx.notify();
6012 });
6013 })
6014 }
6015}
6016
6017impl Table {
6018 fn cell_selection_state(
6022 &self,
6023 base_id: &gpui::ElementId,
6024 order: &[usize],
6025 row_cursor: &gpui::Entity<Option<SharedString>>,
6026 focus: &gpui::FocusHandle,
6027 window: &mut Window,
6028 cx: &mut App,
6029 ) -> Option<CellCtx> {
6030 if !self.cell_selection.enabled || order.is_empty() {
6031 return None;
6032 }
6033 let last = order.len() - 1;
6034 let position = |column: usize| order.iter().position(|c| *c == column);
6035 let column =
6036 window.use_keyed_state(element_id::scoped(base_id, "cell-column"), cx, |_, _| {
6037 None::<usize>
6038 });
6039 let reported =
6040 window.use_keyed_state(element_id::scoped(base_id, "cell-reported"), cx, |_, _| {
6041 None::<TableCell>
6042 });
6043 let seed = match &self.cell_selection.selected {
6044 Some(prop) => prop.clone(),
6045 None => self.cell_selection.default.clone(),
6046 };
6047 crate::util::seed_once(window, cx, element_id::scoped(base_id, "cell-seed"), |cx| {
6048 if let Some(seed) = seed {
6049 row_cursor.update(cx, |c, _| *c = Some(seed.row.clone()));
6050 column.update(cx, |c, _| *c = position(seed.column));
6051 reported.update(cx, |r, _| *r = Some(seed));
6052 }
6053 });
6054 let at = column.read(cx).unwrap_or(0).min(last);
6055 let requested = row_cursor.read(cx).clone().map(|row| TableCell {
6056 row,
6057 column: order[at],
6058 });
6059 if requested.is_some() && requested != *reported.read(cx) {
6060 reported.update(cx, |r, _| r.clone_from(&requested));
6061 if let (Some(cb), Some(cell)) =
6062 (self.cell_selection.on_select.clone(), requested.clone())
6063 {
6064 window.defer(cx, move |window, cx| cb(&cell, window, cx));
6065 }
6066 }
6067 let shown = match &self.cell_selection.selected {
6068 Some(prop) => {
6069 if *prop != requested {
6070 let prop = prop.clone();
6071 row_cursor.update(cx, |c, _| *c = prop.as_ref().map(|p| p.row.clone()));
6072 column.update(cx, |c, _| {
6073 *c = prop.as_ref().and_then(|p| position(p.column));
6074 });
6075 reported.update(cx, |r, _| r.clone_from(&prop));
6076 }
6077 prop.clone()
6078 }
6079 None => requested,
6080 };
6081 Some(CellCtx {
6082 selected: shown.and_then(|cell| Some((cell.row, position(cell.column)?))),
6083 column,
6084 cursor: row_cursor.clone(),
6085 focus: focus.clone(),
6086 })
6087 }
6088
6089 fn cell_selection_keys(
6092 &self,
6093 wrapper: gpui::Div,
6094 cell: &Option<CellCtx>,
6095 rows: &[SharedString],
6096 frozen: Option<FrozenColumns>,
6097 ) -> gpui::Div {
6098 let Some(cell) = cell.clone() else {
6099 return wrapper;
6100 };
6101 let last = self.columns.len().saturating_sub(1);
6102 let first_row = rows
6103 .iter()
6104 .find(|key| !self.disabled_keys.contains(key))
6105 .cloned();
6106 wrapper.capture_key_down(move |event, window, cx| {
6107 if !cell.focus.is_focused(window) {
6108 return;
6109 }
6110 let m = &event.keystroke.modifiers;
6111 if m.control || m.alt || m.platform || m.shift || m.function {
6112 return;
6113 }
6114 let at = cell.column.read(cx).unwrap_or(0).min(last);
6115 let to = match event.keystroke.key.as_str() {
6116 "left" => at.saturating_sub(1),
6117 "right" => (at + 1).min(last),
6118 "home" => 0,
6119 "end" => last,
6120 _ => return,
6121 };
6122 cx.stop_propagation();
6123 crate::util::set_focus_visible(true, cx);
6124 if cell.cursor.read(cx).is_none() {
6125 let first = first_row.clone();
6126 cell.cursor.update(cx, |c, cx| {
6127 *c = first;
6128 cx.notify();
6129 });
6130 }
6131 cell.column.update(cx, |c, cx| {
6132 *c = Some(to);
6133 cx.notify();
6134 });
6135 if let Some(frozen) = &frozen {
6136 frozen.reveal(to);
6137 }
6138 })
6139 }
6140}
6141
6142#[cfg(test)]
6143mod tests {
6144 use super::*;
6145
6146 #[test]
6147 fn tree_column_padding_uses_one_rem_per_one_based_level() {
6148 assert_eq!(tree_column_padding(0), None);
6149 assert_eq!(tree_column_padding(1), Some(px(32.)));
6150 assert_eq!(tree_column_padding(2), Some(px(48.)));
6151 assert_eq!(tree_column_padding(3), Some(px(64.)));
6152 }
6153
6154 #[test]
6155 fn a_new_column_starts_ascending() {
6156 let next = SortDescriptor::next(None, "name");
6157 assert_eq!(next, SortDescriptor::new("name", SortDirection::Ascending));
6158 }
6159
6160 #[test]
6161 fn clicking_the_sorted_column_flips_it() {
6162 let current = SortDescriptor::new("name", SortDirection::Ascending);
6163 let next = SortDescriptor::next(Some(¤t), "name");
6164 assert_eq!(next.direction, SortDirection::Descending);
6165 let back = SortDescriptor::next(Some(&next), "name");
6166 assert_eq!(back.direction, SortDirection::Ascending);
6167 }
6168
6169 #[test]
6170 fn disabled_only_controlled_selection_is_indeterminate() {
6171 let selectable = vec![SharedString::from("alpha")];
6172 let selected = vec![SharedString::from("beta")];
6173 assert_eq!(select_all_flags(&selectable, &selected), (false, true));
6174 }
6175
6176 #[test]
6177 fn switching_column_resets_to_ascending() {
6178 let current = SortDescriptor::new("name", SortDirection::Descending);
6179 let next = SortDescriptor::next(Some(¤t), "size");
6180 assert_eq!(next, SortDescriptor::new("size", SortDirection::Ascending));
6181 }
6182
6183 #[test]
6184 fn row_keys_fall_back_to_the_index() {
6185 let table = Table::new(vec!["A".into()])
6186 .row(vec![])
6187 .keyed_row("second", vec![])
6188 .row(vec![]);
6189 assert_eq!(
6190 table.row_keys(),
6191 vec![
6192 SharedString::from("0"),
6193 SharedString::from("second"),
6194 SharedString::from("2"),
6195 ]
6196 );
6197 }
6198
6199 #[test]
6205 fn shift_home_end_extends_only_from_control_outside_macos() {
6206 for key in ["home", "end"] {
6207 assert!(
6208 !shift_home_end_extends(key, true, true),
6209 "macOS registers no Home/End extension"
6210 );
6211 assert!(!shift_home_end_extends(key, false, true));
6212 assert!(
6213 shift_home_end_extends(key, true, false),
6214 "Control+Shift+{key} must extend on Windows and Linux"
6215 );
6216 assert!(
6217 !shift_home_end_extends(key, false, false),
6218 "plain Shift+{key} must only move the focus"
6219 );
6220 }
6221 }
6222
6223 #[test]
6227 fn shift_navigation_keys_do_not_consult_the_home_end_gate() {
6228 for key in ["left", "right", "up", "down"] {
6229 assert!(shift_home_end_extends(key, false, true));
6230 assert!(shift_home_end_extends(key, true, false));
6231 }
6232 }
6233
6234 #[test]
6243 fn home_end_registration_matches_the_pinned_chord_map() {
6244 let none = gpui::Modifiers::none();
6245 let shift = gpui::Modifiers {
6246 shift: true,
6247 ..none
6248 };
6249 let alt = gpui::Modifiers { alt: true, ..none };
6250 let alt_shift = gpui::Modifiers { shift: true, ..alt };
6251 let function = gpui::Modifiers {
6252 function: true,
6253 ..none
6254 };
6255 let function_alt = gpui::Modifiers {
6256 alt: true,
6257 ..function
6258 };
6259 for modifiers in [none, shift, alt, alt_shift, function, function_alt] {
6260 assert!(
6261 home_end_registered(modifiers, true),
6262 "macOS must register {modifiers:?}"
6263 );
6264 }
6265 let control = gpui::Modifiers {
6266 control: true,
6267 ..none
6268 };
6269 let control_shift = gpui::Modifiers {
6270 shift: true,
6271 ..control
6272 };
6273 let platform = gpui::Modifiers {
6274 platform: true,
6275 ..none
6276 };
6277 let platform_shift = gpui::Modifiers {
6278 shift: true,
6279 ..platform
6280 };
6281 for modifiers in [control, control_shift, platform, platform_shift] {
6282 assert!(
6283 !home_end_registered(modifiers, true),
6284 "macOS must not register {modifiers:?}"
6285 );
6286 }
6287 for modifiers in [none, shift, control, control_shift, function] {
6288 assert!(
6289 home_end_registered(modifiers, false),
6290 "Windows and Linux must register {modifiers:?}"
6291 );
6292 }
6293 for modifiers in [alt, alt_shift, function_alt, platform, platform_shift] {
6294 assert!(
6295 !home_end_registered(modifiers, false),
6296 "Windows and Linux must not register {modifiers:?}"
6297 );
6298 }
6299 }
6300
6301 #[test]
6308 fn keystroke_spellings_reach_the_registration_gate() {
6309 let ctrl_shift_home = gpui::Keystroke::parse("ctrl-shift-home").unwrap();
6310 assert!(home_end_registered(ctrl_shift_home.modifiers, false));
6311 assert!(!home_end_registered(ctrl_shift_home.modifiers, true));
6312 let cmd_shift_home = gpui::Keystroke::parse("cmd-shift-home").unwrap();
6313 assert!(!home_end_registered(cmd_shift_home.modifiers, true));
6314 let alt_shift_end = gpui::Keystroke::parse("alt-shift-end").unwrap();
6315 assert!(home_end_registered(alt_shift_end.modifiers, true));
6316 assert!(!home_end_registered(alt_shift_end.modifiers, false));
6317 let fn_home = gpui::Keystroke::parse("fn-home").unwrap();
6318 assert!(fn_home.modifiers.function);
6319 assert!(home_end_registered(fn_home.modifiers, true));
6320 assert!(home_end_registered(fn_home.modifiers, false));
6321 }
6322
6323 #[test]
6327 fn the_sort_header_hover_paints_no_background() {
6328 let source = include_str!("table.rs")
6331 .split("#[cfg(test)]")
6332 .next()
6333 .expect("the implementation section is always present");
6334 assert!(
6335 source.contains(".group_hover(sort_group, |s| s.text_color(colors.foreground))"),
6336 "the sortable header hover must recolour the text to `--foreground` \
6337 (pinned `.table__column[data-allows-sorting]:hover`)"
6338 );
6339 assert!(
6340 !source.contains("colors.default.soft()"),
6341 "the sortable header hover must not come back as a soft background"
6342 );
6343 }
6344
6345 #[test]
6349 fn the_row_hover_is_variant_specific() {
6350 let source = include_str!("table.rs")
6352 .split("#[cfg(test)]")
6353 .next()
6354 .expect("the implementation section is always present");
6355 assert!(
6356 source.contains("colors.default.color.alpha(0.5)")
6357 && source.contains("colors.surface.background.alpha(0.4)"),
6358 "the row hover must read `bg-default/50` on secondary and \
6359 `bg-surface/40` on primary"
6360 );
6361 }
6362
6363 #[test]
6368 fn the_selected_row_fills_surface_ten_percent_and_outranks_the_hover() {
6369 let source = include_str!("table.rs")
6372 .split("#[cfg(test)]")
6373 .next()
6374 .expect("the implementation section is always present");
6375 assert!(
6376 source.contains(
6377 "let selected_bg = is_selected.then(|| colors.surface.background.alpha(0.1));"
6378 ),
6379 "the selected row must fill `bg-surface/10` \
6380 (pinned `.table__row[data-selected] .table__cell`)"
6381 );
6382 assert!(
6383 source.contains(".when_some(selected_bg, |cell, bg| cell.bg(bg))")
6384 && source.contains("cell.group_hover(hover_group.clone(), move |s| s.bg(bg))"),
6385 "selected and hover fills must be painted on each cell, with the \
6386 selected row omitting the hover group so HeroUI's selected rule wins"
6387 );
6388 assert!(
6389 !source.contains("accent.soft()"),
6390 "the selected row must not paint a role soft wash"
6391 );
6392 }
6393
6394 #[test]
6395 fn table_focus_ring_is_split_per_cell() {
6396 let source = include_str!("table.rs")
6397 .split("#[cfg(test)]")
6398 .next()
6399 .expect("the implementation section is always present");
6400 assert!(
6401 source.contains("fn table_cell_focus_ring(cx: &App, first: bool, last: bool)")
6402 && source.contains(".top_0()")
6403 && source.contains(".bottom_0()")
6404 && source.contains("cell_el = cell_el.child(table_cell_focus_ring("),
6405 "focused rows must paint clipped edge strips on each cell"
6406 );
6407 assert!(
6408 !source.contains("r.child(crate::util::inset_focus_ring(cx))"),
6409 "the row must not use one rounded overlay that bleeds across cells"
6410 );
6411 }
6412
6413 #[test]
6414 fn table_header_uses_medium_weight() {
6415 let source = include_str!("table.rs")
6416 .split("#[cfg(test)]")
6417 .next()
6418 .expect("the implementation section is always present");
6419 assert!(
6420 source.contains(".text_size(px(12.))")
6421 && source.contains(".line_height(px(16.))")
6422 && source.contains(".font_weight(gpui::FontWeight::MEDIUM)"),
6423 "table column headers must use the pinned `font-medium` weight"
6424 );
6425 }
6426
6427 #[test]
6428 fn table_header_preserves_caller_label_case() {
6429 let source = include_str!("table.rs")
6430 .split("#[cfg(test)]")
6431 .next()
6432 .expect("the implementation section is always present");
6433 assert!(
6434 source.contains(".child(column.label.clone());"),
6435 "table column headers must render the caller's label without uppercasing it"
6436 );
6437 }
6438
6439 #[test]
6440 fn table_header_and_rows_use_half_alpha_separator() {
6441 let source = include_str!("table.rs")
6442 .split("#[cfg(test)]")
6443 .next()
6444 .expect("the implementation section is always present");
6445 assert!(
6446 source.match_indices("colors.separator.alpha(0.5)").count() >= 2,
6447 "the header and row separators must use the separator token at 50% alpha"
6448 );
6449 }
6450
6451 #[test]
6452 fn table_last_row_omits_the_bottom_separator() {
6453 let source = include_str!("table.rs")
6454 .split("#[cfg(test)]")
6455 .next()
6456 .expect("the implementation section is always present");
6457 assert!(
6458 source.contains("if !self.secondary && i + 1 < self.row_keys.len() {")
6459 && source
6460 .contains("row = row.border_b_1().border_color(colors.separator.alpha(0.5));"),
6461 "the final visible Table row must not receive a bottom separator"
6462 );
6463 }
6464
6465 #[test]
6466 fn secondary_table_uses_surface_headers_and_cell_separators() {
6467 let source = include_str!("table.rs")
6468 .split("#[cfg(test)]")
6469 .next()
6470 .expect("the implementation section is always present");
6471 assert!(
6472 source.contains("cell = cell\n .bg(colors.surface_secondary)")
6473 && source.contains("colors.separator_tertiary().alpha(0.5)"),
6474 "secondary tables must paint the header surface and per-cell tertiary separators"
6475 );
6476 }
6477
6478 #[test]
6479 fn table_headers_keep_short_separators_and_rotate_builtin_sort_indicator() {
6480 let source = include_str!("table.rs")
6481 .split("#[cfg(test)]")
6482 .next()
6483 .expect("the implementation section is always present");
6484 assert!(
6485 source.contains(".h(px(16.))")
6486 && source.contains(".bg(colors.separator)")
6487 && source.contains("rotating_indicator_with_duration")
6488 && source.contains(".path(icons::CHEVRON_UP)"),
6489 "table headers must draw the pinned separator and use the shared 100ms rotation"
6490 );
6491 }
6492
6493 #[test]
6496 fn primary_row_fills_round_to_the_body_on_the_edges_they_touch() {
6497 let body = px(16.);
6498 let wrapper = px(24.);
6499 assert_eq!(
6500 edge_corner_rounding(false, false, true, true, body, wrapper),
6501 (Some(body), Some(body))
6502 );
6503 assert_eq!(
6505 edge_corner_rounding(false, true, true, true, body, wrapper),
6506 (Some(body), Some(body))
6507 );
6508 assert_eq!(
6510 edge_corner_rounding(false, false, false, true, body, wrapper),
6511 (None, Some(body))
6512 );
6513 assert_eq!(
6514 edge_corner_rounding(false, false, true, false, body, wrapper),
6515 (Some(body), None)
6516 );
6517 assert_eq!(
6518 edge_corner_rounding(false, false, false, false, body, wrapper),
6519 (None, None)
6520 );
6521 }
6522
6523 #[test]
6526 fn secondary_row_fills_round_only_to_the_wrapper_bottom() {
6527 let body = px(16.);
6528 let wrapper = px(24.);
6529 assert_eq!(
6530 edge_corner_rounding(true, false, true, true, body, wrapper),
6531 (None, Some(wrapper))
6532 );
6533 assert_eq!(
6535 edge_corner_rounding(true, true, true, true, body, wrapper),
6536 (None, None)
6537 );
6538 assert_eq!(
6539 edge_corner_rounding(true, false, true, false, body, wrapper),
6540 (None, None)
6541 );
6542 }
6543
6544 #[test]
6547 fn only_the_outer_cells_of_the_edge_rows_round_their_fill() {
6548 let source = include_str!("table.rs")
6549 .split("#[cfg(test)]")
6550 .next()
6551 .expect("the implementation section is always present");
6552 assert!(
6553 source.contains("let round_top = (i == 0).then_some(self.round_top).flatten();")
6554 && source.contains("let round_bottom = (i + 1 == self.row_keys.len())"),
6555 "only the first and last body rows may round their cell fills"
6556 );
6557 assert!(
6558 source.contains(".when(!self.selectable && c == 0, |cell| {")
6559 && source.contains(".when(c + 1 == data_count, |cell| {")
6560 && source.contains(".when_some(round_top, |cell, r| cell.rounded_tl(r))")
6561 && source.contains(".when_some(round_bottom, |cell, r| cell.rounded_br(r))"),
6562 "the row's leading and trailing cells must carry the box's corners"
6563 );
6564 assert!(
6565 source.contains("virtual_scroll_now.is_scrolled_to_top()")
6566 && source.contains("virtual_scroll_now.is_scrolled_to_end()")
6567 && source.contains("top.item_ix == 0 && top.offset_in_item <= px(0.5)"),
6568 "a virtual body must only round the edges it is scrolled to"
6569 );
6570 }
6571
6572 #[test]
6573 fn virtual_selectable_keys_are_not_materialized_on_every_render() {
6574 let source = include_str!("table.rs")
6575 .split("#[cfg(test)]")
6576 .next()
6577 .expect("the implementation section is always present");
6578 assert!(
6579 !source.contains("let selectable_collection_keys: Vec<SharedString> ="),
6580 "virtual Table must not allocate a selectable-key Vec for every render"
6581 );
6582 }
6583}
6584
6585crate::util::impl_component_styled!(Table);