Skip to main content

gpui_kit/data/
grid.rs

1//! A virtualized, column-oriented grid over caller-owned rows.
2//!
3//! [`crate::data::Table`] takes materialized rows, so every cell element
4//! already exists by the time the table sees it and every row it is given is
5//! laid out. `DataGrid` takes a *render closure* instead — the same shape
6//! [`crate::data::List`] uses — so the body can live inside
7//! [`gpui::uniform_list`] and only the rows the viewport holds are ever built.
8//!
9//! Everything the grid shows is the caller's answer. Order, sort, column
10//! widths, column order, selection, expansion, and the value in an editing
11//! cell are all host state; the grid reports what was operated and renders
12//! exactly what it was handed back, so a host that refuses a change keeps
13//! showing the state that still holds.
14//!
15//! # Which nodes publish
16//!
17//! Only the rows the viewport drew publish nodes. The grid node itself carries
18//! the number of rows it was given in `value`, which is how a snapshot stays
19//! honest about the difference between twelve thousand rows and the fourteen
20//! that are on screen. A cell publishes a [`Role::Cell`] node only where the
21//! caller marks it with [`Cell::published`] or declares its column editable.
22//!
23//! # What this grid does not do
24//!
25//! **It does not scroll horizontally.** `uniform_list` owns its own scroll
26//! offset and lays every row out at the width it is given; a frozen left group
27//! under a horizontal scroll needs either two vertically-synchronised uniform
28//! lists — and nothing in this GPUI revision keeps two
29//! [`gpui::UniformListScrollHandle`]s in step without one writing the other
30//! every frame, which is a redraw loop — or a per-row counter-translation that
31//! fights the list's own content mask. So [`GridColumn::pinned`] means "this
32//! column keeps the left edge, whatever order the caller puts the columns in,
33//! and cannot be dragged out of it", not "this column stays while the rest
34//! scrolls away". Columns share the grid's width the way `Table`'s do.
35//!
36//! **It does not measure a column to its content.** A double click on a
37//! resize handle reports a fit request and stops: the grid can only measure
38//! the rows it drew, and a width fitted to fourteen of twelve thousand rows is
39//! a guess wearing a measurement's clothes. The host owns the data and can
40//! answer properly.
41
42use 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
71/// How wide the grab area of a column edge is. The value occurs only here.
72const RESIZE_HANDLE: f32 = 7.0;
73
74/// How wide the leading gutter columns are — the select-all box and the
75/// disclosure. Neither holds data, so neither takes part in the column widths
76/// the caller owns.
77const GUTTER: f32 = 28.0;
78
79/// How much of a row's height the disclosure mark occupies.
80const 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/// One column of a grid.
94///
95/// `key` addresses the cells that belong to it and appears in every id the
96/// column publishes, so it is the column's business identity rather than its
97/// place in the header.
98#[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    /// The narrowest a resize may report, and the narrowest a flexible column
142    /// is allowed to shrink to.
143    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    /// Puts a grab handle on the column's trailing edge. The grid reports the
159    /// width the pointer asks for and applies nothing.
160    pub fn resizable(mut self, resizable: bool) -> Self {
161        self.resizable = resizable;
162        self
163    }
164
165    /// Lets the header be picked up and put down somewhere else. The order is
166    /// the caller's state; a drop reports where the column should go.
167    pub fn reorderable(mut self, reorderable: bool) -> Self {
168        self.reorderable = reorderable;
169        self
170    }
171
172    /// Keeps the column at the left edge whatever order the caller declares,
173    /// and takes it out of the reorder. See the module documentation for what
174    /// pinning does not do here.
175    pub fn pinned(mut self, pinned: bool) -> Self {
176        self.pinned = pinned;
177        self
178    }
179
180    /// Marks the column's cells as fields a double click or enter opens. The
181    /// grid never writes the value; it reports the edit.
182    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
192/// One row, built on demand, keyed by the identity the row already has.
193pub 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    /// The name the row publishes, for a test or a reader that has only the
237    /// tree to go on.
238    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/// How many rows may be selected at once.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
265pub enum SelectionMode {
266    #[default]
267    None,
268    Single,
269    Multiple,
270}
271
272/// What a gesture asked the selection to become.
273///
274/// The grid applies none of it. Every variant names rows by identity, and the
275/// two "all" variants are kept apart on purpose: a header checkbox can only
276/// speak for the rows the host has loaded, and claiming otherwise is the
277/// oldest lie in this part of the interface.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum SelectionChange {
280    /// A plain click: this row and nothing else.
281    Replace(SharedString),
282    /// A cmd or ctrl click: add this row, or take it away.
283    Toggle(SharedString),
284    /// A shift click: everything between the row last operated and this one.
285    /// The host owns the order, so the host resolves the span.
286    Range {
287        anchor: SharedString,
288        to: SharedString,
289    },
290    /// Every row the grid was given — which is every row the host has loaded,
291    /// and may be fewer than exist.
292    Loaded,
293    /// Every row that exists, including the ones nobody has loaded. Only ever
294    /// reported from a control that says that is what it does.
295    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/// One opened row: its identity, and where it sits.
313///
314/// A virtualized body reserves room by counting fixed-height slots, so it has
315/// to know where an opened row is before it has drawn it. The caller owns the
316/// order and can answer at once; the grid would have to build every row to
317/// find out. The index is layout arithmetic and never reaches an id.
318#[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/// How an edit ended.
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum EditOutcome {
336    /// Enter, or tab. The typed value is the caller's to apply.
337    Commit,
338    /// Escape. Nothing was typed as far as the host is concerned.
339    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/// One finished edit, as it is reported to the host.
352#[derive(Debug, Clone, PartialEq, Eq)]
353pub struct EditIntent {
354    pub row: SharedString,
355    pub column: SharedString,
356    /// What the field held. On a revert this is the value the grid was given,
357    /// unchanged.
358    pub value: SharedString,
359    pub outcome: EditOutcome,
360    /// The cell tab asked to open next, when tab is what ended the edit.
361    pub next: Option<(SharedString, SharedString)>,
362}
363
364/// The cell the caller has opened for editing, and the value it holds.
365#[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/// Whether a grid draws rules between its rows.
391///
392/// A grid groups with alignment and with the wash a row takes under the
393/// pointer, which is enough at the row heights this library ships. Rules are
394/// the answer to one specific problem — a dense grid whose rows are short
395/// enough that the eye loses which cell belongs to which record — and they
396/// stay off until a caller says that is the problem they have.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
398pub enum GridLines {
399    #[default]
400    None,
401    Rows,
402}
403
404/// A grid that renders only the rows its viewport holds.
405#[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    /// `count` is how many rows the caller has; `render_row` builds one of
458    /// them, and is only ever called for a row that is about to be drawn or
459    /// about to be reported.
460    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    /// How many rows exist on the host, when the grid has been given only some
499    /// of them. Without it the grid assumes it has everything.
500    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    /// Draws rules between rows, for a grid dense enough to need them.
516    pub fn lines(mut self, lines: GridLines) -> Self {
517        self.lines = lines;
518        self
519    }
520
521    /// The sort the caller applied, which is the only sort the grid shows.
522    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    /// The rows the caller says are selected. The grid renders these and
537    /// nothing else, whatever a click reported a moment ago.
538    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    /// The rows the caller has opened, and where each one sits.
544    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    /// How many row heights an opened detail region occupies.
551    pub fn detail_rows(mut self, rows: usize) -> Self {
552        self.detail_rows = rows.max(1);
553        self
554    }
555
556    /// Builds the detail region for an opened row. Called only for rows the
557    /// caller has opened *and* the viewport is drawing.
558    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    /// The cell the caller has opened for editing, and the value it holds.
567    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    /// Bounds the viewport to `rows` rows, which is what lets the grid skip
578    /// the rows it does not show.
579    pub fn visible_rows(mut self, rows: usize) -> Self {
580        self.visible_rows = Some(rows);
581        self
582    }
583
584    /// A first load with nothing to show yet. A refresh over rows that already
585    /// exist is not this; see [`DataGrid::failure`].
586    pub fn loading(mut self, loading: bool) -> Self {
587        self.loading = loading;
588        self
589    }
590
591    /// A refresh that failed. Rows the grid already has stay on screen with
592    /// the failure stated above them; only a failure with nothing behind it
593    /// takes the surface over.
594    pub fn failure(mut self, failure: impl Into<SharedString>) -> Self {
595        self.failure = Some(failure.into());
596        self
597    }
598
599    /// What to show when the query succeeded and returned nothing.
600    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    /// Reports the width a drag on a column edge asked for.
622    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    /// Reports that a double click on a column edge asked for a width that
631    /// fits the content. The grid measures nothing; see the module docs.
632    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    /// Reports where a dragged column should go. The grid does not move it.
641    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    /// Reports the disclosure state a row should take.
650    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    /// Reports that a cell was asked to become a field.
659    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    /// Reports how an edit ended, and what was in the field when it did.
668    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    /// The number of rows that exist, which is the number loaded unless the
677    /// caller said otherwise.
678    fn total_rows(&self) -> usize {
679        self.total.unwrap_or(self.count).max(self.count)
680    }
681
682    /// Pinned columns first, in the order the caller declared them.
683    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// -- slot arithmetic ----------------------------------------------------------
714
715/// What a virtualized slot holds.
716///
717/// A `uniform_list` gives every slot the same height, so an opened row cannot
718/// simply grow. Instead the detail region is painted over the slots that
719/// follow the row, and those slots are drawn empty so nothing lands on top of
720/// it.
721#[derive(Debug, Clone, Copy, PartialEq, Eq)]
722pub(crate) enum Slot {
723    Row(usize),
724    /// Space held open beneath an opened row.
725    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
732/// Which row, if any, a slot draws. `expanded` must be sorted.
733pub(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
751/// Which slot a row is drawn in. `expanded` must be sorted.
752pub(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// -- per-identity state -------------------------------------------------------
757
758/// What a grid remembers between two frames.
759///
760/// A `RenderOnce` builder is rebuilt every frame and cannot carry anything, so
761/// the scroll position, the anchor a shift click measures from, the drag a
762/// resize handle started, and the field an editing cell holds all live in an
763/// application global keyed by the grid's identity — the same arrangement
764/// [`crate::layout::measure`] uses for measurements.
765#[derive(Default)]
766struct Memory {
767    scroll: UniformListScrollHandle,
768    /// The row the last plain click or keyboard move landed on, which is what
769    /// a shift click measures its span from.
770    anchor: RefCell<Option<SharedString>>,
771    /// The column whose edge is currently being dragged.
772    resizing: RefCell<Option<SharedString>>,
773    editor: RefCell<Option<Entity<TextInput>>>,
774    /// The cell the field is currently showing, so the grid seeds it once
775    /// rather than on every frame.
776    edit_target: RefCell<Option<(SharedString, SharedString)>>,
777    /// Whether the bulk bar has been shown before, so a bar that exists on the
778    /// first frame is already there rather than arriving.
779    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
795/// Which index published which row id on the last frame.
796type Drawn = Rc<RefCell<HashMap<usize, SharedString>>>;
797
798// -- rendering ----------------------------------------------------------------
799
800impl 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    /// The refusal or failure shown above rows that are still true.
861    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            // A pinned column holds the left edge, so nothing may be dropped
913            // across it and it may not be picked up.
914            accepts: Rc::new(move |item: &DragItem, position: &DropPosition| {
915                item.source == own && !pinned.contains(position.anchor())
916            }),
917            on_drop,
918        })
919    }
920
921    /// The field an editing cell shows, created once per grid and seeded only
922    /// when the cell it belongs to changes.
923    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
958/// The question a drop asks before it is allowed to land.
959type Accepts = Rc<dyn Fn(&DragItem, &DropPosition) -> bool>;
960
961/// A column edge as the resize drag sees it: where the header was last painted,
962/// and the width it may not go below.
963type MeasuredEdge = (Rc<StdCell<gpui::Bounds<gpui::Pixels>>>, f32);
964
965/// What a header cell needs to take part in a column reorder.
966#[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    /// The box that speaks for the rows the host has loaded, and says so.
1017    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                        // A box in a virtualized grid can only ever speak for
1090                        // the rows the host handed over, so it publishes both
1091                        // numbers rather than one that reads like the whole
1092                        // data set.
1093                        .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        // A column that cannot be moved can still be landed beside, so every
1119        // unpinned header offers slots while only a reorderable one is a
1120        // handle.
1121        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        // The header cell is measured so a drag on its trailing edge can turn
1128        // a pointer position into a width.
1129        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                // A header reports the direction it currently shows, not the
1235                // one a click would ask for.
1236                .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        // A column that moved slides from where it was; the layout already
1245        // put it where the caller says it belongs.
1246        let handle = flip(ident.child("slide").semantic_id(), cx);
1247        cell.flip(&handle, window, cx).into_any_element()
1248    }
1249
1250    /// The grab area on a column's trailing edge.
1251    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                // `semantic_in` makes its host relative so the probe measures
1321                // it, which would put the handle back in the flow.
1322                .absolute()
1323                .into_any_element(),
1324        )
1325    }
1326
1327    /// Follows a resize drag across the whole grid.
1328    ///
1329    /// A seven-pixel handle cannot receive the moves of a drag that has left
1330    /// it, so the handle only records that one started and the frame does the
1331    /// following — the same arrangement [`crate::layout::SplitPane`] uses.
1332    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    /// Up, down, home and end move the reported selection and scroll the row
1382    /// they name into view, even when the viewport has never drawn it.
1383    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            // The anchor is read now rather than when the frame was built: a
1411            // click sets it without the caller having to redraw, and the move
1412            // that follows should start from the row that was clicked.
1413            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    /// What a grid with no rows shows, which is never the same thing twice.
1576    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
1637/// Everything a row needs that does not come from the row itself.
1638struct 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        // Tab leaves an open cell for the next editable column in the same
1718        // row. A row whose editable columns are exhausted simply commits: the
1719        // row after it may not have been drawn, and the grid will not build a
1720        // row nobody asked to see in order to guess where a caret should go.
1721        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            // A shift click measures from the anchor and does not move it, so
1765            // widening a span twice keeps starting where the typist started.
1766            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    // The detail is painted over the slots the grid held open beneath the
1792    // row, because a uniform list gives every slot the same height and an
1793    // opened row cannot simply grow.
1794    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                // `semantic_in` makes its host relative so the probe measures
1814                // it, which would put the detail back in the flow.
1815                .absolute(),
1816        );
1817    }
1818
1819    element.into_any_element()
1820}
1821
1822/// The line that says where the pinned group ends.
1823fn 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
1831/// The mark that says a row is in the selection. It is not a control: the row
1832/// itself takes the click, and a second box would be a second thing to aim at.
1833fn 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    // A treegrid's rendered columns are structural gridcells, not optional
1946    // diagnostic detail. DataGrid retains its opt-in publication policy.
1947    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/// A cell the caller has opened, showing the one field the grid keeps.
2035#[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    // The field's own key bindings dispatch before any ancestor's key
2062    // listener, so enter and escape are taken in the capture phase of the
2063    // action rather than as keystrokes that never arrive.
2064    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        // A zero basis makes a share depend on the flex factor alone, so a
2143        // header cell and the body cell under it always agree on where the
2144        // column starts.
2145        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
2159/// Where the row a keyboard move starts from sits among the rows that were
2160/// drawn.
2161///
2162/// A selection scrolled out of the viewport has no known index, so a move
2163/// starts from the top of what is visible rather than from nowhere.
2164fn 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
2195/// The first row from `target` in `step`'s direction that accepts a selection.
2196///
2197/// Naming a row that was never drawn means asking the caller to build it,
2198/// which is the only way a grid that does not hold the data can report a row
2199/// the typist cannot yet see.
2200fn 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
2219// -- the bulk bar -------------------------------------------------------------
2220
2221type BulkHandler = Rc<dyn Fn(&mut Window, &mut App)>;
2222
2223/// The bar that appears over a selection, says how large it is, and offers
2224/// what can be done to it.
2225///
2226/// The count is the number of rows the caller says are selected, which is the
2227/// number the bar states. When more rows exist than the host has loaded, the
2228/// bar offers the wider intent as a separate, named action rather than
2229/// quietly claiming the selection already covers them.
2230#[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    /// How many rows exist. When it is larger than the count, the bar offers
2267    /// to widen the selection and says exactly how far.
2268    pub fn total(mut self, total: usize) -> Self {
2269        self.total = Some(total);
2270        self
2271    }
2272
2273    /// What the count is counting, for a surface whose rows are not "rows".
2274    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    /// Dismissing a bulk bar clears the selection: there is nothing else it
2290    /// could mean, and a bar that hid while the selection stood would leave
2291    /// the typist operating rows they can no longer see.
2292    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                    // A bar the first frame already needs is not an arrival.
2307                    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}