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}