1use std::cell::{Cell as StdCell, RefCell};
43use std::collections::{BTreeSet, HashMap};
44use std::ops::Range;
45use std::rc::Rc;
46
47use gpui::{
48 AnyElement, App, AppContext, ClickEvent, Entity, Focusable, Global, InteractiveElement,
49 IntoElement, ListSizingBehavior, MouseButton, ParentElement, RenderOnce, ScrollStrategy,
50 SharedString, StatefulInteractiveElement, Styled, UniformListScrollHandle, Window, div,
51 prelude::FluentBuilder, px, uniform_list,
52};
53use gpui_kit_assets::{Icon, icon};
54use gpui_kit_semantics::{NodeSpec, Role, Semantic};
55use gpui_kit_theme::{
56 ActiveTheme, ControlSize, Elevation, Radius, Space, Surface, TextTone, Theme, TypeScale,
57};
58
59use crate::controls::input::{Cancel, Submit, TextInput};
60use crate::data::table::{Align, Cell, ColumnWidth, SortDirection};
61use crate::display::empty::{EmptyKind, EmptyState};
62use crate::foundation::direction::{ActiveDirection, DirectionalExt};
63use crate::foundation::{Disableable, FocusRing, Ident, Pressable, Sizable, StyledExt, text};
64use crate::interaction::dnd::{
65 self, DragItem, DropAxis, DropIntent, DropPosition, RowTarget, SurfaceDrag,
66};
67use crate::layout::measure;
68use crate::motion::{Flipping, Presence, entrance, flip, state_change};
69use crate::strings::{ActiveStrings, StringKey};
70
71const RESIZE_HANDLE: f32 = 7.0;
73
74const GUTTER: f32 = 28.0;
78
79const MARK: f32 = 14.0;
81
82type RenderRow = Rc<dyn Fn(usize, &mut Window, &mut App) -> GridRow>;
83type RenderDetail = Rc<dyn Fn(SharedString, &mut Window, &mut App) -> AnyElement>;
84type SortHandler = Rc<dyn Fn(SharedString, SortDirection, &mut Window, &mut App)>;
85type SelectHandler = Rc<dyn Fn(&SelectionChange, &mut Window, &mut App)>;
86type ResizeHandler = Rc<dyn Fn(SharedString, f32, &mut Window, &mut App)>;
87type FitHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
88type ReorderHandler = Rc<dyn Fn(&DropIntent, &mut Window, &mut App)>;
89type ExpandHandler = Rc<dyn Fn(SharedString, bool, &mut Window, &mut App)>;
90type EditRequestHandler = Rc<dyn Fn(SharedString, SharedString, &mut Window, &mut App)>;
91type EditHandler = Rc<dyn Fn(&EditIntent, &mut Window, &mut App)>;
92
93#[derive(Debug, Clone)]
99pub struct GridColumn {
100 key: SharedString,
101 header: SharedString,
102 width: ColumnWidth,
103 min_width: f32,
104 align: Align,
105 sortable: bool,
106 resizable: bool,
107 reorderable: bool,
108 pinned: bool,
109 editable: bool,
110}
111
112impl GridColumn {
113 pub fn new(key: impl Into<SharedString>, header: impl Into<SharedString>) -> Self {
114 Self {
115 key: key.into(),
116 header: header.into(),
117 width: ColumnWidth::Flex(1.0),
118 min_width: 48.0,
119 align: Align::default(),
120 sortable: false,
121 resizable: false,
122 reorderable: false,
123 pinned: false,
124 editable: false,
125 }
126 }
127
128 pub fn width(mut self, width: ColumnWidth) -> Self {
129 self.width = width;
130 self
131 }
132
133 pub fn fixed(self, width: f32) -> Self {
134 self.width(ColumnWidth::Fixed(width))
135 }
136
137 pub fn flex(self, share: f32) -> Self {
138 self.width(ColumnWidth::Flex(share))
139 }
140
141 pub fn min_width(mut self, min_width: f32) -> Self {
144 self.min_width = min_width.max(0.0);
145 self
146 }
147
148 pub fn align(mut self, align: Align) -> Self {
149 self.align = align;
150 self
151 }
152
153 pub fn sortable(mut self, sortable: bool) -> Self {
154 self.sortable = sortable;
155 self
156 }
157
158 pub fn resizable(mut self, resizable: bool) -> Self {
161 self.resizable = resizable;
162 self
163 }
164
165 pub fn reorderable(mut self, reorderable: bool) -> Self {
168 self.reorderable = reorderable;
169 self
170 }
171
172 pub fn pinned(mut self, pinned: bool) -> Self {
176 self.pinned = pinned;
177 self
178 }
179
180 pub fn editable(mut self, editable: bool) -> Self {
183 self.editable = editable;
184 self
185 }
186
187 pub fn key(&self) -> &SharedString {
188 &self.key
189 }
190}
191
192pub struct GridRow {
194 id: SharedString,
195 text: Option<SharedString>,
196 disabled: bool,
197 cells: Vec<(SharedString, Cell)>,
198 hierarchy: Option<HierarchyRow>,
199}
200
201#[derive(Debug, Clone)]
202pub(crate) struct HierarchyRow {
203 pub level: u32,
204 pub has_children: bool,
205 pub expanded: bool,
206 pub parent: Option<SharedString>,
207}
208
209impl std::fmt::Debug for GridRow {
210 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 formatter
212 .debug_struct("GridRow")
213 .field("id", &self.id)
214 .field("cells", &self.cells.len())
215 .field("disabled", &self.disabled)
216 .finish()
217 }
218}
219
220impl GridRow {
221 pub fn new(id: impl Into<SharedString>) -> Self {
222 Self {
223 id: id.into(),
224 text: None,
225 disabled: false,
226 cells: Vec::new(),
227 hierarchy: None,
228 }
229 }
230
231 pub fn cell(mut self, key: impl Into<SharedString>, cell: impl Into<Cell>) -> Self {
232 self.cells.push((key.into(), cell.into()));
233 self
234 }
235
236 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
239 self.text = Some(text.into());
240 self
241 }
242
243 pub fn disabled(mut self, disabled: bool) -> Self {
244 self.disabled = disabled;
245 self
246 }
247
248 pub fn id(&self) -> &SharedString {
249 &self.id
250 }
251
252 pub(crate) fn hierarchy(mut self, hierarchy: HierarchyRow) -> Self {
253 self.hierarchy = Some(hierarchy);
254 self
255 }
256
257 fn take(&mut self, key: &SharedString) -> Option<Cell> {
258 let position = self.cells.iter().position(|(name, _)| name == key)?;
259 Some(self.cells.remove(position).1)
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
265pub enum SelectionMode {
266 #[default]
267 None,
268 Single,
269 Multiple,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum SelectionChange {
280 Replace(SharedString),
282 Toggle(SharedString),
284 Range {
287 anchor: SharedString,
288 to: SharedString,
289 },
290 Loaded,
293 Everything,
296 Clear,
297}
298
299impl SelectionChange {
300 pub fn as_str(&self) -> &'static str {
301 match self {
302 Self::Replace(_) => "replace",
303 Self::Toggle(_) => "toggle",
304 Self::Range { .. } => "range",
305 Self::Loaded => "loaded",
306 Self::Everything => "everything",
307 Self::Clear => "clear",
308 }
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct Expanded {
320 pub id: SharedString,
321 pub index: usize,
322}
323
324impl Expanded {
325 pub fn new(id: impl Into<SharedString>, index: usize) -> Self {
326 Self {
327 id: id.into(),
328 index,
329 }
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum EditOutcome {
336 Commit,
338 Revert,
340}
341
342impl EditOutcome {
343 pub fn as_str(self) -> &'static str {
344 match self {
345 Self::Commit => "commit",
346 Self::Revert => "revert",
347 }
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
353pub struct EditIntent {
354 pub row: SharedString,
355 pub column: SharedString,
356 pub value: SharedString,
359 pub outcome: EditOutcome,
360 pub next: Option<(SharedString, SharedString)>,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct EditingCell {
367 pub row: SharedString,
368 pub column: SharedString,
369 pub value: SharedString,
370}
371
372impl EditingCell {
373 pub fn new(
374 row: impl Into<SharedString>,
375 column: impl Into<SharedString>,
376 value: impl Into<SharedString>,
377 ) -> Self {
378 Self {
379 row: row.into(),
380 column: column.into(),
381 value: value.into(),
382 }
383 }
384
385 fn target(&self) -> (SharedString, SharedString) {
386 (self.row.clone(), self.column.clone())
387 }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
398pub enum GridLines {
399 #[default]
400 None,
401 Rows,
402}
403
404#[derive(IntoElement)]
406pub struct DataGrid {
407 ident: Ident,
408 count: usize,
409 total: Option<usize>,
410 render_row: RenderRow,
411 render_detail: Option<RenderDetail>,
412 columns: Vec<GridColumn>,
413 lines: GridLines,
414 sort: Option<(SharedString, SortDirection)>,
415 selection_mode: SelectionMode,
416 selected: BTreeSet<SharedString>,
417 expanded: Vec<Expanded>,
418 detail_rows: usize,
419 editing: Option<EditingCell>,
420 row_height: Option<f32>,
421 visible_rows: Option<usize>,
422 size: ControlSize,
423 disabled: bool,
424 loading: bool,
425 failure: Option<SharedString>,
426 empty: Option<EmptyState>,
427 on_sort: Option<SortHandler>,
428 on_select: Option<SelectHandler>,
429 on_resize: Option<ResizeHandler>,
430 on_fit: Option<FitHandler>,
431 on_reorder: Option<ReorderHandler>,
432 on_expand: Option<ExpandHandler>,
433 on_edit_request: Option<EditRequestHandler>,
434 on_edit: Option<EditHandler>,
435 hierarchy: bool,
436}
437
438impl std::fmt::Debug for DataGrid {
439 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 formatter
441 .debug_struct("DataGrid")
442 .field("ident", &self.ident)
443 .field("count", &self.count)
444 .field("total", &self.total)
445 .field("columns", &self.columns.len())
446 .field("sort", &self.sort)
447 .field("selection_mode", &self.selection_mode)
448 .field("selected", &self.selected.len())
449 .field("expanded", &self.expanded.len())
450 .field("editing", &self.editing)
451 .field("disabled", &self.disabled)
452 .finish()
453 }
454}
455
456impl DataGrid {
457 pub fn new(
461 ident: impl Into<Ident>,
462 count: usize,
463 render_row: impl Fn(usize, &mut Window, &mut App) -> GridRow + 'static,
464 ) -> Self {
465 Self {
466 ident: ident.into(),
467 count,
468 total: None,
469 render_row: Rc::new(render_row),
470 render_detail: None,
471 columns: Vec::new(),
472 lines: GridLines::default(),
473 sort: None,
474 selection_mode: SelectionMode::None,
475 selected: BTreeSet::new(),
476 expanded: Vec::new(),
477 detail_rows: 2,
478 editing: None,
479 row_height: None,
480 visible_rows: None,
481 size: ControlSize::Md,
482 disabled: false,
483 loading: false,
484 failure: None,
485 empty: None,
486 on_sort: None,
487 on_select: None,
488 on_resize: None,
489 on_fit: None,
490 on_reorder: None,
491 on_expand: None,
492 on_edit_request: None,
493 on_edit: None,
494 hierarchy: false,
495 }
496 }
497
498 pub fn total(mut self, total: usize) -> Self {
501 self.total = Some(total);
502 self
503 }
504
505 pub fn column(mut self, column: GridColumn) -> Self {
506 self.columns.push(column);
507 self
508 }
509
510 pub fn columns(mut self, columns: impl IntoIterator<Item = GridColumn>) -> Self {
511 self.columns.extend(columns);
512 self
513 }
514
515 pub fn lines(mut self, lines: GridLines) -> Self {
517 self.lines = lines;
518 self
519 }
520
521 pub fn sort(mut self, sort: Option<(SharedString, SortDirection)>) -> Self {
523 self.sort = sort;
524 self
525 }
526
527 pub fn sorted_by(self, key: impl Into<SharedString>, direction: SortDirection) -> Self {
528 self.sort(Some((key.into(), direction)))
529 }
530
531 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
532 self.selection_mode = mode;
533 self
534 }
535
536 pub fn selected(mut self, ids: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
539 self.selected = ids.into_iter().map(Into::into).collect();
540 self
541 }
542
543 pub fn expanded(mut self, rows: impl IntoIterator<Item = Expanded>) -> Self {
545 self.expanded = rows.into_iter().collect();
546 self.expanded.sort_by_key(|row| row.index);
547 self
548 }
549
550 pub fn detail_rows(mut self, rows: usize) -> Self {
552 self.detail_rows = rows.max(1);
553 self
554 }
555
556 pub fn detail(
559 mut self,
560 render: impl Fn(SharedString, &mut Window, &mut App) -> AnyElement + 'static,
561 ) -> Self {
562 self.render_detail = Some(Rc::new(render));
563 self
564 }
565
566 pub fn editing(mut self, cell: Option<EditingCell>) -> Self {
568 self.editing = cell;
569 self
570 }
571
572 pub fn row_height(mut self, height: f32) -> Self {
573 self.row_height = Some(height);
574 self
575 }
576
577 pub fn visible_rows(mut self, rows: usize) -> Self {
580 self.visible_rows = Some(rows);
581 self
582 }
583
584 pub fn loading(mut self, loading: bool) -> Self {
587 self.loading = loading;
588 self
589 }
590
591 pub fn failure(mut self, failure: impl Into<SharedString>) -> Self {
595 self.failure = Some(failure.into());
596 self
597 }
598
599 pub fn empty(mut self, empty: EmptyState) -> Self {
601 self.empty = Some(empty);
602 self
603 }
604
605 pub fn on_sort(
606 mut self,
607 handler: impl Fn(SharedString, SortDirection, &mut Window, &mut App) + 'static,
608 ) -> Self {
609 self.on_sort = Some(Rc::new(handler));
610 self
611 }
612
613 pub fn on_select(
614 mut self,
615 handler: impl Fn(&SelectionChange, &mut Window, &mut App) + 'static,
616 ) -> Self {
617 self.on_select = Some(Rc::new(handler));
618 self
619 }
620
621 pub fn on_resize(
623 mut self,
624 handler: impl Fn(SharedString, f32, &mut Window, &mut App) + 'static,
625 ) -> Self {
626 self.on_resize = Some(Rc::new(handler));
627 self
628 }
629
630 pub fn on_fit(
633 mut self,
634 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
635 ) -> Self {
636 self.on_fit = Some(Rc::new(handler));
637 self
638 }
639
640 pub fn on_reorder(
642 mut self,
643 handler: impl Fn(&DropIntent, &mut Window, &mut App) + 'static,
644 ) -> Self {
645 self.on_reorder = Some(Rc::new(handler));
646 self
647 }
648
649 pub fn on_expand(
651 mut self,
652 handler: impl Fn(SharedString, bool, &mut Window, &mut App) + 'static,
653 ) -> Self {
654 self.on_expand = Some(Rc::new(handler));
655 self
656 }
657
658 pub fn on_edit_request(
660 mut self,
661 handler: impl Fn(SharedString, SharedString, &mut Window, &mut App) + 'static,
662 ) -> Self {
663 self.on_edit_request = Some(Rc::new(handler));
664 self
665 }
666
667 pub fn on_edit(
669 mut self,
670 handler: impl Fn(&EditIntent, &mut Window, &mut App) + 'static,
671 ) -> Self {
672 self.on_edit = Some(Rc::new(handler));
673 self
674 }
675
676 fn total_rows(&self) -> usize {
679 self.total.unwrap_or(self.count).max(self.count)
680 }
681
682 fn ordered_columns(&self) -> Vec<&GridColumn> {
684 let mut ordered: Vec<&GridColumn> = self.columns.iter().filter(|c| c.pinned).collect();
685 ordered.extend(self.columns.iter().filter(|c| !c.pinned));
686 ordered
687 }
688
689 fn expanded_indices(&self) -> Vec<usize> {
690 self.expanded.iter().map(|row| row.index).collect()
691 }
692
693 pub(crate) fn hierarchy_mode(mut self) -> Self {
694 self.hierarchy = true;
695 self
696 }
697}
698
699impl Disableable for DataGrid {
700 fn disabled(mut self, disabled: bool) -> Self {
701 self.disabled = disabled;
702 self
703 }
704}
705
706impl Sizable for DataGrid {
707 fn control_size(mut self, size: ControlSize) -> Self {
708 self.size = size;
709 self
710 }
711}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
722pub(crate) enum Slot {
723 Row(usize),
724 Detail,
726}
727
728pub(crate) fn slot_count(count: usize, expanded: &[usize], detail_rows: usize) -> usize {
729 count + expanded.iter().filter(|index| **index < count).count() * detail_rows
730}
731
732pub(crate) fn slot_at(slot: usize, expanded: &[usize], detail_rows: usize) -> Slot {
734 let mut consumed = 0;
735 for index in expanded {
736 let opened = index + consumed;
737 if slot < opened {
738 break;
739 }
740 if slot == opened {
741 return Slot::Row(*index);
742 }
743 if slot <= opened + detail_rows {
744 return Slot::Detail;
745 }
746 consumed += detail_rows;
747 }
748 Slot::Row(slot - consumed)
749}
750
751pub(crate) fn slot_of(index: usize, expanded: &[usize], detail_rows: usize) -> usize {
753 index + expanded.iter().filter(|opened| **opened < index).count() * detail_rows
754}
755
756#[derive(Default)]
766struct Memory {
767 scroll: UniformListScrollHandle,
768 anchor: RefCell<Option<SharedString>>,
771 resizing: RefCell<Option<SharedString>>,
773 editor: RefCell<Option<Entity<TextInput>>>,
774 edit_target: RefCell<Option<(SharedString, SharedString)>>,
777 bulk: RefCell<Option<Presence>>,
780}
781
782#[derive(Default)]
783struct Memories(RefCell<HashMap<SharedString, Rc<Memory>>>);
784
785impl Global for Memories {}
786
787fn memory(id: &SharedString, cx: &mut App) -> Rc<Memory> {
788 if !cx.has_global::<Memories>() {
789 cx.set_global(Memories::default());
790 }
791 let mut memories = cx.global::<Memories>().0.borrow_mut();
792 Rc::clone(memories.entry(id.clone()).or_default())
793}
794
795type Drawn = Rc<RefCell<HashMap<usize, SharedString>>>;
797
798impl RenderOnce for DataGrid {
801 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
802 let theme = cx.theme().clone();
803 let metrics = theme.control.get(self.size);
804 let row_height = self.row_height.unwrap_or(metrics.height);
805 let ident = self.ident.clone();
806 let state = memory(&ident.semantic_id(), cx);
807 let columns: Vec<GridColumn> = self.ordered_columns().into_iter().cloned().collect();
808 let expanded = self.expanded_indices();
809 let detail_rows = self.detail_rows;
810 let slots = slot_count(self.count, &expanded, detail_rows);
811 let drawn: Drawn = Rc::new(RefCell::new(HashMap::new()));
812 let reorder = self.reorder(window, cx);
813 let editor = self.editor(&state, window, cx);
814
815 let header = self.header(&theme, row_height, &columns, reorder.as_ref(), window, cx);
816 let vacancy = self.empty.take();
817 let body = self.body(
818 &theme,
819 row_height,
820 &columns,
821 &expanded,
822 slots,
823 &state,
824 &drawn,
825 editor.clone(),
826 vacancy,
827 cx,
828 );
829
830 let mut frame = div()
831 .id(ident.element_id())
832 .column()
833 .w_full()
834 .radius(&theme, Radius::Card)
835 .frame(&theme, Surface::Panel, Elevation::Raised)
836 .overflow_hidden()
837 .children(self.banner(&theme, cx))
838 .child(header)
839 .child(body);
840
841 frame = self.wire_resize_drag(frame, &state, &columns, cx);
842 frame = self.wire_keyboard(frame, &state, &drawn, &expanded);
843
844 frame.semantic_in(
845 cx,
846 NodeSpec::new(
847 ident.semantic_id(),
848 if self.hierarchy {
849 Role::TreeGrid
850 } else {
851 Role::Table
852 },
853 )
854 .value(self.count.to_string()),
855 )
856 }
857}
858
859impl DataGrid {
860 fn banner(&self, theme: &Theme, cx: &mut App) -> Option<AnyElement> {
862 let failure = self.failure.clone()?;
863 if self.count == 0 {
864 return None;
865 }
866 let ident = self.ident.child("failure");
867 Some(
868 div()
869 .row()
870 .w_full()
871 .gap_token(theme, Space::Xs)
872 .px_token(theme, Space::Sm)
873 .py_token(theme, Space::Xs)
874 .bg(theme
875 .colors
876 .danger
877 .opacity(theme.effects.selected_ring_alpha))
878 .child(
879 icon(Icon::Danger)
880 .size(px(theme.control.sm.icon_size))
881 .text_color(theme.colors.danger),
882 )
883 .child(text(theme, TypeScale::Body, failure.clone()))
884 .semantic_in(
885 cx,
886 NodeSpec::new(ident.semantic_id(), Role::Status)
887 .parent(self.ident.semantic_id())
888 .text(failure)
889 .value("stale")
890 .invalid(true),
891 )
892 .into_any_element(),
893 )
894 }
895
896 fn reorder(&self, window: &mut Window, cx: &mut App) -> Option<Reorder> {
897 if self.disabled || !self.columns.iter().any(|column| column.reorderable) {
898 return None;
899 }
900 let on_drop = self.on_reorder.clone()?;
901 let surface = self.ident.child("header").semantic_id();
902 let pinned: Vec<SharedString> = self
903 .columns
904 .iter()
905 .filter(|column| column.pinned)
906 .map(|column| column.key.clone())
907 .collect();
908 let own = surface.clone();
909 Some(Reorder {
910 drag: dnd::surface_drag(&surface, window, cx),
911 surface,
912 accepts: Rc::new(move |item: &DragItem, position: &DropPosition| {
915 item.source == own && !pinned.contains(position.anchor())
916 }),
917 on_drop,
918 })
919 }
920
921 fn editor(
924 &self,
925 state: &Rc<Memory>,
926 window: &mut Window,
927 cx: &mut App,
928 ) -> Option<Entity<TextInput>> {
929 let cell = self.editing.clone()?;
930 if self.disabled {
931 return None;
932 }
933 let existing = state.editor.borrow().clone();
934 let input = match existing {
935 Some(input) => input,
936 None => {
937 let ident = self.ident.child("edit");
938 let input = cx.new(|cx| TextInput::new(ident, window, cx).bare(true));
939 *state.editor.borrow_mut() = Some(input.clone());
940 input
941 }
942 };
943
944 let target = cell.target();
945 let changed = state.edit_target.borrow().as_ref() != Some(&target);
946 if changed {
947 *state.edit_target.borrow_mut() = Some(target);
948 input.update(cx, |field, cx| {
949 field.set_text_quietly(cell.value.clone(), cx);
950 });
951 let handle = input.focus_handle(cx);
952 window.focus(&handle, cx);
953 }
954 Some(input)
955 }
956}
957
958type Accepts = Rc<dyn Fn(&DragItem, &DropPosition) -> bool>;
960
961type MeasuredEdge = (Rc<StdCell<gpui::Bounds<gpui::Pixels>>>, f32);
964
965#[derive(Clone)]
967struct Reorder {
968 surface: SharedString,
969 drag: Option<SurfaceDrag>,
970 accepts: Accepts,
971 on_drop: ReorderHandler,
972}
973
974impl DataGrid {
975 #[allow(clippy::too_many_arguments)]
976 fn header(
977 &self,
978 theme: &Theme,
979 height: f32,
980 columns: &[GridColumn],
981 reorder: Option<&Reorder>,
982 window: &mut Window,
983 cx: &mut App,
984 ) -> AnyElement {
985 let mut header = div()
986 .row()
987 .w_full()
988 .h(px(height))
989 .flex_none()
990 .px_token(theme, Space::Sm)
991 .gap_token(theme, Space::Sm)
992 .surface(theme, Surface::Raised)
993 .when(self.hierarchy, |header| {
994 header.row_reading(cx.layout_direction())
995 });
996
997 if let Some(box_element) = self.select_all(theme, cx) {
998 header = header.child(box_element);
999 }
1000 if self.on_expand.is_some() {
1001 header = header.child(div().w(px(GUTTER)).flex_none());
1002 }
1003
1004 let pinned = columns.iter().filter(|column| column.pinned).count();
1005 for (index, column) in columns.iter().enumerate() {
1006 header =
1007 header.child(self.header_cell(theme, height, column, index, reorder, window, cx));
1008 if index + 1 == pinned {
1009 header = header.child(pinned_edge(theme));
1010 }
1011 }
1012
1013 header.into_any_element()
1014 }
1015
1016 fn select_all(&self, theme: &Theme, cx: &mut App) -> Option<AnyElement> {
1018 if self.selection_mode != SelectionMode::Multiple {
1019 return None;
1020 }
1021 let ident = self.ident.child("select-all");
1022 let loaded = self.count;
1023 let chosen = self.selected.len();
1024 let total = self.total_rows();
1025 let all = loaded > 0 && chosen >= loaded;
1026 let mixed = chosen > 0 && chosen < loaded;
1027 let actionable = !self.disabled && self.on_select.is_some() && loaded > 0;
1028
1029 let mark = div()
1030 .size(px(MARK))
1031 .flex()
1032 .items_center()
1033 .justify_center()
1034 .flex_none()
1035 .radius(theme, Radius::Small)
1036 .border(px(theme.borders.hairline))
1037 .border_color(if all || mixed {
1038 theme.colors.accent
1039 } else {
1040 theme.colors.hairline_strong
1041 })
1042 .when(all || mixed, |element| element.bg(theme.colors.accent))
1043 .when(all, |element| {
1044 element.child(
1045 icon(Icon::Check)
1046 .size(px(MARK * 0.7))
1047 .text_color(theme.colors.text_on_accent),
1048 )
1049 })
1050 .when(mixed, |element| {
1051 element.child(
1052 div()
1053 .w(px(MARK * 0.5))
1054 .h(px(theme.borders.thick))
1055 .bg(theme.colors.text_on_accent),
1056 )
1057 });
1058
1059 let mut element = div()
1060 .id(ident.element_id())
1061 .w(px(GUTTER))
1062 .h_full()
1063 .flex_none()
1064 .flex()
1065 .items_center()
1066 .when(actionable, |element| {
1067 element.cursor_pointer().tab_index(0).focus_ring(theme)
1068 })
1069 .child(mark);
1070
1071 if let (true, Some(handler)) = (actionable, self.on_select.clone()) {
1072 let next = if all {
1073 SelectionChange::Clear
1074 } else {
1075 SelectionChange::Loaded
1076 };
1077 element = element.on_click(move |_, window, cx| handler(&next, window, cx));
1078 }
1079
1080 Some(
1081 element
1082 .semantic_in(
1083 cx,
1084 NodeSpec::new(ident.semantic_id(), Role::Checkbox)
1085 .parent(self.ident.semantic_id())
1086 .text(cx.strings().text(StringKey::GridSelectAllLoaded))
1087 .disabled(!actionable)
1088 .tristate(if mixed { None } else { Some(all) })
1089 .value(format!("{chosen} of {loaded} loaded, {total} total")),
1094 )
1095 .into_any_element(),
1096 )
1097 }
1098
1099 #[allow(clippy::too_many_arguments)]
1100 fn header_cell(
1101 &self,
1102 theme: &Theme,
1103 height: f32,
1104 column: &GridColumn,
1105 index: usize,
1106 reorder: Option<&Reorder>,
1107 window: &mut Window,
1108 cx: &mut App,
1109 ) -> AnyElement {
1110 let ident = self.ident.child("header").child(column.key.as_ref());
1111 let hover_group = ident.child("hover").semantic_id();
1112 let direction = self
1113 .sort
1114 .as_ref()
1115 .filter(|(key, _)| key == &column.key)
1116 .map(|(_, direction)| *direction);
1117 let sortable = column.sortable && !self.disabled && self.on_sort.is_some();
1118 let target = reorder.filter(|_| !column.pinned);
1122 let draggable = target.filter(|_| column.reorderable);
1123 let drag = target.and_then(|reorder| reorder.drag.as_ref());
1124 let carried = drag.is_some_and(|drag| drag.carries(&column.key));
1125 let landing = drag.and_then(|drag| drag.indicator_for(&column.key));
1126
1127 let measured = measure::cell(&ident.semantic_id(), cx);
1130
1131 let content = div()
1132 .row()
1133 .overflow_hidden()
1134 .gap_token(theme, Space::Xs)
1135 .child(
1136 text(theme, TypeScale::Label, column.header.clone())
1137 .text_tone(theme, TextTone::Muted)
1138 .when(sortable, |element| {
1139 element.group_hover(hover_group.clone(), |style| {
1140 style.text_color(theme.colors.text)
1141 })
1142 }),
1143 )
1144 .children(direction.map(|direction| {
1145 text(
1146 theme,
1147 TypeScale::Label,
1148 SharedString::from(match direction {
1149 SortDirection::Ascending => "↑",
1150 SortDirection::Descending => "↓",
1151 }),
1152 )
1153 }));
1154
1155 let mut cell = column_frame(div().id(ident.element_id()), column, theme)
1156 .group(hover_group)
1157 .relative()
1158 .when(carried, |element| element.opacity(theme.opacity.muted))
1159 .when(sortable, |element| {
1160 element
1161 .cursor_pointer()
1162 .tab_index(0)
1163 .pressable(cx)
1164 .focus_ring(theme)
1165 })
1166 .child(
1167 div()
1168 .flex_1()
1169 .overflow_hidden()
1170 .on_children_prepainted({
1171 let measured = Rc::clone(&measured);
1172 move |bounds, window, _| {
1173 if let Some(first) = bounds.first() {
1174 measure::record(&measured, *first, window);
1175 }
1176 }
1177 })
1178 .child(content),
1179 )
1180 .children(landing.map(|(position, accepted)| {
1181 dnd::indicator(&position, accepted, DropAxis::Horizontal, cx)
1182 }));
1183
1184 if let (true, Some(handler)) = (sortable, self.on_sort.clone()) {
1185 let key = column.key.clone();
1186 let next = direction.map_or(SortDirection::Ascending, SortDirection::reversed);
1187 let clicked = key.clone();
1188 let click = Rc::clone(&handler);
1189 cell = cell
1190 .on_click(move |_, window, cx| click(clicked.clone(), next, window, cx))
1191 .on_key_down(move |event, window, cx| {
1192 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
1193 handler(key.clone(), next, window, cx);
1194 cx.stop_propagation();
1195 }
1196 });
1197 }
1198
1199 if let Some(handle) = self.resize_handle(theme, height, column, &measured, cx) {
1200 cell = cell.child(handle);
1201 }
1202
1203 if let Some(reorder) = draggable {
1204 cell = dnd::draggable(
1205 cell,
1206 DragItem::new(
1207 reorder.surface.clone(),
1208 column.key.clone(),
1209 column.header.clone(),
1210 ),
1211 );
1212 }
1213
1214 if let Some(reorder) = target {
1215 cell = dnd::drop_target(
1216 cell,
1217 RowTarget {
1218 surface: reorder.surface.clone(),
1219 id: column.key.clone(),
1220 index,
1221 allow_into: false,
1222 axis: DropAxis::Horizontal,
1223 accepts: Rc::clone(&reorder.accepts),
1224 on_drop: Rc::clone(&reorder.on_drop),
1225 },
1226 );
1227 }
1228
1229 let spec = if column.sortable {
1230 NodeSpec::new(ident.semantic_id(), Role::Button)
1231 .parent(self.ident.semantic_id())
1232 .text(column.header.clone())
1233 .disabled(!sortable)
1234 .value(direction.map_or("unsorted", SortDirection::as_str))
1237 } else {
1238 NodeSpec::new(ident.semantic_id(), Role::Cell)
1239 .parent(self.ident.semantic_id())
1240 .text(column.header.clone())
1241 };
1242 let cell = cell.semantic_in(cx, spec);
1243
1244 let handle = flip(ident.child("slide").semantic_id(), cx);
1247 cell.flip(&handle, window, cx).into_any_element()
1248 }
1249
1250 fn resize_handle(
1252 &self,
1253 theme: &Theme,
1254 height: f32,
1255 column: &GridColumn,
1256 measured: &Rc<StdCell<gpui::Bounds<gpui::Pixels>>>,
1257 cx: &mut App,
1258 ) -> Option<AnyElement> {
1259 if self.disabled || !column.resizable {
1260 return None;
1261 }
1262 let ident = self
1263 .ident
1264 .child("header")
1265 .child(column.key.as_ref())
1266 .child("resize");
1267 let state = memory(&self.ident.semantic_id(), cx);
1268
1269 let mut handle = div()
1270 .id(ident.element_id())
1271 .absolute()
1272 .top_0()
1273 .right(px(-RESIZE_HANDLE / 2.0))
1274 .w(px(RESIZE_HANDLE))
1275 .h(px(height))
1276 .flex()
1277 .items_center()
1278 .justify_center()
1279 .cursor_pointer()
1280 .child(
1281 div()
1282 .w(px(theme.borders.hairline))
1283 .h_full()
1284 .bg(theme.colors.hairline),
1285 )
1286 .hover(|style| style.bg(theme.colors.hover));
1287
1288 if self.on_resize.is_some() {
1289 let key = column.key.clone();
1290 let started = Rc::clone(&state);
1291 handle = handle.on_mouse_down(MouseButton::Left, move |_, _, _| {
1292 *started.resizing.borrow_mut() = Some(key.clone());
1293 });
1294 }
1295
1296 if let Some(fit) = self.on_fit.clone() {
1297 let key = column.key.clone();
1298 handle = handle.on_click(move |event: &ClickEvent, window, cx| {
1299 if event.click_count() < 2 {
1300 return;
1301 }
1302 fit(key.clone(), window, cx);
1303 cx.stop_propagation();
1304 });
1305 }
1306
1307 let width = f32::from(measured.get().size.width).max(column.min_width);
1308 Some(
1309 handle
1310 .semantic_in(
1311 cx,
1312 NodeSpec::new(ident.semantic_id(), Role::Separator)
1313 .parent(self.ident.semantic_id())
1314 .text(
1315 cx.strings()
1316 .format(StringKey::GridResizeColumn, &[&column.header]),
1317 )
1318 .value(format!("{width:.0}")),
1319 )
1320 .absolute()
1323 .into_any_element(),
1324 )
1325 }
1326
1327 fn wire_resize_drag(
1333 &self,
1334 frame: gpui::Stateful<gpui::Div>,
1335 state: &Rc<Memory>,
1336 columns: &[GridColumn],
1337 cx: &mut App,
1338 ) -> gpui::Stateful<gpui::Div> {
1339 let Some(handler) = self.on_resize.clone().filter(|_| !self.disabled) else {
1340 return frame;
1341 };
1342 let edges: HashMap<SharedString, MeasuredEdge> = columns
1343 .iter()
1344 .filter(|column| column.resizable)
1345 .map(|column| {
1346 let ident = self.ident.child("header").child(column.key.as_ref());
1347 (
1348 column.key.clone(),
1349 (measure::cell(&ident.semantic_id(), cx), column.min_width),
1350 )
1351 })
1352 .collect();
1353 if edges.is_empty() {
1354 return frame;
1355 }
1356
1357 let held = Rc::clone(state);
1358 let frame = frame.on_mouse_move(move |event, window, cx| {
1359 let key = held.resizing.borrow().clone();
1360 let Some(key) = key else {
1361 return;
1362 };
1363 if event.pressed_button != Some(MouseButton::Left) {
1364 *held.resizing.borrow_mut() = None;
1365 return;
1366 }
1367 let Some((bounds, min_width)) = edges.get(&key) else {
1368 return;
1369 };
1370 let left = f32::from(bounds.get().left());
1371 let width = (f32::from(event.position.x) - left).max(*min_width);
1372 handler(key, width, window, cx);
1373 });
1374
1375 let released = Rc::clone(state);
1376 frame.on_mouse_up(MouseButton::Left, move |_, _, _| {
1377 *released.resizing.borrow_mut() = None;
1378 })
1379 }
1380
1381 fn wire_keyboard(
1384 &self,
1385 frame: gpui::Stateful<gpui::Div>,
1386 state: &Rc<Memory>,
1387 drawn: &Drawn,
1388 expanded: &[usize],
1389 ) -> gpui::Stateful<gpui::Div> {
1390 let Some(handler) = self
1391 .on_select
1392 .clone()
1393 .filter(|_| !self.disabled)
1394 .filter(|_| self.selection_mode != SelectionMode::None)
1395 .filter(|_| self.count > 0)
1396 else {
1397 return frame;
1398 };
1399 let render_row = Rc::clone(&self.render_row);
1400 let drawn = Rc::clone(drawn);
1401 let state = Rc::clone(state);
1402 let count = self.count;
1403 let expanded = expanded.to_vec();
1404 let detail_rows = self.detail_rows;
1405 let selected = self.selected.clone();
1406
1407 let hierarchy = self.hierarchy;
1408 let on_expand = self.on_expand.clone();
1409 frame.on_key_down(move |event, window, cx| {
1410 let anchor = state.anchor.borrow().clone();
1414 let from = current_index(&drawn, anchor.as_ref(), &selected);
1415 if hierarchy && let Some(index) = from {
1416 let row = render_row(index, window, cx);
1417 if let Some(meta) = row.hierarchy {
1418 if row.disabled {
1419 return;
1420 }
1421 let logical = cx
1422 .layout_direction()
1423 .arrow_step(event.keystroke.key.as_str());
1424 match logical {
1425 Some(-1) if meta.has_children && meta.expanded => {
1426 if let Some(expand) = &on_expand {
1427 expand(row.id, false, window, cx);
1428 cx.stop_propagation();
1429 }
1430 return;
1431 }
1432 Some(-1) => {
1433 if let Some(parent) = meta.parent {
1434 *state.anchor.borrow_mut() = Some(parent.clone());
1435 handler(&SelectionChange::Replace(parent), window, cx);
1436 cx.stop_propagation();
1437 }
1438 return;
1439 }
1440 Some(1) if meta.has_children && !meta.expanded => {
1441 if let Some(expand) = &on_expand {
1442 expand(row.id, true, window, cx);
1443 cx.stop_propagation();
1444 }
1445 return;
1446 }
1447 Some(1) if meta.has_children && meta.expanded => {
1448 if let Some((_, child)) = reachable(
1449 &render_row,
1450 index.saturating_add(1),
1451 1,
1452 count,
1453 window,
1454 cx,
1455 ) {
1456 *state.anchor.borrow_mut() = Some(child.clone());
1457 handler(&SelectionChange::Replace(child), window, cx);
1458 cx.stop_propagation();
1459 }
1460 return;
1461 }
1462 Some(1) => return,
1463 _ => {}
1464 }
1465 }
1466 }
1467 let Some(target) = target_index(event.keystroke.key.as_str(), from, count) else {
1468 return;
1469 };
1470 let step = if from.is_some_and(|from| target < from) {
1471 -1
1472 } else {
1473 1
1474 };
1475 let Some((index, id)) = reachable(&render_row, target, step, count, window, cx) else {
1476 return;
1477 };
1478 state.scroll.scroll_to_item(
1479 slot_of(index, &expanded, detail_rows),
1480 ScrollStrategy::Nearest,
1481 );
1482 window.refresh();
1483 *state.anchor.borrow_mut() = Some(id.clone());
1484 handler(&SelectionChange::Replace(id), window, cx);
1485 cx.stop_propagation();
1486 })
1487 }
1488
1489 #[allow(clippy::too_many_arguments)]
1490 fn body(
1491 &self,
1492 theme: &Theme,
1493 row_height: f32,
1494 columns: &[GridColumn],
1495 expanded: &[usize],
1496 slots: usize,
1497 state: &Rc<Memory>,
1498 drawn: &Drawn,
1499 editor: Option<Entity<TextInput>>,
1500 vacancy: Option<EmptyState>,
1501 cx: &mut App,
1502 ) -> AnyElement {
1503 if self.count == 0 {
1504 return self.vacant(theme, row_height, vacancy, cx);
1505 }
1506
1507 let ident = self.ident.clone();
1508 let theme = theme.clone();
1509 let columns = columns.to_vec();
1510 let expanded = expanded.to_vec();
1511 let detail_rows = self.detail_rows;
1512 let render_row = Rc::clone(&self.render_row);
1513 let render_detail = self.render_detail.clone();
1514 let drawn = Rc::clone(drawn);
1515 let opened: Vec<SharedString> = self.expanded.iter().map(|row| row.id.clone()).collect();
1516 let context = Rc::new(RowContext {
1517 lines: self.lines,
1518 selected: self.selected.clone(),
1519 selection_mode: self.selection_mode,
1520 disabled: self.disabled,
1521 on_select: self.on_select.clone(),
1522 on_expand: self.on_expand.clone(),
1523 on_edit_request: self.on_edit_request.clone(),
1524 on_edit: self.on_edit.clone(),
1525 editing: self.editing.clone(),
1526 editor,
1527 state: Rc::clone(state),
1528 hierarchy: self.hierarchy,
1529 });
1530
1531 let list = uniform_list(
1532 ident.child("rows").element_id(),
1533 slots,
1534 move |range: Range<usize>, window, cx| {
1535 drawn.borrow_mut().clear();
1536 range
1537 .map(|slot| match slot_at(slot, &expanded, detail_rows) {
1538 Slot::Detail => div().w_full().h(px(row_height)).into_any_element(),
1539 Slot::Row(index) => {
1540 let row = render_row(index, window, cx);
1541 drawn.borrow_mut().insert(index, row.id.clone());
1542 let open = opened.contains(&row.id);
1543 row_element(
1544 &ident,
1545 &theme,
1546 row_height,
1547 detail_rows,
1548 &columns,
1549 row,
1550 open,
1551 render_detail.as_ref(),
1552 &context,
1553 window,
1554 cx,
1555 )
1556 }
1557 })
1558 .collect::<Vec<_>>()
1559 },
1560 )
1561 .track_scroll(&state.scroll)
1562 .w_full()
1563 .with_sizing_behavior(if self.visible_rows.is_some() {
1564 ListSizingBehavior::Auto
1565 } else {
1566 ListSizingBehavior::Infer
1567 })
1568 .when_some(self.visible_rows, |element, rows| {
1569 element.h(px(row_height * rows as f32))
1570 });
1571
1572 list.into_any_element()
1573 }
1574
1575 fn vacant(
1577 &self,
1578 theme: &Theme,
1579 row_height: f32,
1580 vacancy: Option<EmptyState>,
1581 cx: &mut App,
1582 ) -> AnyElement {
1583 if self.loading {
1584 let ident = self.ident.child("loading");
1585 return div()
1586 .column()
1587 .w_full()
1588 .children((0..self.visible_rows.unwrap_or(4)).map(|index| {
1589 div()
1590 .id(ident.indexed_element_id(index))
1591 .w_full()
1592 .h(px(row_height))
1593 .px_token(theme, Space::Sm)
1594 .flex()
1595 .items_center()
1596 .child(
1597 div()
1598 .w_full()
1599 .h(px(theme.spacing.sm))
1600 .radius(theme, Radius::Small)
1601 .bg(theme.colors.hover),
1602 )
1603 }))
1604 .semantic_in(
1605 cx,
1606 NodeSpec::new(ident.semantic_id(), Role::Status)
1607 .parent(self.ident.semantic_id())
1608 .text(cx.strings().text(StringKey::GridLoadingRows))
1609 .value("loading")
1610 .busy(true),
1611 )
1612 .into_any_element();
1613 }
1614
1615 if let Some(failure) = self.failure.clone() {
1616 return EmptyState::new(
1617 self.ident.child("empty"),
1618 cx.strings().text(StringKey::GridLoadFailed),
1619 )
1620 .kind(EmptyKind::Failed)
1621 .detail(failure)
1622 .into_any_element();
1623 }
1624
1625 match vacancy {
1626 Some(empty) => empty.into_any_element(),
1627 None => EmptyState::new(
1628 self.ident.child("empty"),
1629 cx.strings().text(StringKey::GridEmpty),
1630 )
1631 .kind(EmptyKind::Empty)
1632 .into_any_element(),
1633 }
1634 }
1635}
1636
1637struct RowContext {
1639 lines: GridLines,
1640 selected: BTreeSet<SharedString>,
1641 selection_mode: SelectionMode,
1642 disabled: bool,
1643 on_select: Option<SelectHandler>,
1644 on_expand: Option<ExpandHandler>,
1645 on_edit_request: Option<EditRequestHandler>,
1646 on_edit: Option<EditHandler>,
1647 editing: Option<EditingCell>,
1648 editor: Option<Entity<TextInput>>,
1649 state: Rc<Memory>,
1650 hierarchy: bool,
1651}
1652
1653#[allow(clippy::too_many_arguments)]
1654fn row_element(
1655 grid: &Ident,
1656 theme: &Theme,
1657 height: f32,
1658 detail_rows: usize,
1659 columns: &[GridColumn],
1660 mut row: GridRow,
1661 open: bool,
1662 render_detail: Option<&RenderDetail>,
1663 context: &RowContext,
1664 window: &mut Window,
1665 cx: &mut App,
1666) -> AnyElement {
1667 let ident = grid.child(row.id.as_ref());
1668 let selected = context.selected.contains(&row.id);
1669 let selectable = !row.disabled
1670 && !context.disabled
1671 && context.selection_mode != SelectionMode::None
1672 && context.on_select.is_some();
1673
1674 let mut element = div()
1675 .id(ident.element_id())
1676 .relative()
1677 .row()
1678 .w_full()
1679 .h(px(height))
1680 .px_token(theme, Space::Sm)
1681 .gap_token(theme, Space::Sm)
1682 .when(context.lines == GridLines::Rows, |element| {
1683 element
1684 .border_b(px(theme.borders.hairline))
1685 .border_color(theme.colors.hairline)
1686 })
1687 .when(selected, |element| element.bg(theme.colors.selected))
1688 .when(row.disabled, |element| {
1689 element.opacity(theme.opacity.disabled)
1690 })
1691 .when(selectable, |element| {
1692 element
1693 .cursor_pointer()
1694 .tab_index(0)
1695 .pressable(cx)
1696 .when(!selected, |element| {
1697 element.hover(|style| style.bg(theme.colors.hover.opacity(0.3)))
1698 })
1699 .focus_ring(theme)
1700 })
1701 .when(context.hierarchy, |element| {
1702 element.row_reading(cx.layout_direction())
1703 });
1704
1705 if context.selection_mode == SelectionMode::Multiple {
1706 element = element.child(row_mark(theme, selected));
1707 }
1708
1709 if !context.hierarchy
1710 && let Some(expand) = context.on_expand.clone().filter(|_| !context.disabled)
1711 {
1712 element = element.child(disclosure(&ident, theme, &row, open, expand, cx));
1713 }
1714
1715 let pinned = columns.iter().filter(|column| column.pinned).count();
1716 for (position, column) in columns.iter().enumerate() {
1717 let next = columns
1722 .iter()
1723 .skip(position + 1)
1724 .find(|next| next.editable)
1725 .map(|next| (row.id.clone(), next.key.clone()));
1726 element = element.child(cell_element(
1727 &ident,
1728 theme,
1729 column,
1730 &mut row,
1731 next,
1732 position == 0,
1733 context,
1734 window,
1735 cx,
1736 ));
1737 if position + 1 == pinned {
1738 element = element.child(pinned_edge(theme));
1739 }
1740 }
1741
1742 if let (true, Some(handler)) = (selectable, context.on_select.clone()) {
1743 let id = row.id.clone();
1744 let anchor = Rc::clone(&context.state);
1745 let multiple = context.selection_mode == SelectionMode::Multiple;
1746 element = element.on_click(move |event: &ClickEvent, window, cx| {
1747 let modifiers = event.modifiers();
1748 let previous = anchor.anchor.borrow().clone();
1749 let change = match (
1750 multiple,
1751 modifiers.shift,
1752 modifiers.platform || modifiers.control,
1753 ) {
1754 (true, true, _) => match previous {
1755 Some(previous) => SelectionChange::Range {
1756 anchor: previous,
1757 to: id.clone(),
1758 },
1759 None => SelectionChange::Replace(id.clone()),
1760 },
1761 (true, _, true) => SelectionChange::Toggle(id.clone()),
1762 _ => SelectionChange::Replace(id.clone()),
1763 };
1764 if !matches!(change, SelectionChange::Range { .. }) {
1767 *anchor.anchor.borrow_mut() = Some(id.clone());
1768 }
1769 handler(&change, window, cx);
1770 });
1771 }
1772
1773 let mut spec = NodeSpec::new(ident.semantic_id(), Role::Row)
1774 .parent(grid.semantic_id())
1775 .selected(selected)
1776 .disabled(row.disabled || context.disabled);
1777 if context.on_expand.is_some() {
1778 spec = spec.expanded(open);
1779 }
1780 if let Some(meta) = &row.hierarchy {
1781 spec = spec.level(meta.level);
1782 if meta.has_children {
1783 spec = spec.expanded(meta.expanded);
1784 }
1785 }
1786 if let Some(text) = row.text.clone() {
1787 spec = spec.text(text);
1788 }
1789 let mut element = element.semantic_in(cx, spec);
1790
1791 if let (true, Some(render)) = (open, render_detail) {
1795 let detail = ident.child("detail");
1796 element = element.child(
1797 div()
1798 .absolute()
1799 .left_0()
1800 .right_0()
1801 .top(px(height))
1802 .h(px(height * detail_rows as f32))
1803 .overflow_hidden()
1804 .bg(theme.colors.panel)
1805 .p_token(theme, Space::Sm)
1806 .child(render(row.id.clone(), window, cx))
1807 .semantic_in(
1808 cx,
1809 NodeSpec::new(detail.semantic_id(), Role::Group)
1810 .parent(ident.semantic_id())
1811 .text(row.text.clone().unwrap_or_else(|| row.id.clone())),
1812 )
1813 .absolute(),
1816 );
1817 }
1818
1819 element.into_any_element()
1820}
1821
1822fn pinned_edge(theme: &Theme) -> gpui::Div {
1824 div()
1825 .w(px(theme.borders.hairline))
1826 .h_full()
1827 .flex_none()
1828 .bg(theme.colors.hairline_strong)
1829}
1830
1831fn row_mark(theme: &Theme, selected: bool) -> gpui::Div {
1834 div().w(px(GUTTER)).flex_none().flex().items_center().child(
1835 div()
1836 .size(px(MARK))
1837 .flex()
1838 .items_center()
1839 .justify_center()
1840 .flex_none()
1841 .radius(theme, Radius::Small)
1842 .border(px(theme.borders.hairline))
1843 .border_color(if selected {
1844 theme.colors.accent
1845 } else {
1846 theme.colors.hairline_strong
1847 })
1848 .when(selected, |element| {
1849 element.bg(theme.colors.accent).child(
1850 icon(Icon::Check)
1851 .size(px(MARK * 0.7))
1852 .text_color(theme.colors.text_on_accent),
1853 )
1854 }),
1855 )
1856}
1857
1858fn disclosure(
1859 ident: &Ident,
1860 theme: &Theme,
1861 row: &GridRow,
1862 open: bool,
1863 handler: ExpandHandler,
1864 cx: &mut App,
1865) -> AnyElement {
1866 let toggle = ident.child("expand");
1867 let id = row.id.clone();
1868 let next = !open;
1869 let key_handler = Rc::clone(&handler);
1870 let key_id = id.clone();
1871
1872 div()
1873 .id(toggle.element_id())
1874 .w(px(GUTTER))
1875 .h_full()
1876 .flex_none()
1877 .flex()
1878 .items_center()
1879 .justify_center()
1880 .cursor_pointer()
1881 .tab_index(0)
1882 .radius(theme, Radius::Small)
1883 .hover(|style| style.bg(theme.colors.hover))
1884 .focus_ring(theme)
1885 .child(
1886 icon(if open {
1887 Icon::AltArrowDown
1888 } else {
1889 Icon::AltArrowRight
1890 })
1891 .size(px(theme.control.sm.icon_size))
1892 .text_color(theme.colors.text_muted),
1893 )
1894 .on_click(move |_, window, cx| {
1895 handler(id.clone(), next, window, cx);
1896 cx.stop_propagation();
1897 })
1898 .on_key_down(move |event, window, cx| {
1899 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
1900 key_handler(key_id.clone(), next, window, cx);
1901 cx.stop_propagation();
1902 }
1903 })
1904 .semantic_in(
1905 cx,
1906 NodeSpec::new(toggle.semantic_id(), Role::Button)
1907 .parent(ident.semantic_id())
1908 .text(format!(
1909 "{} {}",
1910 cx.strings().text(if open {
1911 StringKey::Collapse
1912 } else {
1913 StringKey::Expand
1914 }),
1915 row.text.clone().unwrap_or_else(|| row.id.clone())
1916 ))
1917 .expanded(open),
1918 )
1919 .into_any_element()
1920}
1921
1922#[allow(clippy::too_many_arguments)]
1923fn cell_element(
1924 ident: &Ident,
1925 theme: &Theme,
1926 column: &GridColumn,
1927 row: &mut GridRow,
1928 next: Option<(SharedString, SharedString)>,
1929 logical_start: bool,
1930 context: &RowContext,
1931 _window: &mut Window,
1932 cx: &mut App,
1933) -> AnyElement {
1934 let cell = row.take(&column.key);
1935 let editing = context
1936 .editing
1937 .as_ref()
1938 .filter(|edit| edit.row == row.id && edit.column == column.key);
1939 let editable = column.editable && !row.disabled && !context.disabled;
1940
1941 if let (Some(edit), Some(field)) = (editing, context.editor.clone()) {
1942 return editor_cell(theme, column, row, edit, field, next, context);
1943 }
1944
1945 let published =
1948 context.hierarchy || cell.as_ref().is_some_and(|cell| cell.published) || editable;
1949 let text = cell.as_ref().and_then(|cell| cell.text.clone());
1950 let mut content = cell.map(|cell| cell.content.into_element(theme));
1951 if logical_start && context.hierarchy {
1952 let hierarchy = row.hierarchy.clone();
1953 let direction = cx.layout_direction();
1954 let mut leading = div()
1955 .row_reading(direction)
1956 .items_center()
1957 .min_w_0()
1958 .w_full();
1959 if let Some(meta) = hierarchy {
1960 leading = leading.ps(
1961 direction,
1962 px((meta.level.saturating_sub(1) as f32) * theme.spacing.md),
1963 );
1964 if meta.has_children {
1965 if let Some(expand) = context
1966 .on_expand
1967 .clone()
1968 .filter(|_| !context.disabled && !row.disabled)
1969 {
1970 leading =
1971 leading.child(disclosure(ident, theme, row, meta.expanded, expand, cx));
1972 }
1973 } else {
1974 leading = leading.child(div().w(px(GUTTER)).flex_none());
1975 }
1976 }
1977 leading = leading.children(content.take());
1978 content = Some(leading.into_any_element());
1979 }
1980
1981 if !published {
1982 return column_frame(div(), column, theme)
1983 .overflow_hidden()
1984 .children(content)
1985 .into_any_element();
1986 }
1987
1988 let cell_ident = ident.child(column.key.as_ref());
1989 let mut spec = NodeSpec::new(
1990 cell_ident.semantic_id(),
1991 if context.hierarchy {
1992 Role::GridCell
1993 } else {
1994 Role::Cell
1995 },
1996 )
1997 .parent(ident.semantic_id());
1998 if let Some(text) = text {
1999 spec = spec.text(text);
2000 }
2001
2002 let mut frame = column_frame(div().id(cell_ident.element_id()), column, theme)
2003 .overflow_hidden()
2004 .children(content);
2005
2006 if let (true, Some(request)) = (editable, context.on_edit_request.clone()) {
2007 let row_id = row.id.clone();
2008 let key = column.key.clone();
2009 let key_request = Rc::clone(&request);
2010 let key_row = row_id.clone();
2011 let key_key = key.clone();
2012 frame = frame
2013 .tab_index(0)
2014 .cursor_pointer()
2015 .focus_ring(theme)
2016 .on_click(move |event: &ClickEvent, window, cx| {
2017 if event.click_count() < 2 {
2018 return;
2019 }
2020 request(row_id.clone(), key.clone(), window, cx);
2021 cx.stop_propagation();
2022 })
2023 .on_key_down(move |event, window, cx| {
2024 if event.keystroke.key.as_str() == "enter" {
2025 key_request(key_row.clone(), key_key.clone(), window, cx);
2026 cx.stop_propagation();
2027 }
2028 });
2029 }
2030
2031 frame.semantic_in(cx, spec).into_any_element()
2032}
2033
2034#[allow(clippy::too_many_arguments)]
2036fn editor_cell(
2037 theme: &Theme,
2038 column: &GridColumn,
2039 row: &GridRow,
2040 edit: &EditingCell,
2041 field: Entity<TextInput>,
2042 next: Option<(SharedString, SharedString)>,
2043 context: &RowContext,
2044) -> AnyElement {
2045 let frame = column_frame(div(), column, theme)
2046 .overflow_hidden()
2047 .px(px(theme.spacing.xs))
2048 .radius(theme, Radius::Small)
2049 .well(theme)
2050 .shadow(theme.focus_ring());
2051
2052 let Some(handler) = context.on_edit.clone() else {
2053 return frame.child(field).into_any_element();
2054 };
2055
2056 let row_id = row.id.clone();
2057 let key = column.key.clone();
2058 let seed = edit.value.clone();
2059 let reading = field.clone();
2060
2061 let commit = {
2065 let handler = Rc::clone(&handler);
2066 let field = reading.clone();
2067 let row_id = row_id.clone();
2068 let key = key.clone();
2069 move |_: &Submit, window: &mut Window, cx: &mut App| {
2070 let value = field.read(cx).value().clone();
2071 handler(
2072 &EditIntent {
2073 row: row_id.clone(),
2074 column: key.clone(),
2075 value,
2076 outcome: EditOutcome::Commit,
2077 next: None,
2078 },
2079 window,
2080 cx,
2081 );
2082 cx.stop_propagation();
2083 }
2084 };
2085
2086 let revert = {
2087 let handler = Rc::clone(&handler);
2088 let row_id = row_id.clone();
2089 let key = key.clone();
2090 move |_: &Cancel, window: &mut Window, cx: &mut App| {
2091 handler(
2092 &EditIntent {
2093 row: row_id.clone(),
2094 column: key.clone(),
2095 value: seed.clone(),
2096 outcome: EditOutcome::Revert,
2097 next: None,
2098 },
2099 window,
2100 cx,
2101 );
2102 cx.stop_propagation();
2103 }
2104 };
2105
2106 let advance = {
2107 let handler = Rc::clone(&handler);
2108 let field = reading.clone();
2109 let row_id = row_id.clone();
2110 let key = key.clone();
2111 move |event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App| {
2112 if event.keystroke.key.as_str() != "tab" {
2113 return;
2114 }
2115 let value = field.read(cx).value().clone();
2116 handler(
2117 &EditIntent {
2118 row: row_id.clone(),
2119 column: key.clone(),
2120 value,
2121 outcome: EditOutcome::Commit,
2122 next: next.clone(),
2123 },
2124 window,
2125 cx,
2126 );
2127 cx.stop_propagation();
2128 }
2129 };
2130
2131 frame
2132 .capture_action::<Submit>(commit)
2133 .capture_action::<Cancel>(revert)
2134 .capture_key_down(advance)
2135 .child(field)
2136 .into_any_element()
2137}
2138
2139fn column_frame<E: Styled>(element: E, column: &GridColumn, theme: &Theme) -> E {
2140 let element = match column.width {
2141 ColumnWidth::Fixed(width) => element.w(px(width)).flex_none(),
2142 ColumnWidth::Flex(share) => element
2146 .flex_grow(share)
2147 .flex_shrink(1.0)
2148 .flex_basis(px(0.0))
2149 .min_w(px(column.min_width)),
2150 };
2151 let element = element.row().h_full().gap(px(theme.space(Space::Xs)));
2152 match column.align {
2153 Align::Start => element.justify_start(),
2154 Align::Center => element.justify_center(),
2155 Align::End => element.justify_end(),
2156 }
2157}
2158
2159fn current_index(
2165 drawn: &Drawn,
2166 anchor: Option<&SharedString>,
2167 selected: &BTreeSet<SharedString>,
2168) -> Option<usize> {
2169 let drawn = drawn.borrow();
2170 let matches = |id: &SharedString| {
2171 drawn
2172 .iter()
2173 .find(|(_, row)| *row == id)
2174 .map(|(index, _)| *index)
2175 };
2176 anchor
2177 .and_then(matches)
2178 .or_else(|| selected.iter().find_map(matches))
2179 .or_else(|| drawn.keys().min().copied())
2180}
2181
2182fn target_index(key: &str, from: Option<usize>, count: usize) -> Option<usize> {
2183 match key {
2184 "up" => from?.checked_sub(1),
2185 "down" => match from {
2186 Some(from) => Some(from + 1).filter(|next| *next < count),
2187 None => Some(0),
2188 },
2189 "home" => Some(0),
2190 "end" => count.checked_sub(1),
2191 _ => None,
2192 }
2193}
2194
2195fn reachable(
2201 render_row: &RenderRow,
2202 target: usize,
2203 step: isize,
2204 count: usize,
2205 window: &mut Window,
2206 cx: &mut App,
2207) -> Option<(usize, SharedString)> {
2208 let mut index = target as isize;
2209 while index >= 0 && (index as usize) < count {
2210 let row = render_row(index as usize, window, cx);
2211 if !row.disabled {
2212 return Some((index as usize, row.id));
2213 }
2214 index += step;
2215 }
2216 None
2217}
2218
2219type BulkHandler = Rc<dyn Fn(&mut Window, &mut App)>;
2222
2223#[derive(IntoElement)]
2231pub struct BulkBar {
2232 ident: Ident,
2233 count: usize,
2234 total: Option<usize>,
2235 noun: Option<SharedString>,
2236 actions: Vec<AnyElement>,
2237 on_select_all: Option<BulkHandler>,
2238 on_dismiss: Option<BulkHandler>,
2239}
2240
2241impl std::fmt::Debug for BulkBar {
2242 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2243 formatter
2244 .debug_struct("BulkBar")
2245 .field("ident", &self.ident)
2246 .field("count", &self.count)
2247 .field("total", &self.total)
2248 .field("actions", &self.actions.len())
2249 .finish()
2250 }
2251}
2252
2253impl BulkBar {
2254 pub fn new(ident: impl Into<Ident>, count: usize) -> Self {
2255 Self {
2256 ident: ident.into(),
2257 count,
2258 total: None,
2259 noun: None,
2260 actions: Vec::new(),
2261 on_select_all: None,
2262 on_dismiss: None,
2263 }
2264 }
2265
2266 pub fn total(mut self, total: usize) -> Self {
2269 self.total = Some(total);
2270 self
2271 }
2272
2273 pub fn noun(mut self, noun: impl Into<SharedString>) -> Self {
2275 self.noun = Some(noun.into());
2276 self
2277 }
2278
2279 pub fn action(mut self, action: impl IntoElement) -> Self {
2280 self.actions.push(action.into_any_element());
2281 self
2282 }
2283
2284 pub fn on_select_all(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
2285 self.on_select_all = Some(Rc::new(handler));
2286 self
2287 }
2288
2289 pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
2293 self.on_dismiss = Some(Rc::new(handler));
2294 self
2295 }
2296}
2297
2298impl RenderOnce for BulkBar {
2299 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
2300 let theme = cx.theme().clone();
2301 let state = memory(&self.ident.semantic_id(), cx);
2302 let progress = {
2303 let mut held = state.bulk.borrow_mut();
2304 let presence = held.get_or_insert_with(|| {
2305 if self.count > 0 {
2306 Presence::visible(entrance(&theme), state_change(&theme))
2308 } else {
2309 Presence::hidden(entrance(&theme), state_change(&theme))
2310 }
2311 });
2312 if self.count > 0 {
2313 presence.show();
2314 } else {
2315 presence.hide();
2316 }
2317 presence.animate(window, cx)
2318 };
2319
2320 if progress <= 0.0 {
2321 return div().into_any_element();
2322 }
2323
2324 let total = self.total.unwrap_or(self.count);
2325 let noun = self
2326 .noun
2327 .clone()
2328 .unwrap_or_else(|| cx.strings().text(StringKey::GridSelectedNoun));
2329 let label = SharedString::from(format!("{} {}", self.count, noun));
2330 let wider = self
2331 .on_select_all
2332 .clone()
2333 .filter(|_| total > self.count)
2334 .map(|handler| (handler, total));
2335
2336 let mut bar = div()
2337 .id(self.ident.element_id())
2338 .row()
2339 .w_full()
2340 .gap_token(&theme, Space::Sm)
2341 .px_token(&theme, Space::Sm)
2342 .py_token(&theme, Space::Xs)
2343 .radius(&theme, Radius::Control)
2344 .frame(&theme, Surface::Raised, Elevation::Raised)
2345 .opacity(progress)
2346 .child(text(&theme, TypeScale::Label, label.clone()).flex_none());
2347
2348 if let Some((handler, total)) = wider {
2349 let ident = self.ident.child("select-all");
2350 bar = bar.child(
2351 div()
2352 .id(ident.element_id())
2353 .flex_none()
2354 .cursor_pointer()
2355 .tab_index(0)
2356 .focus_ring(&theme)
2357 .child(
2358 text(
2359 &theme,
2360 TypeScale::Label,
2361 cx.strings()
2362 .format(StringKey::GridSelectAllTotal, &[&total.to_string()]),
2363 )
2364 .text_color(theme.colors.accent),
2365 )
2366 .on_click(move |_, window, cx| handler(window, cx))
2367 .semantic_in(
2368 cx,
2369 NodeSpec::new(ident.semantic_id(), Role::Button)
2370 .parent(self.ident.semantic_id())
2371 .text(
2372 cx.strings()
2373 .format(StringKey::GridSelectAllTotal, &[&total.to_string()]),
2374 )
2375 .value(total.to_string()),
2376 ),
2377 );
2378 }
2379
2380 bar = bar.child(div().flex_1());
2381
2382 for action in self.actions {
2383 bar = bar.child(div().flex_none().child(action));
2384 }
2385
2386 if let Some(handler) = self.on_dismiss.clone() {
2387 bar = bar.child(
2388 crate::controls::button::IconButton::new(
2389 self.ident.child("dismiss"),
2390 Icon::Close,
2391 cx.strings().text(StringKey::GridClearSelection),
2392 )
2393 .semantic_parent(self.ident.semantic_id())
2394 .on_click(move |window, cx| handler(window, cx)),
2395 );
2396 }
2397
2398 bar.semantic_in(
2399 cx,
2400 NodeSpec::new(self.ident.semantic_id(), Role::Toolbar)
2401 .text(label)
2402 .value(self.count.to_string()),
2403 )
2404 .into_any_element()
2405 }
2406}
2407
2408#[cfg(test)]
2409mod tests {
2410 use super::*;
2411
2412 #[test]
2413 fn an_unopened_grid_maps_every_slot_onto_its_own_row() {
2414 assert_eq!(slot_count(5, &[], 2), 5);
2415 assert_eq!(slot_at(0, &[], 2), Slot::Row(0));
2416 assert_eq!(slot_at(4, &[], 2), Slot::Row(4));
2417 assert_eq!(slot_of(3, &[], 2), 3);
2418 }
2419
2420 #[test]
2421 fn an_opened_row_holds_the_slots_beneath_it_open() {
2422 let expanded = [2usize];
2423 assert_eq!(slot_count(5, &expanded, 2), 7);
2424 assert_eq!(slot_at(1, &expanded, 2), Slot::Row(1));
2425 assert_eq!(slot_at(2, &expanded, 2), Slot::Row(2));
2426 assert_eq!(slot_at(3, &expanded, 2), Slot::Detail);
2427 assert_eq!(slot_at(4, &expanded, 2), Slot::Detail);
2428 assert_eq!(slot_at(5, &expanded, 2), Slot::Row(3));
2429 assert_eq!(slot_of(3, &expanded, 2), 5);
2430 }
2431
2432 #[test]
2433 fn two_opened_rows_each_hold_their_own_slots() {
2434 let expanded = [0usize, 3];
2435 assert_eq!(slot_count(5, &expanded, 1), 7);
2436 assert_eq!(slot_at(0, &expanded, 1), Slot::Row(0));
2437 assert_eq!(slot_at(1, &expanded, 1), Slot::Detail);
2438 assert_eq!(slot_at(2, &expanded, 1), Slot::Row(1));
2439 assert_eq!(slot_at(4, &expanded, 1), Slot::Row(3));
2440 assert_eq!(slot_at(5, &expanded, 1), Slot::Detail);
2441 assert_eq!(slot_at(6, &expanded, 1), Slot::Row(4));
2442 assert_eq!(slot_of(4, &expanded, 1), 6);
2443 }
2444
2445 #[test]
2446 fn a_move_stops_at_the_ends_instead_of_wrapping() {
2447 assert_eq!(target_index("up", Some(0), 10), None);
2448 assert_eq!(target_index("down", Some(9), 10), None);
2449 assert_eq!(target_index("home", Some(3), 10), Some(0));
2450 assert_eq!(target_index("end", Some(3), 10), Some(9));
2451 assert_eq!(target_index("down", None, 10), Some(0));
2452 }
2453}