Skip to main content

gpui_kit_grid/state/
projection.rs

1//! Keeping the display order in step with the store: the view (sort, filter,
2//! quick filter, grouping, expansion), snapshot pulls, throttled background
3//! jobs, the reorder hold, and what removals and new orders do to the
4//! selection, the focus and the scroll position.
5//!
6//! The pending work is a set: a rebuild takes every other work; otherwise
7//! one job runs `compact` → `aggregate` → `expand` over the shown
8//! projection. Data-driven work waits for the `min_rebuild_interval`
9//! throttle, and only a data-driven rebuild meets the reorder policy; a
10//! change of the expansion is a user action and is laid out at once, ahead
11//! of a data-driven rebuild that still waits. One job runs at a time.
12//!
13//! Every job that gives a projection stamps what changed in its summaries
14//! against the projection before it: a rebuild against the held order,
15//! else the shown one, unless it is for a new view or data set; a refresh
16//! against the projection it ran over. The stamps are carried in the job,
17//! off the UI thread (`Projection::carry_summary_stamps`).
18//!
19//! The coverage is the newest store version up to which every change is
20//! reflected in the order shown or held. A pulled snapshot whose changes
21//! need no work is covered at once; a finished job built from snapshot S
22//! that took all the data work covers the current snapshot if no work came
23//! after S, else S.
24//! [`LiveGridState::flush`] waits for it, and so does store progress that
25//! was cleared (`state/progress.rs`).
26
27use std::{sync::Arc, time::Duration};
28
29use async_channel::Sender;
30use gpui_kit::{AppContext as _, Context, SharedString, Task};
31use web_time::Instant;
32
33use super::{
34    ColumnOf, LiveGridDelegate, LiveGridEvent, LiveGridState, SelectionOrigin, SortOrigin,
35    events::GroupOrigin,
36    frame::PaintedLine,
37    grouping::{ExpansionRevision, same_structure},
38    scroll::ScrollReset,
39    stats::{profile_elapsed, profile_start},
40};
41use crate::{
42    engine::{Projection, ProjectionEngine, SortDirection, ViewSpec},
43    highlight::HighlightPolicy,
44    model::GridRow,
45    policy::{ReorderContext, ReorderDecision, ReorderPolicy},
46    quick_filter::QuickFilter,
47    store::{ChangeSet, StoreSnapshot},
48};
49
50/// What the projection needs before it matches the latest snapshot and the
51/// view's expansion: a set of work in which a rebuild takes every other.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub(super) enum PendingWork {
54    #[default]
55    None,
56    /// Work over the shown projection, in one job.
57    Refresh(Refresh),
58    /// Rows were inserted, values the order depends on changed, or the view
59    /// changed. `expand`: the expansion changed while a data-driven rebuild
60    /// waits; the lines are laid out first, as a user action.
61    Rebuild { expand: bool },
62}
63
64impl PendingWork {
65    const REBUILD: Self = Self::Rebuild { expand: false };
66    const COMPACT: Self = Self::Refresh(Refresh {
67        compact: true,
68        aggregate: false,
69        expand: false,
70    });
71    const AGGREGATE: Self = Self::Refresh(Refresh {
72        compact: false,
73        aggregate: true,
74        expand: false,
75    });
76    pub(super) const EXPAND: Self = Self::Refresh(Refresh::EXPAND);
77
78    /// This work and `other`: a rebuild takes the rest, but keeps a change
79    /// of the expansion apart.
80    pub(super) fn with(self, other: Self) -> Self {
81        match (self, other) {
82            (Self::None, work) | (work, Self::None) => work,
83            (Self::Rebuild { expand: a }, Self::Rebuild { expand: b }) => {
84                Self::Rebuild { expand: a || b }
85            }
86            (Self::Rebuild { expand }, Self::Refresh(refresh))
87            | (Self::Refresh(refresh), Self::Rebuild { expand }) => Self::Rebuild {
88                expand: expand || refresh.expand,
89            },
90            (Self::Refresh(a), Self::Refresh(b)) => Self::Refresh(Refresh {
91                compact: a.compact || b.compact,
92                aggregate: a.aggregate || b.aggregate,
93                expand: a.expand || b.expand,
94            }),
95        }
96    }
97}
98
99/// Jobs over the shown projection, run in this order by one job.
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub(super) struct Refresh {
102    /// Rows the projection holds were removed: drop them, recount groups.
103    compact: bool,
104    /// Values aggregates depend on changed, or rows were removed under
105    /// aggregates: recount them, in the held order too. Values only: no
106    /// reorder, no `ProjectionChange`.
107    aggregate: bool,
108    /// The expansion changed: lay the lines out again.
109    expand: bool,
110}
111
112impl Refresh {
113    const EXPAND: Self = Self {
114        compact: false,
115        aggregate: false,
116        expand: true,
117    };
118
119    /// Returns true if the job does work the data asked for, which the
120    /// throttle paces.
121    fn has_data_work(self) -> bool {
122        self.compact || self.aggregate
123    }
124
125    /// Returns true if the job changes the shown lines (an order is shown).
126    fn changes_lines(self) -> bool {
127        self.compact || self.expand
128    }
129
130    /// Runs the jobs over `shown`, in order, and stamps what changed in
131    /// the summaries against `shown` at `stamped`.
132    fn run<R: GridRow>(
133        self,
134        engine: &dyn ProjectionEngine<R>,
135        shown: &Projection,
136        snapshot: &StoreSnapshot<R>,
137        view: &ViewSpec<R>,
138        stamped: SummaryClock,
139    ) -> Projection {
140        let mut refreshed = self.compact.then(|| engine.compact(shown, snapshot));
141        if self.aggregate {
142            let aggregated = engine.aggregate(refreshed.as_ref().unwrap_or(shown), snapshot, view);
143            refreshed = Some(aggregated);
144        }
145        if self.expand {
146            let expanded = engine.expand(refreshed.as_ref().unwrap_or(shown), snapshot, view);
147            refreshed = Some(expanded);
148        }
149        let mut refreshed = refreshed.unwrap_or_else(|| shown.duplicate());
150        stamped.carry(&mut refreshed, shown);
151        refreshed
152    }
153}
154
155/// When and how a job stamps the summaries of its projection: the store
156/// clock at the job's start, in milliseconds since the snapshot's epoch,
157/// and the store's highlight policy, which drops stamps that expired.
158#[derive(Clone, Copy, Debug)]
159struct SummaryClock {
160    now_ms: u32,
161    policy: HighlightPolicy,
162}
163
164impl SummaryClock {
165    /// Stamps `projection`'s aggregates and captions that differ from
166    /// `previous`, and carries `previous`'s stamps that are still hot.
167    fn carry(self, projection: &mut Projection, previous: &Projection) {
168        projection.carry_summary_stamps(previous, self.now_ms, self.policy);
169    }
170}
171
172/// A job to start.
173#[derive(Clone, Copy, Debug)]
174enum Job {
175    Rebuild,
176    Refresh(Refresh),
177}
178
179/// What the pending work allows now.
180enum NextJob {
181    /// Nothing is pending.
182    Idle,
183    /// Data-driven work waits this long for the throttle.
184    Wait(Duration),
185    /// `job` may start; `rest` stays pending after it.
186    Start { job: Job, rest: PendingWork },
187}
188
189/// What [`LiveGridState::flush`] waited for.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub struct FlushOutcome {
192    store_version: u64,
193    row_count: usize,
194    order_held: bool,
195}
196
197impl FlushOutcome {
198    /// The store version every write before the flush is part of.
199    pub fn store_version(&self) -> u64 {
200        self.store_version
201    }
202
203    /// Data rows of the order shown when the flush resolved
204    /// (`Projection::row_count`): rows of collapsed groups count, pinned
205    /// rows do not.
206    pub fn row_count(&self) -> usize {
207        self.row_count
208    }
209
210    /// Returns true if the order that reflects the writes is held by the
211    /// [`ReorderPolicy`](crate::ReorderPolicy) and not shown yet: the
212    /// shown order, and [`Self::row_count`], are the old ones.
213    /// [`LiveGridState::apply_held_order`] shows it.
214    pub fn is_order_held(&self) -> bool {
215        self.order_held
216    }
217}
218
219/// The coverage of the order shown or held, and the flushes waiting for it.
220#[derive(Default)]
221pub(super) struct Coverage {
222    /// Every store change up to this version is reflected in the order
223    /// shown or held.
224    version: u64,
225    /// The version of the latest pulled snapshot whose changes needed work
226    /// (a rebuild, a compaction or a recount of aggregates).
227    last_work: u64,
228    /// `flush` calls waiting for `version` to reach theirs.
229    waiting: Vec<(u64, Sender<FlushOutcome>)>,
230}
231
232impl Coverage {
233    /// Returns true while a flush waits.
234    fn is_waiting(&self) -> bool {
235        !self.waiting.is_empty()
236    }
237
238    /// Returns true if a flush waits for a version the view already holds:
239    /// the work for it starts without the throttle, once.
240    fn lifts_throttle(&self, held_version: u64) -> bool {
241        self.waiting
242            .iter()
243            .any(|(version, _)| *version <= held_version)
244    }
245}
246
247/// A newer display order kept back by the reorder policy.
248pub(super) struct HeldOrder {
249    projection: Arc<Projection>,
250    /// The expansion its lines are laid out for: shown with an older one,
251    /// it is laid out again (`expand`).
252    revision: ExpansionRevision,
253    since: Instant,
254    _timer: Option<Task<()>>,
255}
256
257impl<D: LiveGridDelegate> LiveGridState<D> {
258    /// Returns true if a newer order is ready but held by the reorder policy.
259    pub fn is_order_held(&self) -> bool {
260        self.held.is_some()
261    }
262
263    /// Shows the held order now (for a "refresh order" control).
264    pub fn apply_held_order(&mut self, cx: &mut Context<Self>) {
265        // `install` releases the held order and emits the event.
266        if let Some((projection, revision)) = self
267            .held
268            .as_ref()
269            .map(|held| (held.projection.clone(), held.revision))
270        {
271            self.install(projection, ScrollReset::Keep, revision, cx);
272        }
273    }
274
275    /// Replaces the reorder policy at run time, as a "Live / Hold / Manual"
276    /// switch does; [`Self::with_reorder_policy`] sets it when the grid is
277    /// built. A held order is offered to the new policy at once, as when the
278    /// pointer leaves the grid, with the hold's start kept: switching to
279    /// [`LiveReorder`](crate::LiveReorder) shows it and emits
280    /// `OrderHeldChange { held: false }`. While the context menu is open the
281    /// built-in policies keep holding. With no order held only the policy
282    /// changes.
283    ///
284    /// ```
285    /// use gpui_kit::{App, Entity};
286    /// use gpui_kit_grid::{LiveGridDelegate, LiveGridState, LiveReorder, ManualReorder};
287    ///
288    /// /// A "Live / Manual" switch in a toolbar.
289    /// fn set_live<D: LiveGridDelegate>(grid: &Entity<LiveGridState<D>>, live: bool, cx: &mut App) {
290    ///     grid.update(cx, |grid, cx| {
291    ///         if live {
292    ///             grid.set_reorder_policy(LiveReorder, cx);
293    ///         } else {
294    ///             grid.set_reorder_policy(ManualReorder, cx);
295    ///         }
296    ///     });
297    /// }
298    /// ```
299    pub fn set_reorder_policy(&mut self, policy: impl ReorderPolicy, cx: &mut Context<Self>) {
300        self.reorder = Box::new(policy);
301        self.reconsider_held(cx);
302    }
303
304    /// Resolves once the order shown, or held by the reorder policy,
305    /// reflects every operation written to the store before the call: with
306    /// what it waited for, or `None` if the grid or the store is gone.
307    ///
308    /// For tests and for "select the first row after loading":
309    ///
310    /// ```ignore
311    /// writer.replace_all(rows);
312    /// let flushed = grid.update(cx, |grid, cx| grid.flush(cx));
313    /// cx.spawn(async move |_, cx| {
314    ///     if flushed.await.is_some() {
315    ///         grid.update(cx, |grid, cx| {
316    ///             let first = grid
317    ///                 .projection()
318    ///                 .rows()
319    ///                 .first()
320    ///                 .and_then(|id| grid.snapshot().row(*id))
321    ///                 .map(|row| row.key().clone());
322    ///             grid.select_key(first, cx);
323    ///         })
324    ///         .ok();
325    ///     }
326    /// })
327    /// .detach();
328    /// ```
329    ///
330    /// Call it after the writes it should wait for: a producer on another
331    /// thread awaits `GridWriter::flush` itself or tells the UI thread it
332    /// wrote. Operations buffered by an unfinished reload are not waited
333    /// for (`GridWriter::flush`). An order held by the reorder policy
334    /// resolves the flush with [`FlushOutcome::is_order_held`]; a new data
335    /// set (`replace_all`, a finished reload) is shown under any policy.
336    /// `ProjectionChange` of the order comes before the flush resolves.
337    ///
338    /// The work the flush waits for starts at once: without the
339    /// `min_rebuild_interval` throttle, and from the flush itself when the
340    /// grid is not painted (a hidden tab, a grid not in a window). So a
341    /// flush is not something to call on every tick: each call lifts the
342    /// throttle for the rebuild it waits for.
343    pub fn flush(&mut self, cx: &mut Context<Self>) -> Task<Option<FlushOutcome>> {
344        let written = self.store.writer().flush();
345        cx.spawn(async move |this, cx| {
346            let version = written.await?;
347            let (done, outcome) = async_channel::bounded(1);
348            this.update(cx, |state, cx| state.wait_for_coverage(version, done, cx))
349                .ok()?;
350            outcome.recv().await.ok()
351        })
352    }
353
354    /// Registers a flush of the store version `version` and starts the work
355    /// it waits for.
356    fn wait_for_coverage(
357        &mut self,
358        version: u64,
359        done: Sender<FlushOutcome>,
360        cx: &mut Context<Self>,
361    ) {
362        self.coverage.waiting.push((version, done));
363        self.resolve_flushes();
364        if self.coverage.is_waiting() {
365            self.ensure_projection(cx);
366        }
367    }
368
369    /// The order shown or held now covers `version`: flushes up to it
370    /// resolve, and store progress cleared up to it leaves.
371    fn cover(&mut self, version: u64, cx: &mut Context<Self>) {
372        self.coverage.version = self.coverage.version.max(version);
373        self.resolve_flushes();
374        if self.progress.release(self.coverage.version) {
375            cx.notify();
376        }
377    }
378
379    /// Resolves the flushes the coverage reached; drops the ones nobody
380    /// awaits any more.
381    fn resolve_flushes(&mut self) {
382        if !self.coverage.is_waiting() {
383            return;
384        }
385        let covered = self.coverage.version;
386        let row_count = self.projection.row_count();
387        let order_held = self.held.is_some();
388        self.coverage.waiting.retain(|(version, done)| {
389            if *version > covered {
390                return !done.is_closed();
391            }
392            let _ = done.try_send(FlushOutcome {
393                store_version: *version,
394                row_count,
395                order_held,
396            });
397            false
398        });
399    }
400
401    /// A job built from the snapshot of `version` finished. `covers`: its
402    /// order is shown or held and it took all the data work pending when it
403    /// started; not when it was dropped as stale or was laid out ahead of a
404    /// rebuild that still waits. With no work since, the order covers the
405    /// current snapshot; else that version. Pending work starts at once
406    /// while a flush waits, else from `render`.
407    fn job_done(&mut self, version: u64, covers: bool, cx: &mut Context<Self>) {
408        if self.pending == PendingWork::None && self.coverage.last_work <= version {
409            self.cover(self.snapshot.version(), cx);
410        } else if covers {
411            self.cover(version, cx);
412        }
413        if self.pending != PendingWork::None {
414            if self.coverage.is_waiting() {
415                self.ensure_projection(cx);
416            }
417            cx.notify();
418        }
419    }
420
421    /// Tracks the pointer over the body; leaving may release a held order.
422    pub(crate) fn set_pointer_inside(&mut self, inside: bool, cx: &mut Context<Self>) {
423        if self.pointer_inside == inside {
424            return;
425        }
426        self.pointer_inside = inside;
427        if !inside {
428            self.reconsider_held(cx);
429        }
430    }
431
432    /// Replaces the view (sort, filter, grouping or tree, aggregates) and
433    /// rebuilds at once. The new order is shown from the top, unless the
434    /// focused line (a row or a group caption) is on screen and still shown:
435    /// it keeps its screen line. The grid owns the quick filter
436    /// and the expansion: they stay, and those of `view` are replaced by
437    /// them ([`Self::set_quick_filter`], [`Self::set_expanded`]). Emits
438    /// `SortChange` (`SortOrigin::Programmatic`) if the sort differs, then
439    /// `GroupChange` (`GroupOrigin::Programmatic`) if the grouping or the
440    /// tree does.
441    pub fn set_view(&mut self, view: ViewSpec<D::Row>, cx: &mut Context<Self>) {
442        let sort_changed = view.sort() != self.view.sort();
443        let structure_changed = !same_structure(&view, &self.view);
444        let quick_filter = self.view.quick_filter().cloned();
445        let expansion = self.view.expansion().clone();
446        self.view = view
447            .with_quick_filter(quick_filter)
448            .with_expansion(expansion);
449        self.view_changed(
450            sort_changed.then_some(SortOrigin::Programmatic),
451            structure_changed.then_some(GroupOrigin::Programmatic),
452            cx,
453        );
454    }
455
456    /// The sort, most significant column first; empty for the insertion
457    /// order.
458    pub fn sort(&self) -> &[(ColumnOf<D>, SortDirection)] {
459        self.view.sort()
460    }
461
462    /// Replaces the sort and rebuilds at once (`SortChange` with
463    /// `SortOrigin::Programmatic`), shown as by [`Self::set_view`]: from the
464    /// top, or with the focused line on its screen line. Repeated columns are
465    /// dropped, the first one stays; the same sort again does nothing. Works
466    /// for columns that a header click does not sort.
467    pub fn set_sort(
468        &mut self,
469        sort: impl IntoIterator<Item = (ColumnOf<D>, SortDirection)>,
470        cx: &mut Context<Self>,
471    ) {
472        self.apply_sort(sort, SortOrigin::Programmatic, cx);
473    }
474
475    /// The one path of every sort change: header clicks, `set_sort`, and the
476    /// menu and layouts of later steps. Drops repeated columns (the first one
477    /// stays) and returns false, doing nothing, if the sort is unchanged.
478    /// Otherwise releases a held order (`OrderHeldChange { held: false }`),
479    /// emits `SortChange { sort, origin }` and rebuilds at once.
480    pub(crate) fn apply_sort(
481        &mut self,
482        sort: impl IntoIterator<Item = (ColumnOf<D>, SortDirection)>,
483        origin: SortOrigin,
484        cx: &mut Context<Self>,
485    ) -> bool {
486        let mut normalized: Vec<(ColumnOf<D>, SortDirection)> = Vec::new();
487        for (column, direction) in sort {
488            if !normalized.iter().any(|(sorted, _)| *sorted == column) {
489                normalized.push((column, direction));
490            }
491        }
492        if normalized == self.view.sort() {
493            return false;
494        }
495        self.view = std::mem::take(&mut self.view).with_sort(normalized);
496        self.view_changed(Some(origin), None, cx);
497        true
498    }
499
500    /// The quick filter text, trimmed; empty when the filter is off.
501    pub fn quick_filter(&self) -> &SharedString {
502        &self.quick_filter_text
503    }
504
505    /// Shows only rows whose display text contains the text, as the
506    /// [`QuickFilterMatcher`] decides (default [`ContainsAllTerms`]: every
507    /// whitespace-separated term, ignoring case, in some quick filter
508    /// column). Rebuilds at once, like any user action, and shows the new
509    /// order as [`Self::set_view`] does.
510    /// The text is trimmed; empty or blank text turns the filter off, and the
511    /// same trimmed text again does nothing.
512    ///
513    /// [`QuickFilterMatcher`]: crate::QuickFilterMatcher
514    /// [`ContainsAllTerms`]: crate::ContainsAllTerms
515    pub fn set_quick_filter(&mut self, text: impl Into<SharedString>, cx: &mut Context<Self>) {
516        let text = text.into();
517        let trimmed = text.trim();
518        if trimmed == &*self.quick_filter_text {
519            return;
520        }
521        self.quick_filter_text = if trimmed.len() == text.len() {
522            text.clone()
523        } else {
524            SharedString::from(trimmed.to_owned())
525        };
526        self.recompile_quick_filter();
527        self.view_changed(None, None, cx);
528    }
529
530    /// Compiles the quick filter text again into the view (new text, matcher
531    /// or columns). Does not rebuild.
532    pub(super) fn recompile_quick_filter(&mut self) {
533        let columns = self
534            .quick_filter_columns
535            .unwrap_or_else(|| self.columns.visible_set());
536        let filter = QuickFilter::new(
537            &self.quick_filter_text,
538            columns,
539            &*self.quick_filter_matcher,
540        )
541        .map(|filter| filter.with_number_formats(self.columns.number_formats()));
542        self.view = std::mem::take(&mut self.view).with_quick_filter(filter);
543    }
544
545    /// The visible columns changed: a quick filter over the default columns
546    /// now searches others, which is a user action. The scroll position
547    /// stays: the user changed the columns, not the view.
548    pub(super) fn quick_filter_columns_changed(&mut self, cx: &mut Context<Self>) {
549        if self.follow_visible_columns() {
550            self.restart_projection(cx);
551            cx.notify();
552        }
553    }
554
555    /// Compiles a quick filter over the default columns again for the
556    /// visible columns. Returns true if the view changed and needs a
557    /// rebuild; does not rebuild.
558    pub(super) fn follow_visible_columns(&mut self) -> bool {
559        if self.quick_filter_columns.is_some() || self.view.quick_filter().is_none() {
560            return false;
561        }
562        self.recompile_quick_filter();
563        true
564    }
565
566    /// A new view: rebuild at once and show it from the top, or with a
567    /// painted focused line on its screen line (`restore_line_anchor`).
568    /// Events in this order: `OrderHeldChange { held: false }` (if held),
569    /// then `SortChange` (with a sort change), then `GroupChange` (with a
570    /// change of the grouping or the tree), then later `ProjectionChange`.
571    pub(super) fn view_changed(
572        &mut self,
573        sort_changed: Option<SortOrigin>,
574        structure_changed: Option<GroupOrigin>,
575        cx: &mut Context<Self>,
576    ) {
577        self.restart_projection(cx);
578        // A new grouping or tree shows its first caption at the top unless
579        // the user had scrolled (`restore_line_anchor`).
580        self.pending_scroll_reset = self
581            .pending_scroll_reset
582            .max(if structure_changed.is_some() {
583                ScrollReset::NewStructure
584            } else {
585                ScrollReset::NewView
586            });
587        if let Some(origin) = sort_changed {
588            cx.emit(LiveGridEvent::SortChange {
589                sort: self.view.sort().into(),
590                origin,
591            });
592        }
593        if let Some(origin) = structure_changed {
594            cx.emit(LiveGridEvent::GroupChange { origin });
595        }
596        cx.notify();
597    }
598
599    /// Schedules a rebuild that starts in the next `render` regardless of the
600    /// throttle, skips the reorder hold, takes every other pending work (a
601    /// change of the expansion among it) and drops work for the old view and
602    /// a held order of it.
603    pub(super) fn restart_projection(&mut self, cx: &mut Context<Self>) {
604        self.pending = PendingWork::REBUILD;
605        self.pending_user_action = true;
606        self.last_rebuild_started = None;
607        // Work for the old view is useless now: dropping cancels it.
608        self.rebuild_task = None;
609        self.rebuild_timer = None;
610        if self.held.take().is_some() {
611            cx.emit(LiveGridEvent::OrderHeldChange { held: false });
612        }
613    }
614
615    pub(super) fn apply_snapshot(
616        &mut self,
617        snapshot: Arc<StoreSnapshot<D::Row>>,
618        changes: ChangeSet,
619        cx: &mut Context<Self>,
620    ) {
621        if changes.is_empty() && snapshot.version() == self.snapshot.version() {
622            return;
623        }
624        let started = profile_start();
625        let work = self.work_for(&changes);
626        let version = snapshot.version();
627        if work != PendingWork::None {
628            self.coverage.last_work = version;
629        }
630        if changes.columns().intersects(self.view.aggregate_columns()) {
631            self.aggregates_changed = true;
632        }
633        self.pending = self.pending.with(work);
634        // A new data set (or the first rows of an empty grid) is not a
635        // stream of changes: show it at once instead of after the throttle,
636        // and drop a rebuild of the old data still in flight.
637        if changes.is_reset() || (self.projection.is_empty() && changes.inserted() > 0) {
638            self.last_rebuild_started = None;
639            self.rebuild_task = None;
640            self.pending_user_action = true;
641        }
642        // A visible change: a painted row changed or was removed, or a
643        // painted fading row left the snapshot.
644        let visible = changes
645            .updated()
646            .iter()
647            .chain(changes.removed())
648            .any(|id| self.painted.iter().any(|line| line.row() == Some(*id)))
649            || changes
650                .faded()
651                .iter()
652                .any(|id| self.painted.contains(&PaintedLine::Ghost(*id)));
653
654        // The view often holds the last reference to the old snapshot:
655        // free its retired pages and rows off the UI thread. A projection
656        // built before a removal, or before a fading row left, may still
657        // hold its id: a compaction drops it when shown.
658        if !changes.removed().is_empty() || !changes.faded().is_empty() {
659            self.last_removal_version = version;
660        }
661        if changes.has_progress_change() {
662            self.progress.follow_store(snapshot.progress(), version);
663        }
664        let retired = std::mem::replace(&mut self.snapshot, snapshot);
665        self.stats.snapshots += 1;
666
667        // Removed keys leave the selection, read through the retired
668        // snapshot; a removed focus moves to its successor. A new data set
669        // keeps only the keys it has. O(removed) per pull.
670        let mark = self.selection.mark();
671        self.selection.remove_rows(changes.removed(), &retired);
672        if changes.is_reset() {
673            self.selection.retain_present(&self.snapshot);
674        } else if !changes.removed().is_empty() {
675            self.selection
676                .follow_removed(&self.projection, &retired, &self.snapshot);
677        }
678        let selection_changed = self.finish_change(mark, SelectionOrigin::DataRemoved, cx);
679        cx.background_spawn(async move { drop(retired) }).detach();
680        // The edited row may have changed or left the store.
681        self.follow_edit_rows(&changes, cx);
682
683        // Pinned rows are always on screen, and the progress may show or
684        // leave: any change of them repaints. A band change resizes the
685        // data rows' area.
686        if changes.has_pinned_change() {
687            self.clamp_scroll();
688        }
689        // Off-screen updates do not repaint: a grid fed 10k updates/s across
690        // 100k rows mostly changes rows nobody sees. Pending work starts in
691        // `render`, so it needs a render only once it may start; until the
692        // throttle allows it, one timer waits instead of a render per pull.
693        let mut notify = visible
694            || selection_changed
695            || changes.has_pinned_change()
696            || changes.has_progress_change();
697        if self.rebuild_task.is_none() {
698            match self.next_job(self.store.now()) {
699                NextJob::Idle => {}
700                NextJob::Start { .. } => notify = true,
701                NextJob::Wait(wait) => self.ensure_rebuild_timer(wait, cx),
702            }
703        }
704        // Changes that need no work are covered as soon as they are taken.
705        if self.pending == PendingWork::None && self.rebuild_task.is_none() {
706            self.cover(version, cx);
707        }
708        // While a flush waits, pending work starts here and not only from
709        // `render`: the grid may not be painted at all.
710        if self.coverage.is_waiting() {
711            self.ensure_projection(cx);
712        }
713        if notify {
714            cx.notify();
715        }
716        self.stats.last_pull = profile_elapsed(started);
717    }
718
719    /// The work a pulled change set needs. O(1) by column sets, plus
720    /// O(removed) to find removed rows the projection holds.
721    fn work_for(&self, changes: &ChangeSet) -> PendingWork {
722        let columns = changes.columns();
723        let removed = changes.removed();
724        let mut work = PendingWork::None;
725        // Inserted rows and changes of what orders, filters or groups rows
726        // can move any row.
727        if changes.has_insertions_or_reset() || columns.intersects(self.view.dependencies()) {
728            work = PendingWork::REBUILD;
729        }
730        // A new parent moves a tree row with its subtree, though no column
731        // may have changed.
732        if self.view.tree().is_some() && changes.has_parent_change() {
733            work = PendingWork::REBUILD;
734        }
735        // A removed tree row with children moves them to another parent.
736        if self.projection.is_tree()
737            && removed
738                .iter()
739                .any(|id| self.projection.branch(*id).is_some())
740        {
741            work = PendingWork::REBUILD;
742        }
743        // Removing rows the projection does not hold needs no work at all;
744        // removing rows it holds (shown, or in a collapsed group) needs a
745        // compaction, not a sort. A job in flight was built from an older
746        // snapshot and may still hold them.
747        if removed.iter().any(|id| self.projection.contains(*id))
748            || (self.rebuild_task.is_some() && !removed.is_empty())
749        {
750            work = work.with(self.removal_work());
751        }
752        // A fading row whose fade ended left the snapshot: the projection
753        // still holds its id, and a compaction drops it. It counted in no
754        // group and no aggregate, so nothing is recounted.
755        let faded = changes.faded();
756        if faded.iter().any(|id| self.projection.contains(*id))
757            || (self.rebuild_task.is_some() && !faded.is_empty())
758        {
759            work = work.with(PendingWork::COMPACT);
760        }
761        // Values of aggregates change, not the order.
762        if columns.intersects(self.view.aggregate_columns()) {
763            work = work.with(PendingWork::AGGREGATE);
764        }
765        work
766    }
767
768    /// The stamp clock of a job starting at `now` (store clock).
769    fn summary_clock(&self, now: Instant) -> SummaryClock {
770        SummaryClock {
771            now_ms: now
772                .saturating_duration_since(self.snapshot.epoch())
773                .as_millis() as u32,
774            policy: self.store.options().highlight(),
775        }
776    }
777
778    /// What removing rows the projection holds needs: a compaction, with the
779    /// aggregates recounted if the view has any; a rebuild if groups are
780    /// ordered by aggregates, which removals change.
781    fn removal_work(&self) -> PendingWork {
782        let view = &self.view;
783        if !view.group_by().is_empty() && !view.group_order().depends_on().is_empty() {
784            PendingWork::REBUILD
785        } else if view.aggregates().is_empty() {
786            PendingWork::COMPACT
787        } else {
788            PendingWork::COMPACT.with(PendingWork::AGGREGATE)
789        }
790    }
791
792    /// Returns true if the view groups rows or builds a tree: only then has
793    /// it lines to lay out by the expansion.
794    pub(super) fn has_structure(&self) -> bool {
795        !self.view.group_by().is_empty() || self.view.tree().is_some()
796    }
797
798    /// What the pending work allows at `now`. A rebuild for a user action
799    /// starts at once (`restart_projection` cleared the throttle) and takes
800    /// everything; a change of the expansion starts at once, with the other
801    /// work over the shown projection, or alone ahead of a data-driven
802    /// rebuild, which may be throttled and held; other data-driven work
803    /// waits for the throttle.
804    fn next_job(&self, now: Instant) -> NextJob {
805        let start = |job, rest| NextJob::Start { job, rest };
806        let throttled = |job| match self.throttle_wait(now) {
807            Some(wait) => NextJob::Wait(wait),
808            None => start(job, PendingWork::None),
809        };
810        match self.pending {
811            PendingWork::None => NextJob::Idle,
812            PendingWork::Rebuild { expand: true } if !self.pending_user_action => {
813                start(Job::Refresh(Refresh::EXPAND), PendingWork::REBUILD)
814            }
815            PendingWork::Rebuild { .. } => throttled(Job::Rebuild),
816            PendingWork::Refresh(work) if work.expand => {
817                start(Job::Refresh(work), PendingWork::None)
818            }
819            PendingWork::Refresh(work) => throttled(Job::Refresh(work)),
820        }
821    }
822
823    /// How long data-driven work must still wait, or `None` if it may start.
824    /// A flush waiting for a snapshot the view holds lifts the throttle.
825    fn throttle_wait(&self, now: Instant) -> Option<Duration> {
826        if self.coverage.lifts_throttle(self.snapshot.version()) {
827            return None;
828        }
829        let started = self.last_rebuild_started?;
830        let elapsed = now.saturating_duration_since(started);
831        (elapsed < self.tuning.min_rebuild_interval)
832            .then(|| self.tuning.min_rebuild_interval - elapsed)
833    }
834
835    fn ensure_rebuild_timer(&mut self, wait: Duration, cx: &mut Context<Self>) {
836        if self.rebuild_timer.is_some() {
837            return;
838        }
839        self.rebuild_timer = Some(cx.spawn(async move |this, cx| {
840            cx.background_executor().timer(wait).await;
841            let _ = this.update(cx, |state, cx| {
842                state.rebuild_timer = None;
843                if state.coverage.is_waiting() {
844                    state.ensure_projection(cx);
845                }
846                cx.notify();
847            });
848        }));
849    }
850
851    /// Starts the pending work in the background. Called from `render`, so a
852    /// grid that is not painted does no sorting at all; and while a flush
853    /// waits, from the pull, the timer and the flush.
854    pub(super) fn ensure_projection(&mut self, cx: &mut Context<Self>) {
855        if self.pending == PendingWork::None || self.rebuild_task.is_some() {
856            return;
857        }
858        let now = self.store.now();
859        match self.next_job(now) {
860            NextJob::Idle => {}
861            NextJob::Wait(wait) => self.ensure_rebuild_timer(wait, cx),
862            NextJob::Start { job, rest } => {
863                self.pending = rest;
864                match job {
865                    Job::Rebuild => self.start_rebuild(now, cx),
866                    Job::Refresh(work) => {
867                        self.start_refresh(work, rest == PendingWork::None, now, cx)
868                    }
869                }
870            }
871        }
872    }
873
874    /// Rebuilds the view from the snapshot; the reorder policy decides on
875    /// the result unless it is for a user action.
876    fn start_rebuild(&mut self, now: Instant, cx: &mut Context<Self>) {
877        let user_action = std::mem::take(&mut self.pending_user_action);
878        // A rebuild takes in an aggregate job; if its order is held, the
879        // shown order still needs one (`offer`).
880        let aggregates_changed = std::mem::take(&mut self.aggregates_changed);
881        // Copied, not taken: a rebuild cancelled by newer work must leave the
882        // scroll reset to the rebuild that replaces it. A view change always
883        // cancels the rebuild in flight, so a finished one never predates it.
884        let scroll_reset = self.pending_scroll_reset;
885        self.last_rebuild_started = Some(now);
886        let snapshot = self.snapshot.clone();
887        let job_version = snapshot.version();
888        let revision = self.expansion_revision;
889        let view = self.view.clone();
890        let engine = self.engine.clone();
891        // Stamps against the newest projection of the view; none for a new
892        // view or data set: the view changed, not the data.
893        let previous = (!user_action).then(|| {
894            self.held
895                .as_ref()
896                .map_or_else(|| self.projection.clone(), |held| held.projection.clone())
897        });
898        let stamped = self.summary_clock(now);
899        let (done, result) = async_channel::bounded(1);
900        let job = self.runner.run(
901            Box::new(move || {
902                let started = profile_start();
903                let mut projection = engine.rebuild(&snapshot, &view);
904                if let Some(previous) = previous {
905                    stamped.carry(&mut projection, &previous);
906                }
907                let _ = done.try_send((projection, profile_elapsed(started)));
908            }),
909            cx,
910        );
911        self.rebuild_task = Some(cx.spawn(async move |this, cx| {
912            job.await;
913            let Ok((projection, elapsed)) = result.recv().await else {
914                return;
915            };
916            let _ = this.update(cx, |state, cx| {
917                state.rebuild_task = None;
918                state.stats.rebuilds += 1;
919                state.stats.last_rebuild = elapsed;
920                state.stats.max_rebuild = state.stats.max_rebuild.max(elapsed);
921                state.offer(
922                    Arc::new(projection),
923                    user_action,
924                    scroll_reset,
925                    revision,
926                    cx,
927                );
928                // Values are not held: with the order held, the shown
929                // order's aggregates are recounted by a job of their own.
930                if aggregates_changed && state.held.is_some() {
931                    state.pending = state.pending.with(PendingWork::AGGREGATE);
932                    cx.notify();
933                }
934                state.job_done(job_version, true, cx);
935            });
936        }));
937    }
938
939    /// Runs `work` over the shown projection, and recounts the aggregates of
940    /// a held order in the same job. Nothing here reorders, so the reorder
941    /// policy is not asked: a held order stays held. `covers`: the job takes
942    /// all the data work pending (see [`Self::job_done`]).
943    fn start_refresh(&mut self, work: Refresh, covers: bool, now: Instant, cx: &mut Context<Self>) {
944        if work.has_data_work() {
945            self.last_rebuild_started = Some(now);
946        }
947        if work.aggregate {
948            self.aggregates_changed = false;
949        }
950        let snapshot = self.snapshot.clone();
951        let job_version = snapshot.version();
952        // The lines are laid out for the view's expansion if the job lays
953        // them out, else they stay those of the shown projection.
954        let revision = if work.expand {
955            self.expansion_revision
956        } else {
957            self.shown_revision
958        };
959        let shown = self.projection.clone();
960        let held = self
961            .held
962            .as_ref()
963            .filter(|_| work.aggregate)
964            .map(|held| held.projection.clone());
965        let (shown_base, held_base) = (shown.clone(), held.clone());
966        let view = self.view.clone();
967        let engine = self.engine.clone();
968        let stamped = self.summary_clock(now);
969        let (done, result) = async_channel::bounded(1);
970        let job = self.runner.run(
971            Box::new(move || {
972                let refreshed = work.run(&*engine, &shown, &snapshot, &view, stamped);
973                // The held order keeps its store version: a compaction it may
974                // need when shown is found by that version.
975                let held = held.map(|held| {
976                    let mut aggregated = engine
977                        .aggregate(&held, &snapshot, &view)
978                        .with_store_version(held.store_version());
979                    stamped.carry(&mut aggregated, &held);
980                    aggregated
981                });
982                let _ = done.try_send((refreshed, held));
983            }),
984            cx,
985        );
986        self.rebuild_task = Some(cx.spawn(async move |this, cx| {
987            job.await;
988            let Ok((refreshed, held)) = result.recv().await else {
989                return;
990            };
991            let _ = this.update(cx, |state, cx| {
992                state.rebuild_task = None;
993                if let (Some(projection), Some(base)) = (held, held_base) {
994                    state.refresh_held(projection, &base);
995                }
996                let applied = Arc::ptr_eq(&state.projection, &shown_base);
997                if applied {
998                    state.finish_refresh(Arc::new(refreshed), work, revision, cx);
999                } else {
1000                    // A held order was applied meanwhile: this is the old
1001                    // order. The work stays pending for the shown one
1002                    // (`show` scheduled a compaction it needs), and `render`
1003                    // starts it.
1004                    state.pending = state.pending.with(PendingWork::Refresh(work));
1005                    cx.notify();
1006                }
1007                state.job_done(job_version, applied && covers, cx);
1008            });
1009        }));
1010    }
1011
1012    /// Shows the result of `work` over the shown projection.
1013    fn finish_refresh(
1014        &mut self,
1015        projection: Arc<Projection>,
1016        work: Refresh,
1017        revision: ExpansionRevision,
1018        cx: &mut Context<Self>,
1019    ) {
1020        if work.compact {
1021            self.stats.compactions += 1;
1022        }
1023        if work.changes_lines() {
1024            // Removals and a new expansion never reorder: no reason to hold,
1025            // and a newer order held by the reorder policy stays held.
1026            self.show(projection, ScrollReset::Keep, revision, cx);
1027        } else {
1028            // Fresh aggregates over the same lines: values, not an order, so
1029            // no `ProjectionChange`.
1030            self.projection = projection;
1031            cx.notify();
1032        }
1033    }
1034
1035    /// Puts recounted aggregates into the held order, if it is still the
1036    /// one they were recounted from.
1037    fn refresh_held(&mut self, projection: Projection, base: &Arc<Projection>) {
1038        if let Some(held) = &mut self.held
1039            && Arc::ptr_eq(&held.projection, base)
1040        {
1041            held.projection = Arc::new(projection);
1042        }
1043    }
1044
1045    /// Asks the reorder policy whether a new order may replace the shown one.
1046    /// The order of a new view is shown from the top (`scroll_reset`), or
1047    /// with a painted focused row on its line.
1048    fn offer(
1049        &mut self,
1050        projection: Arc<Projection>,
1051        user_action: bool,
1052        scroll_reset: ScrollReset,
1053        revision: ExpansionRevision,
1054        cx: &mut Context<Self>,
1055    ) {
1056        let now = self.store.now();
1057        let since = self.held.as_ref().map_or(now, |held| held.since);
1058        let context = self.reorder_context(now, since, user_action);
1059        match self.reorder.decide(&context) {
1060            ReorderDecision::Apply => {
1061                if scroll_reset.is_reset() {
1062                    self.pending_scroll_reset = ScrollReset::Keep;
1063                }
1064                self.install(projection, scroll_reset, revision, cx)
1065            }
1066            ReorderDecision::Hold => self.hold(projection, revision, since, None, cx),
1067            ReorderDecision::HoldUntil(deadline) => {
1068                let wait = deadline.saturating_duration_since(now);
1069                let timer = cx.spawn(async move |this, cx| {
1070                    cx.background_executor().timer(wait).await;
1071                    let _ = this.update(cx, |state, cx| state.reconsider_held(cx));
1072                });
1073                self.hold(projection, revision, since, Some(timer), cx);
1074            }
1075        }
1076    }
1077
1078    fn hold(
1079        &mut self,
1080        projection: Arc<Projection>,
1081        revision: ExpansionRevision,
1082        since: Instant,
1083        timer: Option<Task<()>>,
1084        cx: &mut Context<Self>,
1085    ) {
1086        let was_held = self.held.is_some();
1087        self.held = Some(HeldOrder {
1088            projection,
1089            revision,
1090            since,
1091            _timer: timer,
1092        });
1093        if !was_held {
1094            cx.emit(LiveGridEvent::OrderHeldChange { held: true });
1095            // The header shows that a newer order is waiting.
1096            cx.notify();
1097        }
1098    }
1099
1100    /// What the reorder policy is told: the clock, the start of the hold,
1101    /// the pointer over the rows, the source of the order, the open menu
1102    /// and the shown tooltip.
1103    fn reorder_context(&self, now: Instant, since: Instant, user_action: bool) -> ReorderContext {
1104        ReorderContext::new(now, since, self.is_pointer_over_rows(), user_action)
1105            .with_menu_open(self.is_menu_open())
1106            .with_tooltip_shown(self.tooltip.is_shown())
1107            // The built-in policies hold while editing; a policy may hold
1108            // while commits wait.
1109            .with_editing(self.is_editing())
1110            .with_pending_edits(self.pending_edit_count() > 0)
1111            // A policy may hold while the chooser is open.
1112            .with_column_chooser_open(self.is_column_chooser_open())
1113            // A policy may hold while a drag is over the rows.
1114            .with_drag_over(self.drag.is_over())
1115    }
1116
1117    /// Whether the pointer hovers the rows, as the reorder policy sees it:
1118    /// never during a column gesture, or dragging a header over the body
1119    /// would hold the order.
1120    fn is_pointer_over_rows(&self) -> bool {
1121        self.pointer_inside && !self.is_column_gesture_active()
1122    }
1123
1124    /// Re-asks the policy about a held order (pointer left, deadline passed,
1125    /// a column gesture started, the context menu closed).
1126    pub(super) fn reconsider_held(&mut self, cx: &mut Context<Self>) {
1127        if let Some(held) = self.held.take() {
1128            let since = held.since;
1129            // Keep `since` so a hold is bounded from its start, not restarted.
1130            self.held = Some(HeldOrder {
1131                projection: held.projection.clone(),
1132                revision: held.revision,
1133                since,
1134                _timer: None,
1135            });
1136            let now = self.store.now();
1137            let context = self.reorder_context(now, since, false);
1138            match self.reorder.decide(&context) {
1139                ReorderDecision::Apply => {
1140                    self.install(held.projection, ScrollReset::Keep, held.revision, cx)
1141                }
1142                ReorderDecision::Hold => {}
1143                ReorderDecision::HoldUntil(deadline) => {
1144                    let wait = deadline.saturating_duration_since(now);
1145                    let timer = cx.spawn(async move |this, cx| {
1146                        cx.background_executor().timer(wait).await;
1147                        let _ = this.update(cx, |state, cx| state.reconsider_held(cx));
1148                    });
1149                    if let Some(held) = &mut self.held {
1150                        held._timer = Some(timer);
1151                    }
1152                }
1153            }
1154        }
1155    }
1156
1157    /// Shows a new display order in place of the shown one and of a held one.
1158    fn install(
1159        &mut self,
1160        projection: Arc<Projection>,
1161        scroll_reset: ScrollReset,
1162        revision: ExpansionRevision,
1163        cx: &mut Context<Self>,
1164    ) {
1165        if self.held.take().is_some() {
1166            cx.emit(LiveGridEvent::OrderHeldChange { held: false });
1167        }
1168        self.show(projection, scroll_reset, revision, cx);
1169    }
1170
1171    /// Shows `projection`: the one place `ProjectionChange` is emitted, once
1172    /// per order shown, never for a held one. A projection built from an
1173    /// older snapshot may hold rows removed since: a compaction follows to
1174    /// drop them. Lines laid out for an older expansion (`revision`, a held
1175    /// order or a rebuild that started before the change) are laid out again.
1176    ///
1177    /// The scroll anchor keeps its screen line: a toggled caption or tree
1178    /// row when the lines of its newest expansion are shown, else the
1179    /// painted focused line, else the first painted line. A focus or an
1180    /// anchor on a row the new order leaves out moves to its successor; a
1181    /// focused caption whose group is gone moves to the line at its place,
1182    /// and a focused line a collapsed group or tree row now hides moves to
1183    /// its caption or row (`FocusChange` with `Filtered`, after
1184    /// `ProjectionChange`).
1185    fn show(
1186        &mut self,
1187        projection: Arc<Projection>,
1188        scroll_reset: ScrollReset,
1189        revision: ExpansionRevision,
1190        cx: &mut Context<Self>,
1191    ) {
1192        let row_count = projection.row_count();
1193        let store_version = projection.store_version();
1194        if store_version < self.last_removal_version {
1195            // Which removed rows it holds is not known here: in a tree, one
1196            // may have had children, which move.
1197            let work = if projection.is_tree() {
1198                PendingWork::REBUILD
1199            } else {
1200                self.removal_work()
1201            };
1202            self.pending = self.pending.with(work);
1203        }
1204        self.shown_revision = revision;
1205        if revision != self.expansion_revision && self.has_structure() {
1206            self.pending = self.pending.with(PendingWork::EXPAND);
1207        }
1208        let toggled = if revision == self.expansion_revision {
1209            self.expand_anchor.take()
1210        } else {
1211            None
1212        };
1213        let anchor = toggled.or_else(|| self.line_anchor());
1214        let previous = std::mem::replace(&mut self.projection, projection);
1215        let mark = self.selection.mark();
1216        self.selection
1217            .follow_filtered(&previous, &self.projection, &self.snapshot);
1218        self.follow_shown_lines(&previous);
1219        self.restore_line_anchor(anchor, &previous, scroll_reset);
1220        self.clamp_scroll();
1221        cx.emit(LiveGridEvent::ProjectionChange {
1222            row_count,
1223            store_version,
1224        });
1225        self.finish_change(mark, SelectionOrigin::Filtered, cx);
1226        // An edited row the new order hides.
1227        self.follow_edit_shown(cx);
1228        cx.notify();
1229    }
1230}