Skip to main content

gpui_kit_grid/state/
mod.rs

1//! `LiveGridState`: the grid view. It owns the view settings, pulls store
2//! snapshots, runs projection rebuilds in the background and plans what the
3//! body element paints.
4
5mod a11y;
6// How a highlighted change paints a cell.
7mod change;
8// Clicks.
9mod click;
10mod columns;
11// The column chooser.
12mod chooser;
13// Copy.
14mod copy;
15// Exporting to a stream or a file.
16mod export;
17// Expanding and collapsing on screen.
18mod disclosure;
19// Dragging rows.
20mod drag;
21// Editing cells.
22mod edit;
23// The built-in cell editors.
24mod editors;
25mod events;
26// Removed rows fading out on screen.
27mod fade;
28mod frame;
29// The text style of drag ghosts, which GPUI draws outside the root.
30mod ghost;
31// The group panel.
32mod group_panel;
33// The grid without its delegate type.
34mod handle;
35// Links in cells.
36mod link;
37// Grouping and expansion.
38mod grouping;
39// Group captions on screen.
40mod groups;
41mod header;
42// Images in cells.
43mod images;
44mod layout;
45mod menu;
46// Commits waiting for an answer.
47mod pending;
48mod pinned;
49mod progress;
50mod projection;
51mod scroll;
52mod selection;
53mod stats;
54mod style;
55// Totals and group footers.
56mod summary;
57mod tooltip;
58// The tree on screen.
59mod tree;
60// Elements in cells.
61mod widgets;
62
63use std::{
64    hash::{Hash, Hasher as _},
65    sync::Arc,
66};
67
68use gpui_kit::base::{AutoScroll, Scrollbar, TestSupportExt as _};
69use gpui_kit::component::{ActiveTheme as _, Size as ComponentSize};
70use gpui_kit::{
71    App, Bounds, Context, EventEmitter, FocusHandle, Focusable, InteractiveElement as _,
72    IntoElement, KeyContext, ParentElement as _, Pixels, Render, SharedString,
73    StatefulInteractiveElement as _, Styled as _, Task, Window, div, prelude::FluentBuilder as _,
74    px,
75};
76use web_time::Instant;
77
78pub(crate) use self::{
79    a11y::{A11yPlan, FOCUS_CELL_ID},
80    tooltip::CellHover,
81};
82use self::{
83    a11y::{RowNumbering, root_role},
84    click::PendingClick,
85    columns::ColumnLayout,
86    frame::PaintedLine,
87    ghost::GhostTextStyle,
88    grouping::ExpansionRevision,
89    groups::CaptionTexts,
90    header::HeaderState,
91    menu::OpenMenu,
92    pinned::PaintedBands,
93    progress::ProgressSlot,
94    projection::{Coverage, HeldOrder, PendingWork},
95    scroll::{GridScrollHandle, LineAnchor, ScrollReset},
96    selection::{PointerTarget, Press, SelectionModel},
97    summary::SummaryTexts,
98    tooltip::TooltipState,
99};
100pub use self::{
101    change::{ChangePaint, ChangePainter, ChangePalette, DirectionalChanges, UniformChanges},
102    chooser::{
103        ColumnChooserContext, ColumnChooserFactory, ColumnChooserPanel, PopoverColumnChooser,
104    },
105    columns::{
106        CellContent, ColumnAlign, ColumnBand, ColumnZone, GridColumn, GridSchema, HeaderLabel,
107        MIN_COLUMN_WIDTH, ViewColumnIx,
108    },
109    drag::{DragPayload, DropPlace, DropSource, DropTarget, RowDrag, RowDragContext},
110    edit::{
111        CellEditor, EditCancelReason, EditError, EditId, EditInput, EditMessageContext, EditOrigin,
112        EditState, EditValidateContext, EditableContext, EditorCell, EditorField, EditorMark,
113        EditorOpen, EditorStart,
114    },
115    editors::{
116        CellEditHandle, CheckboxEditor, DateEditor, NumberEditor, NumberKind, SelectEditor,
117        SelectOption, TextEditor,
118    },
119    events::{
120        ExpandTarget, GroupOrigin, LayoutOrigin, LayoutPart, LayoutParts, LiveGridEvent,
121        SelectionOrigin, SortOrigin,
122    },
123    export::{EmptyExport, ExportHeader, ExportJob, ExportOutcome, ExportProgress, ExportRequest},
124    groups::{CaptionTag, GroupCaptionContext},
125    handle::{BandInfo, ColumnInfo, GridHandleEvent, LiveGridHandle, WeakLiveGridHandle},
126    images::{AssetImageSource, CellImage, CellImageSource, CellImages, ImageFit},
127    link::{LinkOpener, OpenInBrowser},
128    menu::{GridMenuContext, GridTarget},
129    progress::ProgressContext,
130    projection::FlushOutcome,
131    selection::{FocusedCell, GridSelection, ScopedSelection, SelectionMode},
132    stats::{GridStats, GridTuning},
133    style::{
134        AggregateLine, AggregateStyleContext, CellChange, CellStyle, CellStyleContext,
135        LiveGridDelegate,
136    },
137    tooltip::{CellTooltip, CellTooltipContext, HeaderTooltipContext},
138    widgets::CellElementContext,
139};
140use crate::{
141    actions::{CELL_CONTENT_KEY, KEY_CONTEXT},
142    element::GridBody,
143    engine::{GroupPath, Projection, ProjectionEngine, Rebuild, ViewSpec},
144    model::{ColumnIx, ColumnSet, GridColumns as _, GridRow},
145    policy::{
146        AutofitPolicy, BackgroundRunner, BandedMove, ColumnMovePolicy, ContextMenuGesture,
147        ExpandGesture, FocusCaptions, LiveReorder, ProjectionScope, ReorderPolicy,
148        SelectionGesture, SelectionPolicy, SelectionScope, SortCycle, StandardContextMenuGesture,
149        StandardExpandGesture, StandardSelectionGesture, StandardSortCycle, TaskRunner,
150        VisibleRowsAutofit,
151    },
152    quick_filter::{ContainsAllTerms, QuickFilterMatcher},
153    store::{GridStore, GridWriter, PinnedEdge, StoreOptions, StoreSnapshot},
154    text::{TextCaches, TextStats},
155};
156
157type ColumnOf<D> = <<D as LiveGridDelegate>::Row as GridRow>::Column;
158type KeyOf<D> = <<D as LiveGridDelegate>::Row as GridRow>::Key;
159
160const CELL_PADDING: Pixels = px(6.);
161
162/// The hash of a row key: the integer of element ids made from a row's
163/// identity, such as the tooltip div of a hovered cell
164/// (`NamedInteger(column id, key_hash)`) and the accessibility row of a
165/// data row (`NamedInteger("live-grid-row", key_hash)`). A pinned row's key
166/// is of its band's key space: `pinned::pinned_key_hash` hashes the band
167/// with it. `FxHasher`: fast, and stable within the process, which is all
168/// an element id needs.
169pub(crate) fn key_hash<K: Hash + ?Sized>(key: &K) -> u64 {
170    let mut hasher = rustc_hash::FxHasher::default();
171    key.hash(&mut hasher);
172    hasher.finish()
173}
174
175/// The hash of a group's path in element ids: a caption's accessibility row
176/// (`NamedInteger("live-grid-group-row", ..)`), its tooltip divs and its
177/// focus. Tagged, so it never equals a data row's `key_hash`, as
178/// `pinned_key_hash` is.
179pub(crate) fn group_hash(path: &GroupPath) -> u64 {
180    key_hash(&("live-grid-group", path))
181}
182
183/// The hash of a group footer's path in element ids: its accessibility row
184/// (`NamedInteger("live-grid-group-footer-row", ..)`) and its tooltip divs.
185pub(crate) fn footer_hash(path: &GroupPath) -> u64 {
186    key_hash(&("live-grid-group-footer", path))
187}
188
189/// The hash of the totals row of a band in element ids: its accessibility
190/// row (`NamedInteger("live-grid-totals-row", ..)`) and its tooltip divs.
191pub(crate) fn totals_hash(edge: PinnedEdge) -> u64 {
192    key_hash(&(
193        "live-grid-totals",
194        match edge {
195            PinnedEdge::Top => "top",
196            PinnedEdge::Bottom => "bottom",
197        },
198    ))
199}
200
201/// The state and view of a live grid.
202pub struct LiveGridState<D: LiveGridDelegate> {
203    delegate: D,
204    store: GridStore<D::Row>,
205    snapshot: Arc<StoreSnapshot<D::Row>>,
206    projection: Arc<Projection>,
207    view: ViewSpec<D::Row>,
208    engine: Arc<dyn ProjectionEngine<D::Row>>,
209    tuning: GridTuning,
210    pending: PendingWork,
211    rebuild_task: Option<Task<()>>,
212    rebuild_timer: Option<Task<()>>,
213    last_rebuild_started: Option<Instant>,
214    /// The pending work comes from a user action (a new view, a new data
215    /// set): its result skips the reorder hold.
216    pending_user_action: bool,
217    /// The pending work comes from a new view (sort, filter, quick filter,
218    /// grouping): its result is shown from the top, or with a painted
219    /// focused line on its screen line.
220    pending_scroll_reset: ScrollReset,
221    /// Counts the edits of the view's expansion.
222    expansion_revision: ExpansionRevision,
223    /// The expansion the shown projection's lines are laid out for.
224    shown_revision: ExpansionRevision,
225    /// Values aggregates read changed since the last job that recounted
226    /// them; a held order does not hold them.
227    aggregates_changed: bool,
228    /// The caption or tree row the user toggled last: it keeps its screen
229    /// line when the lines of the new expansion are shown.
230    expand_anchor: Option<LineAnchor>,
231    /// How a press expands or collapses a caption or a tree row.
232    expand_gesture: Box<dyn ExpandGesture>,
233    /// The texts of captions and aggregates, by locale.
234    caption_texts: CaptionTexts,
235    /// The band of the totals row, if shown, and its label by locale.
236    totals: Option<PinnedEdge>,
237    summary_texts: SummaryTexts,
238    reorder: Box<dyn ReorderPolicy>,
239    runner: Box<dyn TaskRunner>,
240    sort_cycle: Box<dyn SortCycle>,
241    quick_filter_matcher: Arc<dyn QuickFilterMatcher>,
242    /// The columns the quick filter searches; `None`: the visible ones.
243    quick_filter_columns: Option<ColumnSet>,
244    /// The quick filter text, trimmed; empty when the filter is off.
245    quick_filter_text: SharedString,
246    held: Option<HeldOrder>,
247    pointer_inside: bool,
248    /// The store version of the latest removal: a projection built before it
249    /// may still hold removed rows.
250    last_removal_version: u64,
251    columns: ColumnLayout<ColumnOf<D>>,
252    /// Saved layouts: view id, codec, merge, schema defaults.
253    layout: layout::LayoutSlot<D::Row>,
254    column_move: Box<dyn ColumnMovePolicy>,
255    autofit: Box<dyn AutofitPolicy>,
256    /// Columns to autofit in the next painted frame, and who asked.
257    pending_autofit: Option<(ColumnSet, LayoutOrigin)>,
258    /// The column that column commands act on.
259    current_column: Option<ColumnIx>,
260    header: HeaderState,
261    scroll_row: usize,
262    scroll_offset: Pixels,
263    scroll_x: Pixels,
264    row_height: Pixels,
265    body_bounds: Bounds<Pixels>,
266    /// What the last frame painted, one entry per line of the data rows:
267    /// hit tests use what is on screen, not the projection, which may still
268    /// hold removed rows.
269    painted: Vec<PaintedLine>,
270    /// The pinned rows painted in the last frame, band by band.
271    painted_pinned: PaintedBands<D::Row>,
272    /// The coverage of the order shown or held, and the flushes waiting
273    /// for it.
274    coverage: Coverage,
275    /// The view's progress and the store's progress as shown.
276    progress: ProgressSlot,
277    /// The scroll position as the scrollbars see it.
278    scroll_handle: GridScrollHandle,
279    /// The selection, the focused cell and the anchor.
280    selection: SelectionModel<KeyOf<D>, ColumnOf<D>>,
281    selection_scope: Box<dyn SelectionScope<D::Row>>,
282    /// Which lines take the selection; what a command over a line acts on.
283    selection_policy: Box<dyn SelectionPolicy<D::Row>>,
284    selection_gesture: Box<dyn SelectionGesture>,
285    context_menu_gesture: Box<dyn ContextMenuGesture>,
286    /// What the last mouse-down on the body hit: the context menu's target.
287    pointer_target: Option<PointerTarget<KeyOf<D>>>,
288    /// The primary-button press in progress.
289    press: Option<Press<KeyOf<D>, ColumnOf<D>>>,
290    /// The row of the first press of a click series: a double click
291    /// confirms it.
292    press_series: Option<KeyOf<D>>,
293    /// The press on the grid that ends in a `Click` if it is released
294    /// without a drag.
295    click: Option<PendingClick<KeyOf<D>, ColumnOf<D>>>,
296    /// Scrolls the rows while a drag-selection is above or below them.
297    drag_scroll: AutoScroll,
298    /// The open context menu.
299    menu: Option<OpenMenu<KeyOf<D>, ColumnOf<D>>>,
300    /// The pointer over the body and the header, and the hovered cell's
301    /// tooltip.
302    tooltip: TooltipState,
303    /// The last frame showed keyboard focus (focus-visible).
304    #[cfg(any(test, feature = "test-support"))]
305    focus_visible: bool,
306    /// The grid's accessible name, from `LiveGrid::accessibility_label`.
307    accessibility_label: Option<SharedString>,
308    /// Plan the accessibility tree while the window's accessibility is off
309    /// ([`Self::set_a11y_forced`]).
310    #[cfg(any(test, feature = "test-support"))]
311    a11y_forced: bool,
312    /// The accessibility plan of the last frame, for tests.
313    #[cfg(any(test, feature = "test-support"))]
314    last_a11y: Option<std::rc::Rc<A11yPlan>>,
315    text_cache: TextCaches,
316    /// How highlighted changes paint cells.
317    change_paint: ChangePaint,
318    /// A test's start of cells: the column, the indent and an icon.
319    #[cfg(any(test, feature = "test-support"))]
320    test_cell_lead: Option<(ColumnIx, Pixels, Option<SharedString>)>,
321    /// A test's cell host on every cell of a column.
322    #[cfg(any(test, feature = "test-support"))]
323    test_cell_host: Option<frame::TestCellHost>,
324    /// What the last frame painted, in order.
325    #[cfg(any(test, feature = "test-support"))]
326    last_paint_order: Vec<&'static str>,
327    /// The icons the body painted in the last frame.
328    #[cfg(any(test, feature = "test-support"))]
329    painted_icons: Vec<(Bounds<Pixels>, SharedString, gpui_kit::Hsla)>,
330    /// The images the body painted in the last frame.
331    #[cfg(any(test, feature = "test-support"))]
332    painted_images: Vec<frame::PaintedImage>,
333    /// The caption tags the body painted in the last frame.
334    #[cfg(any(test, feature = "test-support"))]
335    painted_pills: Vec<(Bounds<Pixels>, gpui_kit::Hsla)>,
336    fade: Option<(u32, Task<()>)>,
337    stats: GridStats,
338    focus_handle: FocusHandle,
339    // Copy: the exporters and the copy being written.
340    copy: copy::CopySlot<D::Row>,
341    // The column chooser.
342    column_chooser: chooser::ChooserSlot,
343    // The group panel.
344    group_panel: group_panel::GroupPanelState,
345    // Editing: the editors and edit seams, the open edit, the commits
346    // waiting, and the elements in cells.
347    edit_setup: edit::EditSetup<D::Row>,
348    edit: edit::EditSlot<KeyOf<D>, ColumnOf<D>>,
349    pending_edits: pending::PendingEdits<KeyOf<D>, ColumnOf<D>>,
350    widgets: widgets::WidgetSlot<KeyOf<D>, ColumnOf<D>>,
351    links: link::LinkSlot,
352    // Where the images of image cells come from.
353    images: images::CellImages,
354    // Row drags.
355    drag: drag::DragSlot<D>,
356    _pull: Task<()>,
357}
358
359impl<D: LiveGridDelegate> LiveGridState<D> {
360    /// A grid over `store`. Several grids may share one store.
361    pub fn new(
362        schema: GridSchema<ColumnOf<D>>,
363        delegate: D,
364        store: GridStore<D::Row>,
365        cx: &mut Context<Self>,
366    ) -> Self {
367        let subscription = store.subscribe();
368        let snapshot = store.snapshot();
369        let pull = cx.spawn(async move |this, cx| {
370            while subscription.changed().await {
371                let (snapshot, changes) = subscription.take();
372                let Ok(interval) = this.update(cx, |state, cx| {
373                    state.apply_snapshot(snapshot, changes, cx);
374                    state.tuning.pull_interval
375                }) else {
376                    break;
377                };
378                cx.background_executor().timer(interval).await;
379            }
380        });
381        Self {
382            delegate,
383            progress: ProgressSlot::new(snapshot.progress()),
384            snapshot,
385            store,
386            projection: Arc::new(Projection::default()),
387            view: ViewSpec::new(),
388            engine: Arc::new(Rebuild::default()),
389            tuning: GridTuning::default(),
390            pending: PendingWork::Rebuild { expand: false },
391            rebuild_task: None,
392            rebuild_timer: None,
393            last_rebuild_started: None,
394            pending_user_action: true,
395            pending_scroll_reset: ScrollReset::Keep,
396            expansion_revision: ExpansionRevision::default(),
397            shown_revision: ExpansionRevision::default(),
398            aggregates_changed: false,
399            expand_anchor: None,
400            expand_gesture: Box::new(StandardExpandGesture),
401            caption_texts: CaptionTexts::default(),
402            totals: None,
403            summary_texts: SummaryTexts::default(),
404            reorder: Box::new(LiveReorder),
405            runner: Box::new(BackgroundRunner),
406            sort_cycle: Box::new(StandardSortCycle),
407            quick_filter_matcher: Arc::new(ContainsAllTerms),
408            quick_filter_columns: None,
409            quick_filter_text: SharedString::default(),
410            held: None,
411            pointer_inside: false,
412            last_removal_version: 0,
413            // Reads the schema before `columns` takes it.
414            layout: layout::LayoutSlot::new(&schema),
415            columns: {
416                let mut columns = ColumnLayout::new(schema);
417                columns.normalize(&BandedMove);
418                columns
419            },
420            column_move: Box::new(BandedMove),
421            autofit: Box::new(VisibleRowsAutofit),
422            pending_autofit: None,
423            current_column: None,
424            header: HeaderState::default(),
425            scroll_row: 0,
426            scroll_offset: px(0.),
427            scroll_x: px(0.),
428            // gpui-kit default size (md); `LiveGrid` sets the size it is given.
429            row_height: ComponentSize::default().table_row_height(),
430            body_bounds: Bounds::default(),
431            painted: Vec::new(),
432            painted_pinned: PaintedBands::default(),
433            coverage: Coverage::default(),
434            scroll_handle: GridScrollHandle::default(),
435            selection: SelectionModel::new(SelectionMode::default()),
436            selection_scope: Box::new(ProjectionScope),
437            selection_policy: Box::new(FocusCaptions),
438            selection_gesture: Box::new(StandardSelectionGesture),
439            context_menu_gesture: Box::new(StandardContextMenuGesture),
440            pointer_target: None,
441            press: None,
442            press_series: None,
443            click: None,
444            drag_scroll: AutoScroll::default(),
445            menu: None,
446            tooltip: TooltipState::default(),
447            #[cfg(any(test, feature = "test-support"))]
448            focus_visible: false,
449            accessibility_label: None,
450            #[cfg(any(test, feature = "test-support"))]
451            a11y_forced: false,
452            #[cfg(any(test, feature = "test-support"))]
453            last_a11y: None,
454            text_cache: TextCaches::default(),
455            change_paint: ChangePaint::default(),
456            #[cfg(any(test, feature = "test-support"))]
457            test_cell_lead: None,
458            #[cfg(any(test, feature = "test-support"))]
459            test_cell_host: None,
460            #[cfg(any(test, feature = "test-support"))]
461            last_paint_order: Vec::new(),
462            #[cfg(any(test, feature = "test-support"))]
463            painted_icons: Vec::new(),
464            #[cfg(any(test, feature = "test-support"))]
465            painted_images: Vec::new(),
466            #[cfg(any(test, feature = "test-support"))]
467            painted_pills: Vec::new(),
468            fade: None,
469            stats: GridStats::default(),
470            // The grid is one Tab stop; Tab leaves it.
471            focus_handle: cx.focus_handle().tab_stop(true),
472            copy: copy::CopySlot::default(),
473            column_chooser: chooser::ChooserSlot::default(),
474            group_panel: group_panel::GroupPanelState::default(),
475            edit_setup: edit::EditSetup::default(),
476            edit: edit::EditSlot::default(),
477            pending_edits: pending::PendingEdits::default(),
478            widgets: widgets::WidgetSlot::default(),
479            links: link::LinkSlot::default(),
480            images: images::CellImages::default(),
481            drag: drag::DragSlot::default(),
482            _pull: pull,
483        }
484    }
485
486    /// A grid with its own store.
487    pub fn new_standalone(
488        schema: GridSchema<ColumnOf<D>>,
489        delegate: D,
490        options: StoreOptions,
491        cx: &mut Context<Self>,
492    ) -> Self {
493        let store = GridStore::new(options, cx);
494        Self::new(schema, delegate, store, cx)
495    }
496
497    /// Sets the view (filter, sort, grouping or tree, aggregates). The grid
498    /// owns the quick filter and the expansion: those of `view` are replaced
499    /// by the grid's own ([`Self::set_quick_filter`],
500    /// [`Self::with_expand_depth`]). The sort and the grouping of a layout
501    /// given to [`Self::with_layout`], before or after, replace those of
502    /// `view`.
503    pub fn with_view(mut self, view: ViewSpec<D::Row>) -> Self {
504        let quick_filter = self.view.quick_filter().cloned();
505        let expansion = self.view.expansion().clone();
506        self.view = view
507            .with_quick_filter(quick_filter)
508            .with_expansion(expansion);
509        // The default layout keeps this sort and grouping.
510        self.layout.set_view(&self.view);
511        self.apply_initial_layout()
512    }
513
514    /// Sets the projection engine (default [`Rebuild`]).
515    pub fn with_engine(mut self, engine: impl ProjectionEngine<D::Row>) -> Self {
516        self.engine = Arc::new(engine);
517        self
518    }
519
520    /// Sets the timing knobs.
521    pub fn with_tuning(mut self, tuning: GridTuning) -> Self {
522        self.tuning = tuning;
523        self
524    }
525
526    /// Sets when a new display order replaces the shown one (default
527    /// [`LiveReorder`]; see also `HoldReorder` and `ManualReorder`).
528    pub fn with_reorder_policy(mut self, policy: impl ReorderPolicy) -> Self {
529        self.reorder = Box::new(policy);
530        self
531    }
532
533    /// Sets where background jobs run (default [`BackgroundRunner`]).
534    pub fn with_task_runner(mut self, runner: impl TaskRunner) -> Self {
535        self.runner = Box::new(runner);
536        self
537    }
538
539    /// Sets how a header click changes the sort (default
540    /// [`StandardSortCycle`]).
541    pub fn with_sort_cycle(mut self, cycle: impl SortCycle) -> Self {
542        self.sort_cycle = Box::new(cycle);
543        self
544    }
545
546    /// Sets how quick filter text matches rows (default
547    /// [`ContainsAllTerms`]).
548    pub fn with_quick_filter_matcher(mut self, matcher: impl QuickFilterMatcher) -> Self {
549        self.quick_filter_matcher = Arc::new(matcher);
550        self.recompile_quick_filter();
551        self
552    }
553
554    /// Sets the columns the quick filter searches (default: the visible
555    /// columns, frozen and scrolled out of view included). Hiding a column
556    /// does not change an explicit set.
557    pub fn with_quick_filter_columns(
558        mut self,
559        columns: impl IntoIterator<Item = ColumnOf<D>>,
560    ) -> Self {
561        let set = columns
562            .into_iter()
563            .fold(ColumnSet::EMPTY, |mut set, column| {
564                set.insert(column.ix());
565                set
566            });
567        self.quick_filter_columns = Some(set);
568        self.recompile_quick_filter();
569        self
570    }
571
572    /// Sets the row height (from the [`crate::LiveGrid`] size).
573    pub(crate) fn set_row_height(&mut self, row_height: Pixels) {
574        if self.row_height != row_height {
575            self.row_height = row_height;
576            self.clamp_scroll();
577        }
578    }
579
580    /// Sets the accessible name (from the [`crate::LiveGrid`] label).
581    pub(crate) fn set_accessibility_label(&mut self, label: Option<SharedString>) {
582        self.accessibility_label = label;
583    }
584
585    /// Plans the accessibility tree even when the window's accessibility is
586    /// off, so headless tests can check the planned elements: the frame
587    /// plans the rows and the focused cell, prepaints their elements and
588    /// builds the nodes GPUI would publish (it publishes none: the window's
589    /// accessibility is off). Takes effect with the next frame.
590    #[cfg(any(test, feature = "test-support"))]
591    pub fn set_a11y_forced(&mut self, forced: bool) {
592        self.a11y_forced = forced;
593    }
594
595    /// The description the last planned accessibility tree gives the
596    /// focused cell: the column title, then the expansion state, the trend
597    /// ("rising", "falling") and the tooltip text. `None` without a plan or
598    /// a painted focused cell. For tests, with [`Self::set_a11y_forced`].
599    #[cfg(any(test, feature = "test-support"))]
600    pub fn a11y_focus_description(&self) -> Option<&str> {
601        self.last_a11y.as_ref()?.focus_description()
602    }
603
604    /// Gives every cell of `column` in the data rows a start, as group
605    /// captions and tree rows have: the text begins `indent` further in
606    /// with that much less room, and `icon` (an icon path) sits in the
607    /// indent's last 20 px. For tests of the frame. Takes effect with the
608    /// next frame.
609    #[cfg(any(test, feature = "test-support"))]
610    pub fn set_test_cell_lead(
611        &mut self,
612        column: ColumnOf<D>,
613        indent: Pixels,
614        icon: Option<SharedString>,
615    ) {
616        self.test_cell_lead = Some((column.ix(), indent, icon));
617    }
618
619    /// Gives every cell of `column` in the painted data and pinned rows a
620    /// cell host: a real element over the cell, after a tree row's indent,
621    /// made by `build` from the host's div, whose id is
622    /// `NamedChild("live-grid-cell", column id)` in the id scope of the row's
623    /// element (`NamedInteger("live-grid-row", key hash)`). The body paints
624    /// no text in those cells. For tests of the cell host mechanism; `None`
625    /// removes it. Takes effect with the next frame.
626    #[cfg(any(test, feature = "test-support"))]
627    pub fn set_test_cell_host(
628        &mut self,
629        column: Option<ColumnOf<D>>,
630        build: impl Fn(gpui_kit::Stateful<gpui_kit::Div>) -> gpui_kit::Stateful<gpui_kit::Div> + 'static,
631    ) {
632        self.test_cell_host = column.map(|column| {
633            let build: std::rc::Rc<
634                dyn Fn(gpui_kit::Stateful<gpui_kit::Div>) -> gpui_kit::Stateful<gpui_kit::Div>,
635            > = std::rc::Rc::new(build);
636            (column.ix(), build)
637        });
638    }
639
640    /// What the body painted in the last frame, in order: `regions` (the
641    /// cells' backgrounds, lines, text and icons), `dividers`, `tooltip`
642    /// (the hovered cell's div), `rows` (row elements: accessibility rows
643    /// and cell hosts) and `outlines` (the focused cell's), each when there
644    /// was one. For tests.
645    #[cfg(any(test, feature = "test-support"))]
646    pub fn last_paint_order(&self) -> &[&'static str] {
647        &self.last_paint_order
648    }
649
650    #[cfg(any(test, feature = "test-support"))]
651    pub(crate) fn set_last_paint_order(&mut self, order: Vec<&'static str>) {
652        self.last_paint_order = order;
653    }
654
655    /// Records how long the last frame's row elements took to lay out and
656    /// prepaint, when it had cell hosts.
657    pub(crate) fn set_last_hosts_prepaint(&mut self, elapsed: std::time::Duration) {
658        self.stats.last_hosts_prepaint = elapsed;
659    }
660
661    /// Whether this frame builds the accessibility tree of the body: only
662    /// while a screen reader or another client is listening (or forced in
663    /// tests), so the grid costs nothing for accessibility otherwise.
664    fn is_a11y_planned(&self, window: &Window) -> bool {
665        #[cfg(any(test, feature = "test-support"))]
666        if self.a11y_forced {
667            return true;
668        }
669        window.is_a11y_active()
670    }
671
672    /// The root's key context: [`KEY_CONTEXT`], with
673    /// [`CELL_CONTENT_KEY`] set to what the focused data cell's column
674    /// paints, so a key binding can tell a control from text.
675    fn root_key_context(&self) -> KeyContext {
676        let mut context = KeyContext::default();
677        context.add(KEY_CONTEXT);
678        let content = self
679            .selection
680            .focus()
681            .filter(|focus| focus.row().is_some())
682            .map(|focus| self.columns.column(focus.column().ix()).content);
683        if self.is_link_focused() {
684            context.set(CELL_CONTENT_KEY, "link");
685        } else if let Some(content) = content {
686            context.set(CELL_CONTENT_KEY, content.key_value());
687        }
688        context
689    }
690
691    /// How the accessibility tree numbers rows now: the header, the top
692    /// band, the rows of the shown projection, the bottom band, with
693    /// the totals row as the outermost row of its band.
694    fn a11y_numbering(&self) -> RowNumbering {
695        RowNumbering::new(
696            self.snapshot.pinned_rows(PinnedEdge::Top).len(),
697            self.projection.len(),
698            self.snapshot.pinned_rows(PinnedEdge::Bottom).len(),
699        )
700        .with_totals(self.totals)
701    }
702
703    /// The store.
704    pub fn store(&self) -> &GridStore<D::Row> {
705        &self.store
706    }
707
708    /// A writer for producers on any thread.
709    pub fn writer(&self) -> GridWriter<D::Row> {
710        self.store.writer()
711    }
712
713    /// The snapshot currently shown.
714    pub fn snapshot(&self) -> &Arc<StoreSnapshot<D::Row>> {
715        &self.snapshot
716    }
717
718    /// The display order currently shown.
719    pub fn projection(&self) -> &Arc<Projection> {
720        &self.projection
721    }
722
723    /// Profiling counters.
724    pub fn stats(&self) -> GridStats {
725        // The cycles of the tree shown.
726        self.stats.with_tree_cycles(self.projection.tree_cycles())
727    }
728
729    /// Text cache counters.
730    pub fn text_stats(&self) -> TextStats {
731        self.text_cache.stats()
732    }
733}
734
735impl<D: LiveGridDelegate> EventEmitter<LiveGridEvent<KeyOf<D>, ColumnOf<D>>> for LiveGridState<D> {}
736
737impl<D: LiveGridDelegate> Focusable for LiveGridState<D> {
738    fn focus_handle(&self, _: &App) -> FocusHandle {
739        self.focus_handle.clone()
740    }
741}
742
743impl<D: LiveGridDelegate> Render for LiveGridState<D> {
744    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
745        self.close_stale_menu(window, cx);
746        // A row drag whose release the grid did not see.
747        self.settle_row_drag(cx);
748        self.apply_scroll_request();
749        self.ensure_projection(cx);
750        let a11y = self.is_a11y_planned(window);
751        let header_tooltip = self.header_tooltip(window, cx);
752        // The drag ghosts' text style: the one the grid inherits here.
753        let ghost_text = GhostTextStyle::capture(window);
754        let header = self.render_header(header_tooltip, &ghost_text, a11y, cx);
755        // The group panel, if `LiveGrid` shows it.
756        let group_panel = self.render_group_panel(window, cx);
757        let scrollbars = self.render_scrollbars();
758        let progress = self.render_progress_overlay(window, cx);
759        let menu = self.render_menu(window);
760        // The column chooser's layer, as the menu's.
761        let column_chooser = self.render_column_chooser(window, cx);
762        // The root is the `Grid` node, a `TreeGrid` for a grouped view or a
763        // tree, and keeps the focus; the focused cell claims the active
764        // descendant under it. The counts are of the whole grid, not of what
765        // is painted.
766        let role = root_role(self.projection.is_grouped() || self.projection.is_tree());
767        let row_count = self.a11y_numbering().row_count();
768        let column_count = self.columns.visible_set().len();
769        let multiselectable = !self.selection.mode().is_single();
770        let theme = cx.theme();
771        let root = div()
772            .id("live-grid")
773            .test_support()
774            .track_focus(&self.focus_handle)
775            .role(role)
776            .when_some(self.accessibility_label.clone(), |this, label| {
777                this.aria_label(label)
778            })
779            .aria_row_count(row_count)
780            .aria_column_count(column_count)
781            // A div has no `aria_*` for it: set on the node itself, and only
782            // while the tree is built (the closure is a boxed allocation).
783            .when(a11y && multiselectable, |this| {
784                this.a11y_synthetic_children(|builder| builder.parent_node().set_multiselectable())
785            })
786            .key_context(self.root_key_context())
787            .on_action(cx.listener(Self::on_autofit_columns))
788            .on_action(cx.listener(Self::on_autofit_column))
789            .on_action(cx.listener(Self::on_hide_column))
790            .on_action(cx.listener(Self::on_show_all_columns))
791            .on_action(cx.listener(Self::on_freeze_column_left))
792            .on_action(cx.listener(Self::on_freeze_column_right))
793            .on_action(cx.listener(Self::on_unfreeze_column))
794            .on_action(cx.listener(Self::on_move_column_left))
795            .on_action(cx.listener(Self::on_move_column_right))
796            .on_action(cx.listener(Self::on_reset_layout))
797            .on_action(cx.listener(Self::on_show_context_menu))
798            .on_action(cx.listener(Self::on_show_column_chooser))
799            .on_action(cx.listener(Self::on_sort_ascending))
800            .on_action(cx.listener(Self::on_sort_descending))
801            .on_action(cx.listener(Self::on_clear_sort))
802            .on_action(cx.listener(Self::on_clear_all_sorting))
803            .map(|this| Self::bind_selection_actions(this, cx))
804            // Expanding and collapsing.
805            .map(|this| Self::bind_disclosure_actions(this, cx))
806            // Copy.
807            .on_action(cx.listener(Self::on_copy))
808            .on_action(cx.listener(Self::on_copy_all))
809            // The grouping Actions and chip drags.
810            .map(|this| Self::bind_group_panel(this, cx))
811            // Editing and elements in cells.
812            .map(|this| Self::bind_edit_actions(this, cx))
813            .map(|this| Self::bind_cell_actions(this, cx))
814            .map(|this| Self::bind_link_actions(this, cx))
815            // Header drags drop anywhere on the grid, header or body.
816            .on_drag_move(cx.listener(Self::on_columns_drag_move))
817            .on_drag_move(cx.listener(Self::on_resize_drag_move))
818            .on_drop(cx.listener(Self::on_columns_drop))
819            .size_full()
820            .flex()
821            .flex_col()
822            .overflow_hidden()
823            .bg(theme.table)
824            .text_color(theme.foreground)
825            // The group panel, above the header.
826            .children(group_panel)
827            .child(header)
828            // The rows' wrapper, the source of row drags.
829            .child(self.render_rows(GridBody::new(cx.entity()), ghost_text, cx))
830            .child(scrollbars)
831            // Over the body, under the header; no hitbox of its own.
832            .children(progress);
833        // The menu layer is a sibling of the grid root, outside its key
834        // context: keys pressed in the menu (Space, Page Down, arrows a menu
835        // passes on) must not move the grid's selection. Its items dispatch
836        // to the grid's focus all the same (`action_context`).
837        div()
838            .size_full()
839            .child(root)
840            .children(menu)
841            .children(column_chooser)
842    }
843}
844
845impl<D: LiveGridDelegate> LiveGridState<D> {
846    /// The scrollbars over the data rows (`gpui_kit::base::Scrollbar`, as
847    /// gpui-kit's `DataTable`): vertical at their right edge, between the
848    /// pinned bands, which do not scroll; horizontal at their bottom edge,
849    /// under the scrolling zone. An overlay scrollbar takes the presses on
850    /// it, so the bands stay clear of both and a pinned row is clickable
851    /// across its height. Rendered by this view, so a thumb drag repaints
852    /// it.
853    fn render_scrollbars(&self) -> impl IntoElement {
854        let geometry = self
855            .columns
856            .geometry(self.body_bounds.size.width, self.scroll_x);
857        let scrolling = geometry.zone(ColumnZone::Scrolling);
858        let (left, width) = (scrolling.x, scrolling.width);
859        let bands = self.body_bands();
860        let top_band = bands.band(PinnedEdge::Top).size.height;
861        let bottom_band = bands.band(PinnedEdge::Bottom).size.height;
862        div()
863            .absolute()
864            // Below the group panel and header.
865            .top(self.body_top())
866            .left_0()
867            .right_0()
868            .bottom_0()
869            .child(
870                div()
871                    .absolute()
872                    .top(top_band)
873                    .right_0()
874                    .bottom(bottom_band)
875                    .w(Scrollbar::width())
876                    .child(
877                        Scrollbar::vertical(&self.scroll_handle)
878                            .viewport_from_layout()
879                            .max_fps(60),
880                    ),
881            )
882            .child(
883                div()
884                    .absolute()
885                    .left(left)
886                    .bottom(bottom_band)
887                    .w(width)
888                    .h(Scrollbar::width())
889                    .child(Scrollbar::horizontal(&self.scroll_handle).viewport_from_layout()),
890            )
891    }
892}