Skip to main content

gpui_kit_grid/engine/
display.rs

1//! The shown lines of a projection and what describes them: display
2//! rows, group identity, the group table, aggregates and the expansion.
3//! No GPUI types.
4//!
5//! A flat projection keeps no list of lines: its lines are its rows at
6//! depth 0, so a view without grouping or a tree pays for none of this. A
7//! grouped or tree projection lists its lines ([`Lines::Built`]); grouping
8//! and the tree build them with the constructors here.
9
10use std::{
11    fmt,
12    hash::Hash,
13    iter::{self, FusedIterator},
14    ops::Range,
15    slice,
16    sync::{Arc, OnceLock},
17};
18
19use rustc_hash::{FxHashMap, FxHashSet};
20use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
21
22use super::{Projection, ViewSpec, expand, places, shared_place, tree::TreeData};
23use crate::{
24    highlight::{ChangeDirection, HighlightPolicy, RowChange, Stamp},
25    model::{ColumnId, ColumnIx, ColumnSet, GridRow, OwnedValue},
26    store::{RowId, StoreSnapshot},
27};
28
29/// A group's position in one projection's group table. Transient, like a
30/// display position: the identity of a group is its [`GroupPath`].
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub struct GroupId(u32);
33
34impl GroupId {
35    pub(crate) fn new(ix: usize) -> Self {
36        Self(ix as u32)
37    }
38
39    /// The position in the group table.
40    pub fn ix(self) -> usize {
41        self.0 as usize
42    }
43}
44
45/// One line of the shown order.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum DisplayRow {
49    /// A data row; `depth` is its tree depth, or the number of group levels
50    /// above it.
51    Data {
52        /// The row.
53        row: RowId,
54        /// How deep it is.
55        depth: u16,
56    },
57    /// The caption of a group.
58    GroupCaption {
59        /// The group.
60        group: GroupId,
61        /// The group's depth: 0 for a top-level group.
62        depth: u16,
63    },
64    /// The footer of an expanded group.
65    GroupFooter {
66        /// The group.
67        group: GroupId,
68        /// The group's depth: 0 for a top-level group.
69        depth: u16,
70    },
71}
72
73impl DisplayRow {
74    /// How deep the line is.
75    pub fn depth(self) -> u16 {
76        match self {
77            Self::Data { depth, .. }
78            | Self::GroupCaption { depth, .. }
79            | Self::GroupFooter { depth, .. } => depth,
80        }
81    }
82
83    /// The data row, for a data line.
84    pub fn row(self) -> Option<RowId> {
85        match self {
86            Self::Data { row, .. } => Some(row),
87            Self::GroupCaption { .. } | Self::GroupFooter { .. } => None,
88        }
89    }
90
91    /// The group, for a caption or a footer.
92    pub fn group(self) -> Option<GroupId> {
93        match self {
94            Self::GroupCaption { group, .. } | Self::GroupFooter { group, .. } => Some(group),
95            Self::Data { .. } => None,
96        }
97    }
98}
99
100/// A group's identity: the id of each level (a column's [`ColumnId`] or a
101/// `GroupKey` id) with the group's key value there, from the top level
102/// down. Never positions, so a group keeps its identity across rebuilds,
103/// sorts and snapshots.
104///
105/// The empty path is the root: it names no group and has no caption.
106/// Cloning is O(1).
107#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
108pub struct GroupPath(Arc<[(ColumnId, OwnedValue)]>);
109
110impl GroupPath {
111    /// The path of these levels, the top level first.
112    pub fn new(levels: impl IntoIterator<Item = (ColumnId, OwnedValue)>) -> Self {
113        Self(levels.into_iter().collect())
114    }
115
116    /// The depth of the group the path names: 0 for a top-level group, one
117    /// more per level below. The root reads 0 too; [`Self::is_root`] tells
118    /// it apart.
119    pub fn depth(&self) -> u16 {
120        self.0.len().saturating_sub(1) as u16
121    }
122
123    /// Returns true for the empty path, the root.
124    pub fn is_root(&self) -> bool {
125        self.0.is_empty()
126    }
127
128    /// The levels, the top level first.
129    pub fn levels(&self) -> &[(ColumnId, OwnedValue)] {
130        &self.0
131    }
132
133    /// The id of the level at `depth`.
134    pub fn level_id(&self, depth: u16) -> Option<&ColumnId> {
135        self.0.get(usize::from(depth)).map(|(id, _)| id)
136    }
137
138    /// The key value at `depth`.
139    pub fn value(&self, depth: u16) -> Option<&OwnedValue> {
140        self.0.get(usize::from(depth)).map(|(_, value)| value)
141    }
142
143    /// The path of the enclosing group: the root for a top-level group,
144    /// `None` for the root.
145    pub fn parent(&self) -> Option<GroupPath> {
146        let (_, parent) = self.0.split_last()?;
147        Some(Self(parent.into()))
148    }
149
150    /// Returns true if `other` is this path or one of its ancestors.
151    pub fn starts_with(&self, other: &GroupPath) -> bool {
152        self.0.starts_with(&other.0)
153    }
154}
155
156/// How deep groups and tree rows are expanded by default. The groups and
157/// rows the user toggled against it are kept apart ([`Expansion`]).
158///
159/// Serialized as `"all"`, `"captions"` or the number of levels.
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum ExpandDepth {
163    /// Everything is expanded: the default.
164    #[default]
165    All,
166    /// The groups and tree rows of the first `n` levels are expanded, the
167    /// deeper ones collapsed: `Levels(0)` collapses everything.
168    Levels(u16),
169    /// Every group level but the last is expanded: captions only, no data
170    /// rows. A tree has no captions: there it is `All`.
171    Captions,
172}
173
174impl ExpandDepth {
175    /// Whether a group of `depth` in a grouping of `levels` levels is
176    /// expanded by default.
177    fn expands_group(self, depth: u16, levels: usize) -> bool {
178        match self {
179            Self::All => true,
180            Self::Levels(n) => depth < n,
181            Self::Captions => usize::from(depth) + 1 < levels,
182        }
183    }
184
185    /// Whether a tree row of `depth` is expanded by default.
186    fn expands_row(self, depth: u16) -> bool {
187        match self {
188            Self::All | Self::Captions => true,
189            Self::Levels(n) => depth < n,
190        }
191    }
192}
193
194impl Serialize for ExpandDepth {
195    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
196        match self {
197            Self::All => serializer.serialize_str("all"),
198            Self::Levels(levels) => serializer.serialize_u16(*levels),
199            Self::Captions => serializer.serialize_str("captions"),
200        }
201    }
202}
203
204impl<'de> Deserialize<'de> for ExpandDepth {
205    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
206        struct DepthVisitor;
207
208        impl de::Visitor<'_> for DepthVisitor {
209            type Value = ExpandDepth;
210
211            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
212                formatter.write_str(r#""all", "captions" or a number of levels"#)
213            }
214
215            fn visit_str<E: de::Error>(self, value: &str) -> Result<ExpandDepth, E> {
216                match value {
217                    "all" => Ok(ExpandDepth::All),
218                    "captions" => Ok(ExpandDepth::Captions),
219                    _ => Err(E::invalid_value(de::Unexpected::Str(value), &self)),
220                }
221            }
222
223            fn visit_u64<E: de::Error>(self, value: u64) -> Result<ExpandDepth, E> {
224                u16::try_from(value)
225                    .map(ExpandDepth::Levels)
226                    .map_err(|_| E::invalid_value(de::Unexpected::Unsigned(value), &self))
227            }
228
229            fn visit_i64<E: de::Error>(self, value: i64) -> Result<ExpandDepth, E> {
230                u64::try_from(value)
231                    .map_err(|_| E::invalid_value(de::Unexpected::Signed(value), &self))
232                    .and_then(|value| self.visit_u64(value))
233            }
234        }
235
236        deserializer.deserialize_any(DepthVisitor)
237    }
238}
239
240/// The expansion a projection is laid out for: the default depth, and the
241/// groups and tree rows the user toggled against it. The grid owns it, as
242/// it owns the quick filter, and sets it into each view
243/// ([`ViewSpec::expansion`]).
244///
245/// Toggles are kept by [`GroupPath`] and by row key, so they survive
246/// rebuilds and a change of grouping: going back to an earlier grouping
247/// brings its expansion back. Cloning is O(1).
248pub struct Expansion<K> {
249    depth: ExpandDepth,
250    /// Groups toggled against `depth`; `None` for none.
251    groups: Option<Arc<FxHashSet<GroupPath>>>,
252    /// Tree rows toggled against `depth`; `None` for none.
253    rows: Option<Arc<FxHashSet<K>>>,
254}
255
256impl<K> Clone for Expansion<K> {
257    fn clone(&self) -> Self {
258        Self {
259            depth: self.depth,
260            groups: self.groups.clone(),
261            rows: self.rows.clone(),
262        }
263    }
264}
265
266impl<K> Default for Expansion<K> {
267    fn default() -> Self {
268        Self {
269            depth: ExpandDepth::default(),
270            groups: None,
271            rows: None,
272        }
273    }
274}
275
276impl<K: fmt::Debug> fmt::Debug for Expansion<K> {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        f.debug_struct("Expansion")
279            .field("depth", &self.depth)
280            .field("groups", &self.groups.as_deref())
281            .field("rows", &self.rows.as_deref())
282            .finish()
283    }
284}
285
286impl<K: Eq + Hash> PartialEq for Expansion<K> {
287    fn eq(&self, other: &Self) -> bool {
288        fn same<T: Eq + Hash>(a: Option<&FxHashSet<T>>, b: Option<&FxHashSet<T>>) -> bool {
289            match (a, b) {
290                (Some(a), Some(b)) => a == b,
291                (Some(set), None) | (None, Some(set)) => set.is_empty(),
292                (None, None) => true,
293            }
294        }
295        self.depth == other.depth
296            && same(self.groups.as_deref(), other.groups.as_deref())
297            && same(self.rows.as_deref(), other.rows.as_deref())
298    }
299}
300
301impl<K: Eq + Hash> Eq for Expansion<K> {}
302
303/// Defines a method that is public with `test-support`, so engine tests and
304/// benchmarks outside the crate can lay groups out for an expansion, and
305/// crate-visible otherwise: the grid owns the expansion.
306macro_rules! pub_for_tests {
307    ($(#[$meta:meta])* fn $($rest:tt)*) => {
308        $(#[$meta])*
309        #[cfg(any(test, feature = "test-support"))]
310        pub fn $($rest)*
311
312        $(#[$meta])*
313        #[cfg(not(any(test, feature = "test-support")))]
314        pub(crate) fn $($rest)*
315    };
316}
317
318impl<K: Clone + Eq + Hash> Expansion<K> {
319    pub_for_tests! {
320        /// `depth` with nothing toggled.
321        fn new(depth: ExpandDepth) -> Self {
322            Self {
323                depth,
324                ..Self::default()
325            }
326        }
327    }
328
329    pub_for_tests! {
330        /// Expands or collapses the group at `path` in a grouping of `levels`
331        /// levels: toggled when that differs from the default depth.
332        fn with_group(mut self, path: GroupPath, expanded: bool, levels: usize) -> Self {
333            let toggled = expanded != self.depth.expands_group(path.depth(), levels);
334            toggle(&mut self.groups, path, toggled);
335            self
336        }
337    }
338
339    pub_for_tests! {
340        /// Expands or collapses the tree row with `key` at `depth`: toggled
341        /// when that differs from the default depth.
342        fn with_row(mut self, key: K, expanded: bool, depth: u16) -> Self {
343            let toggled = expanded != self.depth.expands_row(depth);
344            toggle(&mut self.rows, key, toggled);
345            self
346        }
347    }
348
349    /// The groups toggled against the default depth, in no order.
350    pub(crate) fn toggled_groups(&self) -> impl Iterator<Item = &GroupPath> {
351        self.groups.iter().flat_map(|groups| groups.iter())
352    }
353
354    /// The tree rows toggled against the default depth, in no order.
355    #[cfg_attr(
356        not(test),
357        allow(dead_code, reason = "a tree's expansion is not saved in layouts (9.4)")
358    )]
359    pub(crate) fn toggled_rows(&self) -> impl Iterator<Item = &K> {
360        self.rows.iter().flat_map(|rows| rows.iter())
361    }
362}
363
364/// Adds `item` to a set of toggles or removes it, cloning a shared set.
365fn toggle<T: Clone + Eq + Hash>(set: &mut Option<Arc<FxHashSet<T>>>, item: T, toggled: bool) {
366    if toggled {
367        Arc::make_mut(set.get_or_insert_with(Default::default)).insert(item);
368    } else if let Some(items) = set {
369        if items.contains(&item) {
370            Arc::make_mut(items).remove(&item);
371        }
372    }
373}
374
375impl<K: Eq + Hash> Expansion<K> {
376    /// The default depth.
377    pub fn depth(&self) -> ExpandDepth {
378        self.depth
379    }
380
381    /// Whether the group at `path`, in a grouping of `levels` levels, is
382    /// expanded: by the default depth, inverted if the user toggled it.
383    pub fn is_group_expanded(&self, path: &GroupPath, levels: usize) -> bool {
384        let toggled = self
385            .groups
386            .as_ref()
387            .is_some_and(|groups| groups.contains(path));
388        self.depth.expands_group(path.depth(), levels) != toggled
389    }
390
391    /// Whether the tree row with `key` at `depth` is expanded: by the
392    /// default depth (`Captions` is `All` in a tree), inverted if the user
393    /// toggled it.
394    pub fn is_row_expanded(&self, key: &K, depth: u16) -> bool {
395        let toggled = self.rows.as_ref().is_some_and(|rows| rows.contains(key));
396        self.depth.expands_row(depth) != toggled
397    }
398}
399
400impl<R: GridRow> ViewSpec<R> {
401    /// The expansion the view is laid out for. The grid sets it: groups and
402    /// tree rows are expanded and collapsed through the grid's state.
403    pub fn expansion(&self) -> &Expansion<R::Key> {
404        &self.expansion
405    }
406
407    pub_for_tests! {
408        /// Replaces the expansion; the grid owns it.
409        fn with_expansion(mut self, expansion: Expansion<R::Key>) -> Self {
410            self.expansion = expansion;
411            self
412        }
413    }
414}
415
416/// Aggregated values over a set of columns: one value per column of the
417/// set, in schema order; for a group table, one such run per group.
418#[derive(Clone, Debug, Default)]
419pub(crate) struct AggregateValues {
420    columns: ColumnSet,
421    values: Vec<OwnedValue>,
422}
423
424impl AggregateValues {
425    /// `values` of `columns`, a run of `columns.len()` values per row of
426    /// aggregates.
427    pub(crate) fn new(columns: ColumnSet, values: Vec<OwnedValue>) -> Self {
428        debug_assert!(
429            columns.is_empty() || values.len() % columns.len() == 0,
430            "a value per column"
431        );
432        Self { columns, values }
433    }
434
435    /// The run of aggregates at `ix`, of `row_count` rows.
436    fn row(&self, ix: usize, row_count: usize) -> AggregateRow<'_> {
437        let width = self.columns.len();
438        AggregateRow {
439            columns: self.columns,
440            values: self
441                .values
442                .get(ix * width..(ix + 1) * width)
443                .unwrap_or_default(),
444            row_count,
445        }
446    }
447}
448
449/// The aggregates of one group, or of the whole view for the totals: a
450/// view into the projection.
451#[derive(Clone, Copy, Debug)]
452pub struct AggregateRow<'a> {
453    columns: ColumnSet,
454    values: &'a [OwnedValue],
455    row_count: usize,
456}
457
458impl<'a> AggregateRow<'a> {
459    /// The aggregates `values` of `columns` (one per column, in schema
460    /// order), over `row_count` rows.
461    pub(crate) fn new(columns: ColumnSet, values: &'a [OwnedValue], row_count: usize) -> Self {
462        debug_assert_eq!(values.len(), columns.len(), "a value per column");
463        Self {
464            columns,
465            values,
466            row_count,
467        }
468    }
469
470    /// The aggregate of a column, if the view aggregates it.
471    pub fn get(&self, column: ColumnIx) -> Option<&'a OwnedValue> {
472        if !self.columns.contains(column) {
473            return None;
474        }
475        self.values.get(self.columns.rank(column))
476    }
477
478    /// The aggregated columns.
479    pub fn columns(&self) -> ColumnSet {
480        self.columns
481    }
482
483    /// The live shown rows of the view or group; fading rows are not
484    /// counted. For a group and flat or grouped totals these are the rows
485    /// the aggregates are over. A tree's totals count every live shown row,
486    /// nodes included, while their aggregates follow [`TreeTotals`](super::TreeTotals)
487    /// (`Leaves` by default: only rows without live shown children).
488    pub fn row_count(&self) -> usize {
489        self.row_count
490    }
491
492    /// Returns true if no column is aggregated.
493    pub fn is_empty(&self) -> bool {
494        self.values.is_empty()
495    }
496}
497
498/// One group of a grouped projection.
499#[derive(Clone, Debug)]
500pub(crate) struct Group {
501    pub(super) path: GroupPath,
502    pub(super) parent: Option<GroupId>,
503    /// The group past the last of its subgroups, which follow it in
504    /// caption order: they are `id + 1..end`.
505    pub(super) end: u32,
506    /// The group's place in key order, before `GroupOrder` sorted the
507    /// siblings: aggregates combine subgroups in this order, so they do not
508    /// depend on the group order or on a later job.
509    pub(super) key_ix: u32,
510    /// The group's rows in `Projection::rows`, its subgroups' included.
511    pub(super) rows: Range<u32>,
512    /// Live rows: fading ones left out.
513    pub(super) live: u32,
514    /// The caption line; `u32::MAX` while a collapsed ancestor hides it.
515    pub(super) line: u32,
516    pub(super) expanded: bool,
517    pub(super) footer: bool,
518}
519
520impl Group {
521    /// The group's depth: 0 for a top-level group.
522    pub(super) fn depth(&self) -> u16 {
523        self.path.depth()
524    }
525
526    /// The group's rows, as a range of `Projection::rows`.
527    pub(super) fn row_range(&self) -> Range<usize> {
528        self.rows.start as usize..self.rows.end as usize
529    }
530}
531
532/// The groups of a grouped projection, in caption order: a group, then its
533/// subgroups, siblings in the group order.
534#[derive(Clone, Debug, Default)]
535pub(crate) struct GroupData {
536    pub(super) groups: Vec<Group>,
537    /// Each group's aggregates, a run per group in `GroupId` order.
538    pub(super) aggregates: AggregateValues,
539    /// The number of grouping levels: groups of the last one hold the data
540    /// rows, the others hold groups.
541    pub(super) levels: u16,
542    /// `GroupTable::find`, built on first use: the frame does not need it.
543    by_path: OnceLock<FxHashMap<GroupPath, GroupId>>,
544}
545
546/// The group table of a projection without groups.
547static NO_GROUPS: GroupData = GroupData {
548    groups: Vec::new(),
549    aggregates: AggregateValues {
550        columns: ColumnSet::EMPTY,
551        values: Vec::new(),
552    },
553    levels: 0,
554    by_path: OnceLock::new(),
555};
556
557impl GroupData {
558    /// `groups` of a grouping of `levels` levels, in caption order, with
559    /// their aggregates: a run per group.
560    pub(super) fn new(levels: u16, groups: Vec<Group>, aggregates: AggregateValues) -> Self {
561        debug_assert_eq!(
562            aggregates.values.len(),
563            groups.len() * aggregates.columns.len(),
564            "a value per aggregated column"
565        );
566        Self {
567            groups,
568            aggregates,
569            levels,
570            by_path: OnceLock::new(),
571        }
572    }
573
574    /// Returns true if `group` is of the last level: it holds data rows.
575    pub(super) fn is_leaf(&self, group: &Group) -> bool {
576        usize::from(group.depth()) + 1 == usize::from(self.levels)
577    }
578
579    /// The level ids from the top down, if there is a group: the first
580    /// group of the last level comes right after its ancestors.
581    fn level_ids(&self) -> Option<impl Iterator<Item = &ColumnId>> {
582        let first_leaf = self.groups.get(usize::from(self.levels).checked_sub(1)?)?;
583        Some(first_leaf.path.levels().iter().map(|(id, _)| id))
584    }
585
586    /// The table over `rows`, the projection's rows.
587    pub(super) fn table<'a>(&'a self, rows: &'a [RowId]) -> GroupTable<'a> {
588        GroupTable { data: self, rows }
589    }
590}
591
592/// The groups of a projection: a view into it.
593#[derive(Clone, Copy)]
594pub struct GroupTable<'a> {
595    data: &'a GroupData,
596    /// `Projection::rows`, which groups take their rows from.
597    rows: &'a [RowId],
598}
599
600impl<'a> GroupTable<'a> {
601    /// The number of groups, at every level.
602    pub fn len(&self) -> usize {
603        self.data.groups.len()
604    }
605
606    /// Returns true if the projection has no groups.
607    pub fn is_empty(&self) -> bool {
608        self.data.groups.is_empty()
609    }
610
611    /// The group with `id`.
612    pub fn get(&self, id: GroupId) -> Option<GroupInfo<'a>> {
613        (id.ix() < self.len()).then_some(GroupInfo { table: *self, id })
614    }
615
616    /// Every group in caption order: a group, then its subgroups.
617    pub fn iter(&self) -> impl ExactSizeIterator<Item = GroupInfo<'a>> + 'a {
618        let table = *self;
619        (0..self.len()).map(move |ix| GroupInfo {
620            table,
621            id: GroupId::new(ix),
622        })
623    }
624
625    /// The group at `path`, if the projection has it. The first call builds
626    /// a map of the paths.
627    pub fn find(&self, path: &GroupPath) -> Option<GroupInfo<'a>> {
628        let by_path = self.data.by_path.get_or_init(|| {
629            self.data
630                .groups
631                .iter()
632                .enumerate()
633                .map(|(ix, group)| (group.path.clone(), GroupId::new(ix)))
634                .collect()
635        });
636        self.get(*by_path.get(path)?)
637    }
638}
639
640impl fmt::Debug for GroupTable<'_> {
641    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
642        f.debug_list().entries(self.iter()).finish()
643    }
644}
645
646/// One group of a projection: a view into it.
647#[derive(Clone, Copy)]
648pub struct GroupInfo<'a> {
649    table: GroupTable<'a>,
650    id: GroupId,
651}
652
653impl<'a> GroupInfo<'a> {
654    fn group(&self) -> &'a Group {
655        &self.table.data.groups[self.id.ix()]
656    }
657
658    /// The group's position in this projection's table.
659    pub fn id(&self) -> GroupId {
660        self.id
661    }
662
663    /// The group's identity.
664    pub fn path(&self) -> &'a GroupPath {
665        &self.group().path
666    }
667
668    /// The id of the group's level: a column's id or a `GroupKey` id.
669    pub fn level_id(&self) -> &'a ColumnId {
670        let path = self.path();
671        path.level_id(path.depth())
672            .expect("a group's path is not the root")
673    }
674
675    /// The group's key value.
676    pub fn value(&self) -> &'a OwnedValue {
677        let path = self.path();
678        path.value(path.depth())
679            .expect("a group's path is not the root")
680    }
681
682    /// The group's depth: 0 for a top-level group.
683    pub fn depth(&self) -> u16 {
684        self.path().depth()
685    }
686
687    /// The enclosing group; `None` at the top level.
688    pub fn parent(&self) -> Option<GroupId> {
689        self.group().parent
690    }
691
692    /// The group's live rows, its subgroups' included; fading rows are
693    /// not counted.
694    pub fn row_count(&self) -> usize {
695        self.group().live as usize
696    }
697
698    /// The group's rows in order, its subgroups' included: a slice of
699    /// [`Projection::rows`]. Fading rows are among them.
700    pub fn rows(&self) -> &'a [RowId] {
701        let rows = &self.group().rows;
702        &self.table.rows[rows.start as usize..rows.end as usize]
703    }
704
705    /// Returns true if the group is expanded.
706    pub fn is_expanded(&self) -> bool {
707        self.group().expanded
708    }
709
710    /// Returns true if the group has a footer (`GroupFooter`).
711    pub fn has_footer(&self) -> bool {
712        self.group().footer
713    }
714
715    /// The group's aggregates.
716    pub fn aggregates(&self) -> AggregateRow<'a> {
717        self.table
718            .data
719            .aggregates
720            .row(self.id.ix(), self.row_count())
721    }
722
723    /// The caption line, unless a collapsed ancestor hides it.
724    fn line(&self) -> Option<usize> {
725        let line = self.group().line;
726        (line != u32::MAX).then_some(line as usize)
727    }
728}
729
730impl fmt::Debug for GroupInfo<'_> {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        f.debug_struct("GroupInfo")
733            .field("id", &self.id)
734            .field("path", self.path())
735            .field("row_count", &self.row_count())
736            .field("is_expanded", &self.is_expanded())
737            .field("has_footer", &self.has_footer())
738            .finish()
739    }
740}
741
742/// The shown lines of a projection.
743#[derive(Clone, Debug, Default)]
744pub(crate) enum Lines {
745    /// No grouping, no tree: the lines are `Projection::rows` at depth 0.
746    #[default]
747    Flat,
748    /// Groups or a tree: the lines are listed. Boxed, so a flat projection
749    /// stays small.
750    Built(Box<BuiltLines>),
751}
752
753/// The listed lines of a grouped or tree projection.
754#[derive(Clone, Debug)]
755pub(crate) struct BuiltLines {
756    lines: Vec<DisplayRow>,
757    /// The line of each shown data row, by slot; `u32::MAX` for none.
758    row_to_display: Vec<u32>,
759    /// The lines of the shown data rows whose slot an earlier line holds in
760    /// `row_to_display`: a fading row and the new row that reused its slot.
761    shared_display: FxHashMap<RowId, u32>,
762    structure: BuiltStructure,
763}
764
765impl BuiltLines {
766    /// `lines` of `structure`, over slots below `capacity`.
767    fn new(lines: Vec<DisplayRow>, capacity: usize, structure: BuiltStructure) -> Box<Self> {
768        let (row_to_display, shared_display) = places(
769            lines
770                .iter()
771                .enumerate()
772                .filter_map(|(display_ix, line)| line.row().map(|id| (display_ix, id))),
773            capacity,
774        );
775        Box::new(Self {
776            lines,
777            row_to_display,
778            shared_display,
779            structure,
780        })
781    }
782}
783
784/// What a listed projection is built of.
785#[derive(Clone, Debug)]
786pub(crate) enum BuiltStructure {
787    /// Groups.
788    Groups(GroupData),
789    /// A tree.
790    Tree(TreeData),
791}
792
793impl Projection {
794    /// A projection whose lines are listed: `rows` in order as if all were
795    /// expanded, the shown `lines` and what they are built of. Every data
796    /// line's row must be in `rows`.
797    pub(crate) fn with_lines<R: GridRow>(
798        snapshot: &StoreSnapshot<R>,
799        rows: Vec<RowId>,
800        lines: Vec<DisplayRow>,
801        structure: BuiltStructure,
802    ) -> Self {
803        let mut projection = Self::new(snapshot, rows);
804        projection.lines = Lines::Built(BuiltLines::new(
805            lines,
806            projection.row_to_order.len(),
807            structure,
808        ));
809        projection
810    }
811
812    /// Sets the live rows: fewer than `rows` when fading rows are among
813    /// them, or when the snapshot no longer has some.
814    pub(crate) fn with_row_count(mut self, row_count: usize) -> Self {
815        self.row_count = row_count;
816        self
817    }
818
819    /// A grouped projection: `rows` in order as if all were expanded, the
820    /// rows of each group following each other, and the groups over them
821    /// with their flags. Lays the lines out by those flags.
822    pub(crate) fn with_groups<R: GridRow>(
823        snapshot: &StoreSnapshot<R>,
824        rows: Vec<RowId>,
825        mut groups: GroupData,
826    ) -> Self {
827        let lines = expand::lay_out(&mut groups, &rows);
828        Self::with_lines(snapshot, rows, lines, BuiltStructure::Groups(groups))
829    }
830
831    /// This projection's rows, version and totals over `groups`, its own
832    /// groups with new expansion flags, laid out anew: the expand job.
833    pub(super) fn with_group_layout(&self, mut groups: GroupData) -> Self {
834        let lines = expand::lay_out(&mut groups, &self.rows);
835        Self {
836            store_version: self.store_version,
837            rows: self.rows.clone(),
838            row_to_order: self.row_to_order.clone(),
839            shared_order: self.shared_order.clone(),
840            row_count: self.row_count,
841            lines: Lines::Built(BuiltLines::new(
842                lines,
843                self.row_to_order.len(),
844                BuiltStructure::Groups(groups),
845            )),
846            totals: self.totals.clone(),
847            summary_stamps: None,
848        }
849    }
850
851    /// This grouped projection's order and lines over `groups`, its own
852    /// groups with fresh counts and aggregates, as of `store_version`: the
853    /// aggregate job.
854    pub(super) fn with_group_values(
855        &self,
856        groups: GroupData,
857        totals: AggregateValues,
858        row_count: usize,
859        store_version: u64,
860    ) -> Self {
861        let built = self.built().expect("a grouped projection lists its lines");
862        Self {
863            store_version,
864            rows: self.rows.clone(),
865            row_to_order: self.row_to_order.clone(),
866            shared_order: self.shared_order.clone(),
867            row_count,
868            lines: Lines::Built(Box::new(BuiltLines {
869                lines: built.lines.clone(),
870                row_to_display: built.row_to_display.clone(),
871                shared_display: built.shared_display.clone(),
872                structure: BuiltStructure::Groups(groups),
873            })),
874            totals,
875            summary_stamps: None,
876        }
877    }
878
879    /// The groups of a grouped projection.
880    pub(super) fn group_data(&self) -> Option<&GroupData> {
881        match self.built().map(|built| &built.structure) {
882            Some(BuiltStructure::Groups(groups)) => Some(groups),
883            _ => None,
884        }
885    }
886
887    /// Sets the aggregates of the whole view: one run over their columns.
888    pub(crate) fn with_totals(mut self, totals: AggregateValues) -> Self {
889        self.totals = totals;
890        self
891    }
892
893    /// Sets the store version the projection reflects.
894    pub(crate) fn with_store_version(mut self, store_version: u64) -> Self {
895        self.store_version = store_version;
896        self
897    }
898
899    /// The listed lines, if not flat.
900    fn built(&self) -> Option<&BuiltLines> {
901        match &self.lines {
902            Lines::Flat => None,
903            Lines::Built(built) => Some(built),
904        }
905    }
906
907    /// The number of shown lines: group captions, data rows and group
908    /// footers. In a flat view, the number of displayed rows.
909    pub fn len(&self) -> usize {
910        match &self.lines {
911            Lines::Flat => self.rows.len(),
912            Lines::Built(built) => built.lines.len(),
913        }
914    }
915
916    /// Returns true if no line is shown.
917    pub fn is_empty(&self) -> bool {
918        self.len() == 0
919    }
920
921    /// The line at a display position.
922    pub fn line(&self, display_ix: usize) -> Option<DisplayRow> {
923        match &self.lines {
924            Lines::Flat => self
925                .rows
926                .get(display_ix)
927                .map(|&row| DisplayRow::Data { row, depth: 0 }),
928            Lines::Built(built) => built.lines.get(display_ix).copied(),
929        }
930    }
931
932    /// The data row at a display position; `None` on a group caption or
933    /// footer.
934    pub fn row(&self, display_ix: usize) -> Option<RowId> {
935        match &self.lines {
936            Lines::Flat => self.rows.get(display_ix).copied(),
937            Lines::Built(built) => built.lines.get(display_ix).and_then(|line| line.row()),
938        }
939    }
940
941    /// The display position of a data row; `None` if the view filters it
942    /// out or a collapsed group or tree row hides it.
943    pub fn display_ix(&self, id: RowId) -> Option<usize> {
944        match &self.lines {
945            Lines::Flat => self.order_ix(id),
946            Lines::Built(built) => match built.row_to_display.get(id.ix()) {
947                Some(&ix) if ix != u32::MAX && built.lines[ix as usize].row() == Some(id) => {
948                    Some(ix as usize)
949                }
950                _ => shared_place(&built.shared_display, id),
951            },
952        }
953    }
954
955    /// The position of a row in [`Self::rows`], shown or in a collapsed
956    /// group; `None` if the view filters it out.
957    pub fn order_ix(&self, id: RowId) -> Option<usize> {
958        match self.row_to_order.get(id.ix()) {
959            Some(&ix) if ix != u32::MAX && self.rows[ix as usize] == id => Some(ix as usize),
960            _ => shared_place(&self.shared_order, id),
961        }
962    }
963
964    /// The live rows of [`Self::rows`] when the projection was built: fading
965    /// rows are not counted. In a flat view without fading rows, the number
966    /// of displayed rows.
967    pub fn row_count(&self) -> usize {
968        self.row_count
969    }
970
971    /// The data rows of the shown lines, in display order.
972    // The copy walks the lines; only tests read the data rows alone.
973    #[cfg_attr(not(test), allow(dead_code))]
974    pub(crate) fn shown_rows(&self) -> ShownRowIds<'_> {
975        match &self.lines {
976            Lines::Flat => ShownRowIds::Flat(self.rows.iter()),
977            Lines::Built(built) => ShownRowIds::Built(built.lines.iter()),
978        }
979    }
980
981    /// The group table; empty without grouping.
982    pub fn groups(&self) -> GroupTable<'_> {
983        self.group_data().unwrap_or(&NO_GROUPS).table(&self.rows)
984    }
985
986    /// The display position of a group's caption; `None` while a collapsed
987    /// ancestor hides it.
988    pub fn group_line(&self, group: GroupId) -> Option<usize> {
989        self.groups().get(group)?.line()
990    }
991
992    /// The aggregates of the whole view, for the totals row; empty without
993    /// aggregates.
994    pub fn totals(&self) -> AggregateRow<'_> {
995        self.totals.row(0, self.row_count)
996    }
997
998    /// Returns true if the view is grouped.
999    pub fn is_grouped(&self) -> bool {
1000        matches!(
1001            self.built().map(|built| &built.structure),
1002            Some(BuiltStructure::Groups(_))
1003        )
1004    }
1005
1006    /// Returns true if the view is a tree.
1007    pub fn is_tree(&self) -> bool {
1008        matches!(
1009            self.built().map(|built| &built.structure),
1010            Some(BuiltStructure::Tree(_))
1011        )
1012    }
1013
1014    /// The tree data of a tree projection.
1015    pub(crate) fn tree_data(&self) -> Option<&TreeData> {
1016        match self.built().map(|built| &built.structure) {
1017            Some(BuiltStructure::Tree(tree)) => Some(tree),
1018            _ => None,
1019        }
1020    }
1021
1022    /// The lines as if every group and tree row were expanded, as an export
1023    /// of everything writes them: group footers by
1024    /// [`GroupInfo::has_footer`], tree rows at their depths. In a flat view,
1025    /// the shown lines. Walks the groups or the tree without listing the
1026    /// lines.
1027    pub fn expanded_lines(&self) -> impl Iterator<Item = DisplayRow> + '_ {
1028        match &self.lines {
1029            Lines::Flat => ExpandedLines::Flat(self.rows.iter()),
1030            Lines::Built(built) => match &built.structure {
1031                BuiltStructure::Groups(groups) => {
1032                    ExpandedLines::Groups(expand::ExpandedGroupLines::new(groups, &self.rows))
1033                }
1034                BuiltStructure::Tree(tree) => {
1035                    ExpandedLines::Tree(self.rows.iter().enumerate(), tree)
1036                }
1037            },
1038        }
1039    }
1040}
1041
1042/// The data rows of the shown lines (`Projection::shown_rows`).
1043// Only tests use it: see `Projection::shown_rows`.
1044#[cfg_attr(not(test), allow(dead_code))]
1045pub(crate) enum ShownRowIds<'a> {
1046    Flat(slice::Iter<'a, RowId>),
1047    Built(slice::Iter<'a, DisplayRow>),
1048}
1049
1050impl Iterator for ShownRowIds<'_> {
1051    type Item = RowId;
1052
1053    #[inline]
1054    fn next(&mut self) -> Option<RowId> {
1055        match self {
1056            Self::Flat(rows) => rows.next().copied(),
1057            Self::Built(lines) => lines.find_map(|line| line.row()),
1058        }
1059    }
1060}
1061
1062impl FusedIterator for ShownRowIds<'_> {}
1063
1064/// `Projection::expanded_lines`.
1065enum ExpandedLines<'a> {
1066    Flat(slice::Iter<'a, RowId>),
1067    Groups(expand::ExpandedGroupLines<'a>),
1068    /// The rows of a tree with their places, for their depths.
1069    Tree(iter::Enumerate<slice::Iter<'a, RowId>>, &'a TreeData),
1070}
1071
1072impl Iterator for ExpandedLines<'_> {
1073    type Item = DisplayRow;
1074
1075    fn next(&mut self) -> Option<DisplayRow> {
1076        match self {
1077            Self::Flat(rows) => rows.next().map(|&row| DisplayRow::Data { row, depth: 0 }),
1078            Self::Groups(lines) => lines.next(),
1079            Self::Tree(rows, tree) => rows.next().map(|(order_ix, &row)| DisplayRow::Data {
1080                row,
1081                depth: tree.depth(order_ix),
1082            }),
1083        }
1084    }
1085}
1086
1087/// Drops rows removed since `previous` was built, from a flat or grouped
1088/// projection, and keeps the order. A removed row that fades stays while the
1089/// snapshot has it ([`StoreSnapshot::fading_row`]), not counted as live.
1090///
1091/// A flat projection keeps its totals. A grouped one recounts its groups,
1092/// drops the emptied ones and lays its lines out again with each group's
1093/// expansion and footer as they were; the aggregates stay as they were too,
1094/// as the aggregate job refreshes them. A group left with fading rows
1095/// alone goes with them, as a rebuild makes no group of fading rows.
1096pub(super) fn compact<R: GridRow>(
1097    previous: &Projection,
1098    snapshot: &StoreSnapshot<R>,
1099) -> Projection {
1100    if let Some(groups) = previous.group_data() {
1101        return compact_groups(previous, groups, snapshot);
1102    }
1103    let mut live = 0;
1104    let rows = previous
1105        .rows
1106        .iter()
1107        .copied()
1108        .filter(|&id| is_kept(snapshot, id, &mut live))
1109        .collect();
1110    Projection::new(snapshot, rows)
1111        .with_row_count(live as usize)
1112        .with_totals(previous.totals.clone())
1113}
1114
1115/// Returns true if `snapshot` still has the row, live or fading, and counts
1116/// it in `live` if live.
1117#[inline]
1118fn is_kept<R: GridRow>(snapshot: &StoreSnapshot<R>, id: RowId, live: &mut u32) -> bool {
1119    if snapshot.row(id).is_some() {
1120        *live += 1;
1121        true
1122    } else {
1123        snapshot.fading_row(id).is_some()
1124    }
1125}
1126
1127/// [`compact`] of a grouped projection: O(rows + groups), no sorting.
1128fn compact_groups<R: GridRow>(
1129    previous: &Projection,
1130    data: &GroupData,
1131    snapshot: &StoreSnapshot<R>,
1132) -> Projection {
1133    let old = &data.groups;
1134    // The rows of the last level's groups follow each other in caption
1135    // order and cover every row, so one pass over them keeps the order.
1136    let mut rows = Vec::with_capacity(previous.rows.len());
1137    let mut ranges: Vec<Range<u32>> = vec![0..0; old.len()];
1138    let mut live = vec![0u32; old.len()];
1139    let mut row_count = 0;
1140    for (ix, group) in old.iter().enumerate() {
1141        if data.is_leaf(group) {
1142            let start = rows.len();
1143            let mut group_live = 0;
1144            rows.extend(
1145                previous.rows[group.row_range()]
1146                    .iter()
1147                    .copied()
1148                    .filter(|&id| is_kept(snapshot, id, &mut group_live)),
1149            );
1150            // Fading rows alone make no group.
1151            if group_live == 0 {
1152                rows.truncate(start);
1153            }
1154            ranges[ix] = start as u32..rows.len() as u32;
1155            live[ix] = group_live;
1156            row_count += group_live as usize;
1157        }
1158    }
1159    // An upper group spans its subgroups: from its first one to its last
1160    // descendant, a group of the last level. Later groups come first, so
1161    // the subgroups are done.
1162    for (ix, group) in old.iter().enumerate().rev() {
1163        if !data.is_leaf(group) {
1164            let end = group.end as usize;
1165            ranges[ix] = ranges[ix + 1].start..ranges[end - 1].end;
1166            let mut child = ix + 1;
1167            while child < end {
1168                live[ix] += live[child];
1169                child = old[child].end as usize;
1170            }
1171        }
1172    }
1173
1174    // Emptied groups go, with their subgroups: the ids of the others close
1175    // up. `kept_before[ix]` is the new id of the group at `ix`, if kept.
1176    let mut kept_before = Vec::with_capacity(old.len() + 1);
1177    let mut kept = 0u32;
1178    for range in &ranges {
1179        kept_before.push(kept);
1180        kept += u32::from(!range.is_empty());
1181    }
1182    kept_before.push(kept);
1183    let width = data.aggregates.columns.len();
1184    let mut groups = Vec::with_capacity(kept as usize);
1185    let mut values = Vec::with_capacity(kept as usize * width);
1186    for (ix, group) in old.iter().enumerate() {
1187        if ranges[ix].is_empty() {
1188            continue;
1189        }
1190        groups.push(Group {
1191            path: group.path.clone(),
1192            parent: group.parent.map(|parent| GroupId(kept_before[parent.ix()])),
1193            end: kept_before[group.end as usize],
1194            key_ix: group.key_ix,
1195            rows: ranges[ix].clone(),
1196            live: live[ix],
1197            line: u32::MAX,
1198            expanded: group.expanded,
1199            footer: group.footer,
1200        });
1201        values.extend_from_slice(&data.aggregates.values[ix * width..(ix + 1) * width]);
1202    }
1203    let groups = GroupData::new(
1204        data.levels,
1205        groups,
1206        AggregateValues::new(data.aggregates.columns, values),
1207    );
1208    Projection::with_groups(snapshot, rows, groups)
1209        .with_row_count(row_count)
1210        .with_totals(previous.totals.clone())
1211}
1212
1213/// Change stamps of a projection's aggregates and group captions: what
1214/// changed against the projection before, matched by [`GroupPath`]. Kept
1215/// while any stamp is hot.
1216#[derive(Clone, Debug)]
1217pub(crate) struct SummaryStamps {
1218    /// Stamps per run: one per column up to the last aggregated one, so a
1219    /// run is indexed by `ColumnIx`.
1220    width: usize,
1221    /// A run per group, in `GroupId` order, then the totals' run.
1222    aggregates: Vec<Stamp>,
1223    /// Each group's caption stamp, in `GroupId` order.
1224    captions: Vec<Stamp>,
1225}
1226
1227impl SummaryStamps {
1228    /// The run of stamps at `ix`, if any is set.
1229    fn run(&self, ix: usize) -> Option<&[Stamp]> {
1230        let run = self
1231            .aggregates
1232            .get(ix * self.width..(ix + 1) * self.width)?;
1233        run.iter().any(|stamp| stamp.is_set()).then_some(run)
1234    }
1235}
1236
1237impl Projection {
1238    /// The change stamps of a group's aggregates while any is hot, indexed
1239    /// by `ColumnIx`: one per column up to the last aggregated one,
1240    /// [`Stamp::NONE`] for a column whose aggregate did not change. The view
1241    /// sets them as aggregates change; a new group has none, its caption
1242    /// shows it ([`Self::caption_stamp`]).
1243    pub fn group_stamps(&self, group: GroupId) -> Option<&[Stamp]> {
1244        let stamps = self.summary_stamps.as_deref()?;
1245        if group.ix() >= stamps.captions.len() {
1246            return None;
1247        }
1248        stamps.run(group.ix())
1249    }
1250
1251    /// The change stamps of the totals while any is hot, indexed by
1252    /// `ColumnIx` as [`Self::group_stamps`].
1253    pub fn totals_stamps(&self) -> Option<&[Stamp]> {
1254        let stamps = self.summary_stamps.as_deref()?;
1255        stamps.run(stamps.captions.len())
1256    }
1257
1258    /// The stamp of a group's caption while it is hot: [`RowChange::Inserted`]
1259    /// for a new group, [`RowChange::Updated`] without a direction when its
1260    /// live row count changed.
1261    pub fn caption_stamp(&self, group: GroupId) -> Option<Stamp> {
1262        let stamps = self.summary_stamps.as_deref()?;
1263        stamps
1264            .captions
1265            .get(group.ix())
1266            .copied()
1267            .filter(|stamp| stamp.is_set())
1268    }
1269
1270    pub_for_tests! {
1271        /// Stamps the aggregates and captions that differ from `previous`,
1272        /// the projection the view showed or held before this one, matched
1273        /// by [`GroupPath`], at `now_ms` (the store clock, milliseconds
1274        /// since the snapshot epoch), and carries the stamps of `previous`
1275        /// that are still hot by `policy` where nothing changed. A changed
1276        /// aggregate is [`RowChange::Updated`], with the direction of two
1277        /// numbers; a new group's caption [`RowChange::Inserted`], a caption
1278        /// whose live row count changed `Updated`.
1279        ///
1280        /// A `previous` of another structure or other aggregated columns (a
1281        /// new grouping, flat against grouped) gives no stamps: the view
1282        /// changed, not the data. O(groups × aggregated columns).
1283        fn carry_summary_stamps(
1284            &mut self,
1285            previous: &Projection,
1286            now_ms: u32,
1287            policy: HighlightPolicy,
1288        ) {
1289            self.summary_stamps = None;
1290            if !self.has_summaries_of(previous) {
1291                return;
1292            }
1293            let columns = self.totals.columns;
1294            let width = (0..128u16)
1295                .rev()
1296                .find(|&ix| columns.contains(ColumnIx::new(ix)))
1297                .map_or(0, |ix| usize::from(ix) + 1);
1298            let hot = |stamp: Option<Stamp>| {
1299                stamp.filter(|&stamp| !policy.is_expired(stamp, now_ms))
1300                    .unwrap_or(Stamp::NONE)
1301            };
1302            // One run: a stamp for each changed aggregate, the hot ones
1303            // carried for the rest.
1304            let stamp_run = |run: &mut [Stamp],
1305                             new: AggregateRow<'_>,
1306                             old: AggregateRow<'_>,
1307                             old_stamps: Option<&[Stamp]>| {
1308                for (ix, stamp) in run.iter_mut().enumerate() {
1309                    let column = ColumnIx::new(ix as u16);
1310                    *stamp = match (new.get(column), old.get(column)) {
1311                        (Some(new), Some(old)) if new != old => Stamp::with_direction(
1312                            now_ms,
1313                            RowChange::Updated,
1314                            direction(new, old),
1315                        ),
1316                        (Some(_), Some(_)) => hot(old_stamps.and_then(|old| old.get(ix).copied())),
1317                        _ => Stamp::NONE,
1318                    };
1319                }
1320            };
1321
1322            let groups = self.groups();
1323            let old_groups = previous.groups();
1324            let mut aggregates = vec![Stamp::NONE; (groups.len() + 1) * width];
1325            let mut captions = vec![Stamp::NONE; groups.len()];
1326            for group in groups.iter() {
1327                let ix = group.id().ix();
1328                captions[ix] = match old_groups.find(group.path()) {
1329                    Some(old) => {
1330                        stamp_run(
1331                            &mut aggregates[ix * width..(ix + 1) * width],
1332                            group.aggregates(),
1333                            old.aggregates(),
1334                            previous.group_stamps(old.id()),
1335                        );
1336                        if group.row_count() == old.row_count() {
1337                            hot(previous.caption_stamp(old.id()))
1338                        } else {
1339                            Stamp::new(now_ms, RowChange::Updated)
1340                        }
1341                    }
1342                    None => Stamp::new(now_ms, RowChange::Inserted),
1343                };
1344            }
1345            stamp_run(
1346                &mut aggregates[groups.len() * width..],
1347                self.totals(),
1348                previous.totals(),
1349                previous.totals_stamps(),
1350            );
1351            if aggregates.iter().chain(&captions).any(|stamp| stamp.is_set()) {
1352                self.summary_stamps = Some(Box::new(SummaryStamps {
1353                    width,
1354                    aggregates,
1355                    captions,
1356                }));
1357            }
1358        }
1359    }
1360
1361    /// Returns true if `other` has the summaries of this projection: the
1362    /// same aggregated columns and the same structure, flat, a tree or
1363    /// groups of the same levels.
1364    fn has_summaries_of(&self, other: &Projection) -> bool {
1365        if self.totals.columns != other.totals.columns {
1366            return false;
1367        }
1368        fn structure(projection: &Projection) -> Option<&BuiltStructure> {
1369            projection.built().map(|built| &built.structure)
1370        }
1371        match (structure(self), structure(other)) {
1372            (None, None) | (Some(BuiltStructure::Tree(_)), Some(BuiltStructure::Tree(_))) => true,
1373            (Some(BuiltStructure::Groups(a)), Some(BuiltStructure::Groups(b))) => {
1374                a.levels == b.levels
1375                    && match (a.level_ids(), b.level_ids()) {
1376                        (Some(a), Some(b)) => a.eq(b),
1377                        _ => true,
1378                    }
1379            }
1380            _ => false,
1381        }
1382    }
1383}
1384
1385/// The direction from `old` to `new`, when both are numbers.
1386fn direction(new: &OwnedValue, old: &OwnedValue) -> Option<ChangeDirection> {
1387    let (new, old) = (new.as_cell(), old.as_cell());
1388    if !(new.is_number() && old.is_number()) {
1389        return None;
1390    }
1391    match new.cmp_total(&old) {
1392        std::cmp::Ordering::Greater => Some(ChangeDirection::Up),
1393        std::cmp::Ordering::Less => Some(ChangeDirection::Down),
1394        std::cmp::Ordering::Equal => None,
1395    }
1396}
1397
1398#[cfg(test)]
1399mod tests {
1400    use std::time::Instant;
1401
1402    use super::*;
1403    use crate::{
1404        highlight::HighlightPolicy,
1405        model::{CellValue, GridColumns},
1406        store::{Ingest, Op},
1407    };
1408
1409    fn path(values: &[i64]) -> GroupPath {
1410        const LEVELS: [&str; 3] = ["a", "b", "c"];
1411        GroupPath::new(
1412            values
1413                .iter()
1414                .zip(LEVELS)
1415                .map(|(value, id)| (ColumnId::new_static(id), OwnedValue::Int(*value))),
1416        )
1417    }
1418
1419    #[test]
1420    fn test_group_path() {
1421        let root = GroupPath::default();
1422        assert!(root.is_root() && root.levels().is_empty());
1423        assert_eq!(root.parent(), None);
1424        let top = path(&[1]);
1425        let inner = path(&[1, 2]);
1426        assert_eq!((top.depth(), inner.depth()), (0, 1));
1427        assert_eq!(inner.level_id(1).map(ColumnId::as_str), Some("b"));
1428        assert_eq!(inner.value(0), Some(&OwnedValue::Int(1)));
1429        assert_eq!(inner.value(2), None);
1430        assert_eq!(inner.parent(), Some(top.clone()));
1431        assert_eq!(top.parent(), Some(root.clone()));
1432        assert!(inner.starts_with(&top) && inner.starts_with(&root) && inner.starts_with(&inner));
1433        assert!(!top.starts_with(&inner) && !path(&[2, 2]).starts_with(&top));
1434        // Values equal by `cmp_total` name one group: 1 and 1.0.
1435        let float = GroupPath::new([(ColumnId::new_static("a"), OwnedValue::Float(1.0))]);
1436        assert_eq!(float, top);
1437        assert_eq!(
1438            FxHashSet::from_iter([float])
1439                .get(&top)
1440                .map(GroupPath::depth),
1441            Some(0)
1442        );
1443    }
1444
1445    #[test]
1446    fn test_expansion_rule() {
1447        use ExpandDepth::{All, Captions, Levels};
1448        // (depth, levels, expanded by default) for each default depth.
1449        let cases: [(ExpandDepth, &[(u16, usize, bool)]); 6] = [
1450            (All, &[(0, 1, true), (1, 2, true), (2, 3, true)]),
1451            (Levels(0), &[(0, 1, false), (0, 3, false), (1, 3, false)]),
1452            (Levels(1), &[(0, 1, true), (0, 2, true), (1, 2, false)]),
1453            (Levels(2), &[(0, 3, true), (1, 3, true), (2, 3, false)]),
1454            (Levels(3), &[(0, 3, true), (1, 3, true), (2, 3, true)]),
1455            // Every level but the last: captions only.
1456            (
1457                Captions,
1458                &[
1459                    (0, 1, false),
1460                    (0, 2, true),
1461                    (1, 2, false),
1462                    (0, 3, true),
1463                    (1, 3, true),
1464                    (2, 3, false),
1465                ],
1466            ),
1467        ];
1468        for (depth, groups) in cases {
1469            let expansion = Expansion::<u32>::new(depth);
1470            assert_eq!(expansion.depth(), depth);
1471            for &(group_depth, levels, expanded) in groups {
1472                let values: Vec<i64> = (0..=i64::from(group_depth)).collect();
1473                let group = path(&values);
1474                assert_eq!(
1475                    expansion.is_group_expanded(&group, levels),
1476                    expanded,
1477                    "{depth:?}, depth {group_depth} of {levels}"
1478                );
1479                // Toggling the path inverts the default; toggling it back
1480                // to the default forgets it.
1481                let toggled = expansion
1482                    .clone()
1483                    .with_group(group.clone(), !expanded, levels);
1484                assert_eq!(toggled.is_group_expanded(&group, levels), !expanded);
1485                assert_eq!(toggled.toggled_groups().count(), 1);
1486                assert_ne!(toggled, expansion);
1487                let back = toggled.with_group(group.clone(), expanded, levels);
1488                assert_eq!(back.is_group_expanded(&group, levels), expanded);
1489                assert_eq!(back.toggled_groups().count(), 0);
1490                assert_eq!(back, expansion);
1491            }
1492            // Tree rows: `Captions` is `All`.
1493            for row_depth in 0..4u16 {
1494                let expected = match depth {
1495                    All | Captions => true,
1496                    Levels(n) => row_depth < n,
1497                };
1498                assert_eq!(expansion.is_row_expanded(&7, row_depth), expected);
1499                let toggled = expansion.clone().with_row(7, !expected, row_depth);
1500                assert_eq!(toggled.is_row_expanded(&7, row_depth), !expected);
1501                assert!(expansion.is_row_expanded(&8, row_depth) == expected);
1502                assert_eq!(toggled.toggled_rows().collect::<Vec<_>>(), [&7]);
1503            }
1504        }
1505
1506        // A toggle survives a change of grouping: the same path is toggled
1507        // against whatever default the new grouping gives it.
1508        let collapsed = Expansion::<u32>::default().with_group(path(&[1, 2]), false, 2);
1509        assert!(!collapsed.is_group_expanded(&path(&[1, 2]), 3));
1510        assert!(collapsed.is_group_expanded(&path(&[1, 3]), 3));
1511        // Clones share the toggles until one changes.
1512        let clone = collapsed.clone();
1513        let changed = clone.with_group(path(&[1, 2]), true, 2);
1514        assert!(!collapsed.is_group_expanded(&path(&[1, 2]), 2));
1515        assert!(changed.is_group_expanded(&path(&[1, 2]), 2));
1516    }
1517
1518    #[test]
1519    fn test_expand_depth_serde() {
1520        for (depth, json) in [
1521            (ExpandDepth::All, r#""all""#),
1522            (ExpandDepth::Captions, r#""captions""#),
1523            (ExpandDepth::Levels(0), "0"),
1524            (ExpandDepth::Levels(2), "2"),
1525        ] {
1526            assert_eq!(serde_json::to_string(&depth).unwrap(), json);
1527            assert_eq!(serde_json::from_str::<ExpandDepth>(json).unwrap(), depth);
1528        }
1529        for bad in [r#""deep""#, r#""All""#, "-1", "65536", "1.5", "null"] {
1530            assert!(serde_json::from_str::<ExpandDepth>(bad).is_err(), "{bad}");
1531        }
1532    }
1533
1534    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1535    struct Column;
1536
1537    impl GridColumns for Column {
1538        fn all() -> &'static [Self] {
1539            &[Self]
1540        }
1541
1542        fn id(self) -> &'static str {
1543            "value"
1544        }
1545
1546        fn ix(self) -> ColumnIx {
1547            ColumnIx::new(0)
1548        }
1549    }
1550
1551    struct Row(u32);
1552
1553    impl GridRow for Row {
1554        type Key = u32;
1555        type Column = Column;
1556
1557        fn key(&self) -> &u32 {
1558            &self.0
1559        }
1560
1561        fn value(&self, _: Column) -> CellValue<'_> {
1562            CellValue::Int(self.0.into())
1563        }
1564    }
1565
1566    #[test]
1567    fn test_listed_lines() {
1568        // Two groups of a hand-built projection: {0, 1} expanded with a
1569        // footer, {2} collapsed. Rows of the collapsed group pass the view
1570        // but are not shown.
1571        let epoch = Instant::now();
1572        let mut ingest = Ingest::<Row>::new(HighlightPolicy::default(), epoch);
1573        ingest.apply((0..4).map(|id| Op::UpsertSilent(Arc::new(Row(id)))), epoch);
1574        let snapshot = ingest.snapshot();
1575        let ids: Vec<RowId> = (0..4).map(|key| snapshot.row_id(&key).unwrap()).collect();
1576        let mut columns = ColumnSet::EMPTY;
1577        columns.insert(ColumnIx::new(0));
1578        let group =
1579            |path: GroupPath, end: u32, rows: Range<u32>, expanded: bool, footer: bool| Group {
1580                path,
1581                parent: None,
1582                end,
1583                key_ix: end - 1,
1584                live: rows.end - rows.start,
1585                rows,
1586                line: u32::MAX,
1587                expanded,
1588                footer,
1589            };
1590        let groups = GroupData::new(
1591            1,
1592            vec![
1593                group(path(&[1]), 1, 0..2, true, true),
1594                group(path(&[2]), 2, 2..3, false, false),
1595            ],
1596            AggregateValues::new(columns, vec![OwnedValue::Int(1), OwnedValue::Int(2)]),
1597        );
1598        let (first, second) = (GroupId::new(0), GroupId::new(1));
1599        let lines = vec![
1600            DisplayRow::GroupCaption {
1601                group: first,
1602                depth: 0,
1603            },
1604            DisplayRow::Data {
1605                row: ids[0],
1606                depth: 1,
1607            },
1608            DisplayRow::Data {
1609                row: ids[1],
1610                depth: 1,
1611            },
1612            DisplayRow::GroupFooter {
1613                group: first,
1614                depth: 0,
1615            },
1616            DisplayRow::GroupCaption {
1617                group: second,
1618                depth: 0,
1619            },
1620        ];
1621        // The lines are laid out from the groups' flags.
1622        let projection = Projection::with_groups(&snapshot, ids[..3].to_vec(), groups)
1623            .with_totals(AggregateValues::new(columns, vec![OwnedValue::Int(3)]));
1624        assert!(projection.is_grouped() && !projection.is_tree());
1625        assert_eq!(projection.len(), 5);
1626        assert_eq!(
1627            (0..5)
1628                .map(|ix| projection.line(ix).unwrap())
1629                .collect::<Vec<_>>(),
1630            lines
1631        );
1632        assert_eq!(projection.row(0), None);
1633        assert_eq!(projection.row(2), Some(ids[1]));
1634        assert_eq!(projection.display_ix(ids[1]), Some(2));
1635        assert_eq!(projection.display_ix(ids[2]), None, "in a collapsed group");
1636        assert_eq!(projection.order_ix(ids[2]), Some(2));
1637        assert!(projection.contains(ids[2]) && !projection.contains(ids[3]));
1638        assert_eq!(projection.row_count(), 3);
1639        assert_eq!(projection.shown_rows().collect::<Vec<_>>(), ids[..2]);
1640        assert_eq!(projection.group_line(first), Some(0));
1641        assert_eq!(projection.group_line(second), Some(4));
1642        assert_eq!(
1643            projection.totals().get(ColumnIx::new(0)),
1644            Some(&OwnedValue::Int(3))
1645        );
1646        assert_eq!(projection.totals().get(ColumnIx::new(1)), None);
1647        assert_eq!(projection.branch(ids[0]), None);
1648
1649        let table = projection.groups();
1650        assert_eq!(table.len(), 2);
1651        let group = table.find(&path(&[2])).expect("found by path");
1652        assert_eq!(group.id(), second);
1653        assert_eq!(group.rows(), &ids[2..3]);
1654        assert_eq!((group.row_count(), group.is_expanded()), (1, false));
1655        assert_eq!(
1656            group.aggregates().get(ColumnIx::new(0)),
1657            Some(&OwnedValue::Int(2))
1658        );
1659        let first = table.get(first).unwrap();
1660        assert_eq!(first.value(), &OwnedValue::Int(1));
1661        assert_eq!(first.level_id().as_str(), "a");
1662        assert_eq!(
1663            (first.depth(), first.parent(), first.has_footer()),
1664            (0, None, true)
1665        );
1666        assert!(table.find(&path(&[3])).is_none());
1667        assert_eq!(
1668            table
1669                .iter()
1670                .map(|group| group.id().ix())
1671                .collect::<Vec<_>>(),
1672            [0, 1]
1673        );
1674
1675        // Copies for jobs keep everything.
1676        let copy = projection.duplicate();
1677        assert_eq!(copy.len(), 5);
1678        assert_eq!(copy.groups().len(), 2);
1679    }
1680}