Skip to main content

gpui_kit_grid/state/
layout.rs

1//! Saved layouts on a grid: the view id, the layout codec and merge, the
2//! schema's own defaults, and the methods that export a layout and restore a
3//! saved one.
4//!
5//! A saved layout names columns by [`ColumnId`]; the index built here once
6//! from the schema turns an id into a [`ColumnIx`]. Restoring asks the
7//! [`LayoutMerge`] for one entry per schema column, validates the result
8//! whatever the merge returned, and applies it through the column layout's
9//! own operations, so every invariant of the column layout holds afterwards.
10//!
11//! A layout also holds the grouping by columns and the expansion of groups.
12//! They are restored after the sort, on the one grouping path and the one
13//! expansion path of the state (`apply_group_by`, `apply_expansion`), with
14//! their events.
15
16use std::{cmp::Ordering, hash::Hash};
17
18use gpui_kit::{Context, Pixels, SharedString, Window, px};
19use rustc_hash::FxHashMap;
20
21use super::{
22    ColumnOf, KeyOf, LiveGridDelegate, LiveGridState,
23    columns::{ColumnLayout, ColumnZone, GridSchema, LayoutDelta, column_of},
24    events::{GroupOrigin, LayoutOrigin, LayoutPart, SortOrigin},
25};
26use crate::{
27    actions::ResetLayout,
28    engine::{ExpandDepth, Expansion, GroupBy, GroupPath, SortDirection, TreeSpec, ViewSpec},
29    model::{ColumnId, ColumnIx, ColumnSet, GridColumns, GridRow, OwnedValue},
30    persist::{
31        AppendNewColumns, GridLayout, JsonLayoutCodec, LayoutCodec, LayoutColumn, LayoutError,
32        LayoutExpansion, LayoutGroup, LayoutMerge, LayoutMergeContext, LayoutReport, LayoutSort,
33        ResolvedGrouping, check_version,
34    },
35    policy::{ColumnMovePolicy, ColumnOrder, FreeMove},
36};
37
38/// What a grid keeps for saved layouts: its view id, the codec and the
39/// merge, its schema as declared, and the view and expansion depth given to
40/// the builders.
41pub(super) struct LayoutSlot<R: GridRow> {
42    view_id: Option<SharedString>,
43    codec: Box<dyn LayoutCodec>,
44    merge: Box<dyn LayoutMerge>,
45    /// `ColumnId` → place in `schema`, for every schema column. Built once,
46    /// with the schema.
47    index: FxHashMap<&'static str, usize>,
48    /// Schema columns in the schema's own display order: declaration order
49    /// split by zone, before any move policy.
50    schema: Vec<ColumnIx>,
51    /// The same columns with their default width, visibility and zone: what
52    /// a [`LayoutMerge`] sees.
53    defaults: Vec<LayoutColumn>,
54    /// The bands of every column, by `ColumnIx`.
55    bands: Vec<Box<[&'static str]>>,
56    /// Schema columns the grid may not group by
57    /// (`GridColumn::with_groupable(false)`): a saved level on one is
58    /// dropped.
59    ungroupable: Vec<ColumnId>,
60    /// The sort given to `with_view`: the sort of the default layout.
61    view_sort: Vec<(ColumnIx, SortDirection)>,
62    /// How the view given to `with_view` arranges rows: the grouping of the
63    /// default layout, and what `reset_layout` restores, a level by a
64    /// function or a tree included.
65    view_arrangement: Arrangement<R>,
66    /// The depth given to `with_expand_depth`: the expansion of the default
67    /// layout.
68    view_depth: ExpandDepth,
69    /// The layout given to `with_layout`, applied again by every builder it
70    /// depends on. Only builders read it.
71    initial: Option<GridLayout>,
72}
73
74impl<R: GridRow> LayoutSlot<R> {
75    pub(super) fn new(schema: &GridSchema<R::Column>) -> Self {
76        let declared = ColumnLayout::new(schema.clone());
77        let columns = declared.order().to_vec();
78        let ungroupable = columns
79            .iter()
80            .filter(|ix| !declared.column(**ix).is_groupable())
81            .map(|ix| column_of::<R::Column>(*ix).column_id())
82            .collect();
83        Self {
84            view_id: None,
85            codec: Box::new(JsonLayoutCodec::default()),
86            merge: Box::new(AppendNewColumns),
87            index: columns
88                .iter()
89                .enumerate()
90                .map(|(place, ix)| (column_of::<R::Column>(*ix).id(), place))
91                .collect(),
92            defaults: columns
93                .iter()
94                .map(|ix| {
95                    layout_column::<R::Column>(
96                        *ix,
97                        declared.width(*ix),
98                        declared.is_visible(*ix),
99                        declared.zone_of(*ix),
100                    )
101                })
102                .collect(),
103            schema: columns,
104            bands: R::Column::all()
105                .iter()
106                .map(|column| declared.bands_of(column.ix()).into())
107                .collect(),
108            ungroupable,
109            view_sort: Vec::new(),
110            view_arrangement: Arrangement::Flat,
111            view_depth: ExpandDepth::default(),
112            initial: None,
113        }
114    }
115
116    /// Records the sort and the grouping or tree given to `with_view`, for
117    /// the default layout.
118    pub(super) fn set_view(&mut self, view: &ViewSpec<R>) {
119        self.view_sort = view
120            .sort()
121            .iter()
122            .map(|(column, direction)| (column.ix(), *direction))
123            .collect();
124        self.view_arrangement = Arrangement::of(view);
125    }
126
127    /// Records the depth given to `with_expand_depth`, for the default
128    /// layout.
129    pub(super) fn set_view_depth(&mut self, depth: ExpandDepth) {
130        self.view_depth = depth;
131    }
132
133    /// The schema as a [`LayoutMerge`] and the report see it.
134    fn context(&self) -> LayoutMergeContext<'_> {
135        LayoutMergeContext::new(&self.defaults).with_ungroupable(&self.ungroupable)
136    }
137
138    /// The schema column with this id.
139    fn column_ix(&self, id: &str) -> Option<ColumnIx> {
140        self.index.get(id).map(|place| self.schema[*place])
141    }
142
143    /// An empty layout with the grid's view id.
144    fn empty_layout(&self) -> GridLayout {
145        match &self.view_id {
146            Some(view_id) => GridLayout::new().with_view_id(view_id.to_string()),
147            None => GridLayout::new(),
148        }
149    }
150
151    /// Refuses a layout this build does not read, or one saved for another
152    /// view. A layout or a grid without a view id is accepted.
153    fn check(&self, layout: &GridLayout) -> Result<(), LayoutError> {
154        check_version(layout.version())?;
155        match (&self.view_id, layout.view_id()) {
156            (Some(expected), Some(found)) if expected.as_ref() != found => {
157                Err(LayoutError::ViewMismatch {
158                    expected: expected.to_string(),
159                    found: found.to_owned(),
160                })
161            }
162            _ => Ok(()),
163        }
164    }
165
166    /// Resolves `saved` against the schema: the merge, the validation, the
167    /// sort, the grouping and the expansion, and the report of what did not
168    /// fit.
169    fn resolve(
170        &self,
171        columns: &ColumnLayout<R::Column>,
172        policy: &dyn ColumnMovePolicy,
173        saved: &GridLayout,
174    ) -> (Resolved<R>, LayoutReport) {
175        let context = self.context();
176        let merged = self.merge.merge(&context, saved);
177        let mut resolved = self.validate(columns, policy, &merged);
178        resolved.sort = self.sort_keys(saved.sort());
179        // The report reads the same resolution, so it names exactly what is
180        // left out here.
181        let grouping = ResolvedGrouping::new(&context, saved);
182        let levels: Vec<GroupBy<R>> = grouping
183            .levels()
184            .iter()
185            .filter_map(|level| {
186                let ix = self.column_ix(level.column().as_str())?;
187                Some(GroupBy::column(column_of(ix)).with_direction(level.direction()))
188            })
189            .collect();
190        debug_assert_eq!(levels.len(), grouping.levels().len(), "kept levels group");
191        // A saved path holds values only: the kept levels name their columns,
192        // the top level first.
193        resolved.toggled = grouping
194            .toggled()
195            .iter()
196            .map(|path| {
197                GroupPath::new(
198                    levels
199                        .iter()
200                        .zip(path.iter())
201                        .map(|(level, value)| (level.id(), value.clone())),
202                )
203            })
204            .collect();
205        resolved.depth = grouping.depth();
206        resolved.arrangement = Arrangement::groups(levels);
207        (resolved, LayoutReport::resolve(&context, saved))
208    }
209
210    /// Makes merged columns a valid column layout, whatever the merge
211    /// returned: every schema column once (unknown ids and repeats are
212    /// dropped, missing columns end their zone in schema order with schema
213    /// defaults), widths within the column's limits (a width that is not a
214    /// positive number becomes the schema width), the order split by zone
215    /// and normalized by the move policy, and one column visible. No sort,
216    /// no grouping, the default expansion.
217    fn validate(
218        &self,
219        columns: &ColumnLayout<R::Column>,
220        policy: &dyn ColumnMovePolicy,
221        merged: &[LayoutColumn],
222    ) -> Resolved<R> {
223        let count = self.bands.len();
224        let mut order = Vec::with_capacity(self.schema.len());
225        let mut zones = vec![ColumnZone::Scrolling; count];
226        let mut widths = vec![px(0.); count];
227        let mut visible = ColumnSet::EMPTY;
228        let mut seen = ColumnSet::EMPTY;
229        // Missing columns come after the merged ones in schema order; the
230        // sort by zone below puts each at the end of its zone.
231        for entry in merged.iter().chain(&self.defaults) {
232            let Some(place) = self.index.get(entry.id().as_str()).copied() else {
233                continue;
234            };
235            let ix = self.schema[place];
236            if seen.contains(ix) {
237                continue;
238            }
239            seen.insert(ix);
240            let width = match entry.width() {
241                width if width.is_finite() && width > 0. => width,
242                _ => self.defaults[place].width(),
243            };
244            widths[ix.get()] = columns.column(ix).clamp_width(px(width));
245            zones[ix.get()] = entry.zone();
246            // An internal column stays hidden whatever the layout says.
247            if entry.is_visible() && !columns.is_internal(ix) {
248                visible.insert(ix);
249            }
250            order.push(ix);
251        }
252        sort_by_zone(&mut order, &zones);
253        policy.normalize(&mut ColumnOrder::new(&mut order, &mut zones, &self.bands));
254        sort_by_zone(&mut order, &zones);
255        if visible.is_empty()
256            && let Some(&first) = order.iter().find(|ix| !columns.is_internal(**ix))
257        {
258            visible.insert(first);
259        }
260        Resolved {
261            order,
262            zones,
263            widths,
264            visible,
265            sort: Vec::new(),
266            arrangement: Arrangement::Flat,
267            depth: ExpandDepth::default(),
268            toggled: Vec::new(),
269        }
270    }
271
272    /// The schema's own layout, resolved: its columns normalized by
273    /// `policy`, the sort and the grouping or tree of the view given to
274    /// `with_view`, and the depth given to `with_expand_depth` with no group
275    /// toggled.
276    fn default_resolved(
277        &self,
278        columns: &ColumnLayout<R::Column>,
279        policy: &dyn ColumnMovePolicy,
280    ) -> Resolved<R> {
281        let mut resolved = self.validate(columns, policy, &self.defaults);
282        resolved.sort = self.view_sort.clone();
283        resolved.arrangement = self.view_arrangement.clone();
284        resolved.depth = self.view_depth;
285        resolved
286    }
287
288    /// Saved sort keys on schema columns, each column once (the first key
289    /// wins).
290    fn sort_keys(&self, sort: &[LayoutSort]) -> Vec<(ColumnIx, SortDirection)> {
291        let mut sorted = ColumnSet::EMPTY;
292        let mut keys = Vec::with_capacity(sort.len());
293        for key in sort {
294            if let Some(ix) = self.column_ix(key.column().as_str())
295                && !sorted.contains(ix)
296            {
297                sorted.insert(ix);
298                keys.push((ix, key.direction()));
299            }
300        }
301        keys
302    }
303}
304
305/// How a restored layout arranges rows.
306enum Arrangement<R: GridRow> {
307    /// No grouping levels: grouping is off, and a tree stays (a layout saved
308    /// before grouping existed does not turn the application's tree off).
309    Flat,
310    /// Grouped by these levels, the top one first: they replace a tree (a
311    /// tree and a grouping exclude each other).
312    Groups(Vec<GroupBy<R>>),
313    /// The tree of the view given to `with_view`: no saved layout holds one
314    /// (row keys are not serializable, and the application sets up its
315    /// tree), only the default layout restores it.
316    Tree(TreeSpec<R>),
317}
318
319impl<R: GridRow> Clone for Arrangement<R> {
320    fn clone(&self) -> Self {
321        match self {
322            Self::Flat => Self::Flat,
323            Self::Groups(levels) => Self::Groups(levels.clone()),
324            Self::Tree(tree) => Self::Tree(tree.clone()),
325        }
326    }
327}
328
329impl<R: GridRow> Arrangement<R> {
330    /// The arrangement of a view.
331    fn of(view: &ViewSpec<R>) -> Self {
332        match view.tree() {
333            Some(tree) => Self::Tree(tree.clone()),
334            None => Self::groups(view.group_by().to_vec()),
335        }
336    }
337
338    /// Grouped by `levels`; flat without levels.
339    fn groups(levels: Vec<GroupBy<R>>) -> Self {
340        if levels.is_empty() {
341            Self::Flat
342        } else {
343            Self::Groups(levels)
344        }
345    }
346
347    /// The grouping levels; none when flat or a tree.
348    fn levels(&self) -> &[GroupBy<R>] {
349        match self {
350            Self::Groups(levels) => levels,
351            Self::Flat | Self::Tree(_) => &[],
352        }
353    }
354}
355
356/// A saved layout resolved against the schema and validated: what restoring
357/// applies.
358struct Resolved<R: GridRow> {
359    /// Every schema column in display order, split by zone.
360    order: Vec<ColumnIx>,
361    /// By `ColumnIx`.
362    zones: Vec<ColumnZone>,
363    /// By `ColumnIx`, within each column's limits.
364    widths: Vec<Pixels>,
365    visible: ColumnSet,
366    /// Schema columns only, each once.
367    sort: Vec<(ColumnIx, SortDirection)>,
368    /// From a saved layout, levels on groupable schema columns only, each
369    /// once; for the default layout, the view's own.
370    arrangement: Arrangement<R>,
371    /// The default depth of the expansion.
372    depth: ExpandDepth,
373    /// Groups toggled against `depth`, each a group of the levels of
374    /// `arrangement`.
375    toggled: Vec<GroupPath>,
376}
377
378impl<R: GridRow> Resolved<R> {
379    /// The resolved layout as a `GridLayout` of `slot`'s view.
380    fn to_layout(&self, slot: &LayoutSlot<R>) -> GridLayout {
381        let layout = self
382            .order
383            .iter()
384            .map(|ix| {
385                layout_column::<R::Column>(
386                    *ix,
387                    self.widths[ix.get()],
388                    self.visible.contains(*ix),
389                    self.zones[ix.get()],
390                )
391            })
392            .fold(slot.empty_layout(), GridLayout::with_column)
393            .with_sort(self.sort.iter().map(|(ix, direction)| {
394                LayoutSort::new(column_of::<R::Column>(*ix).column_id(), *direction)
395            }));
396        if let Arrangement::Tree(_) = self.arrangement {
397            return layout;
398        }
399        let levels = self.arrangement.levels();
400        let expansion = toggle_groups(
401            Expansion::<R::Key>::new(self.depth),
402            self.toggled.iter().cloned(),
403            levels.len(),
404        );
405        with_saved_grouping(layout, levels, &expansion)
406    }
407
408    /// Sets `columns` to the resolved order, zones, widths and visibility,
409    /// one column at a time through the column layout's operations. Returns
410    /// what changed; the sort, the grouping and the expansion are not
411    /// applied here.
412    fn apply(&self, columns: &mut ColumnLayout<R::Column>) -> LayoutDelta {
413        fn mark(delta: &mut LayoutDelta, part: LayoutPart, ix: ColumnIx) {
414            delta.parts = delta.parts.with(part);
415            delta.columns.insert(ix);
416        }
417
418        let before = columns.order().to_vec();
419        let mut delta = LayoutDelta::default();
420        for &ix in &self.order {
421            let zone = self.zones[ix.get()];
422            if columns.zone_of(ix) != zone {
423                // One column at a time: the order within zones is set next.
424                columns.set_zone(ix, zone, &FreeMove);
425                mark(&mut delta, LayoutPart::Zone, ix);
426            }
427        }
428        for zone in ColumnZone::ALL {
429            let members = self.order.iter().filter(|ix| self.zones[ix.get()] == zone);
430            for (gap, &ix) in members.enumerate() {
431                // The columns before `gap` are in place: `ix` is at `gap` or
432                // later, and lands at `gap`.
433                columns.move_members(zone, &[ix], gap);
434            }
435        }
436        let moved = moved_columns(&before, columns.order());
437        if !moved.is_empty() {
438            delta.parts = delta.parts.with(LayoutPart::Order);
439            delta.columns.union_with(moved);
440        }
441        for &ix in &self.order {
442            if columns.set_width(ix, self.widths[ix.get()]) {
443                mark(&mut delta, LayoutPart::Width, ix);
444            }
445        }
446        // Show before hiding: the last visible column cannot be hidden.
447        for show in [true, false] {
448            for &ix in &self.order {
449                if self.visible.contains(ix) == show && columns.set_visible(ix, show) == Some(true)
450                {
451                    mark(&mut delta, LayoutPart::Visibility, ix);
452                }
453            }
454        }
455        debug_assert_eq!(columns.order(), self.order, "the restored order");
456        delta
457    }
458}
459
460/// `layout` with the grouping `levels` and the expansion of their groups,
461/// as a layout saves them: the levels, the default depth, and the groups of
462/// these levels toggled against it in the order of their values
463/// (`OwnedValue::cmp_total`), so that a file does not change with the order
464/// of a hash set. Toggles of other groupings are not saved.
465///
466/// A grouping with a level by a function of the row (`GroupKey`) is not
467/// saved, nor its expansion: a function cannot be read back from a file, and
468/// a part of the grouping would be a wrong one.
469fn with_saved_grouping<R: GridRow>(
470    layout: GridLayout,
471    levels: &[GroupBy<R>],
472    expansion: &Expansion<R::Key>,
473) -> GridLayout {
474    let Some(groups) = levels
475        .iter()
476        .map(|level| {
477            let column = level.as_column()?;
478            Some(LayoutGroup::new(column.column_id(), level.direction()))
479        })
480        .collect::<Option<Vec<_>>>()
481    else {
482        return layout;
483    };
484    let mut toggled: Vec<Vec<OwnedValue>> = expansion
485        .toggled_groups()
486        .filter(|path| is_group_of(path, levels))
487        .map(|path| {
488            path.levels()
489                .iter()
490                .map(|(_, value)| value.clone())
491                .collect()
492        })
493        .collect();
494    toggled.sort_by(|a, b| cmp_values(a, b));
495    let expansion = toggled.into_iter().fold(
496        LayoutExpansion::new(expansion.depth()),
497        |expansion, path| expansion.with_toggled(path),
498    );
499    layout.with_group_by(groups).with_expansion(expansion)
500}
501
502/// Returns true if `path` names a group of the grouping `levels`: a group of
503/// one of its levels, whose level ids are the first ids of `levels`.
504fn is_group_of<R: GridRow>(path: &GroupPath, levels: &[GroupBy<R>]) -> bool {
505    !path.is_root()
506        && path.levels().len() <= levels.len()
507        && path
508            .levels()
509            .iter()
510            .zip(levels)
511            .all(|((id, _), level)| *id == level.id())
512}
513
514/// Orders key values of saved paths: value by value (`cmp_total`), then a
515/// shorter path first.
516fn cmp_values(a: &[OwnedValue], b: &[OwnedValue]) -> Ordering {
517    a.iter()
518        .zip(b)
519        .map(|(a, b)| a.cmp_total(b))
520        .find(|order| order.is_ne())
521        .unwrap_or_else(|| a.len().cmp(&b.len()))
522}
523
524/// Toggles each group of `paths` against the depth of `expansion`, in a
525/// grouping of `levels` levels. A group given twice stays toggled.
526fn toggle_groups<K: Clone + Eq + Hash>(
527    expansion: Expansion<K>,
528    paths: impl IntoIterator<Item = GroupPath>,
529    levels: usize,
530) -> Expansion<K> {
531    paths.into_iter().fold(expansion, |expansion, path| {
532        set_toggled(expansion, path, true, levels)
533    })
534}
535
536/// Toggles a group against the depth of `expansion`, or clears its toggle.
537fn set_toggled<K: Clone + Eq + Hash>(
538    expansion: Expansion<K>,
539    path: GroupPath,
540    toggled: bool,
541    levels: usize,
542) -> Expansion<K> {
543    // Expanded by the depth alone, before any toggle.
544    let by_default = Expansion::<()>::new(expansion.depth()).is_group_expanded(&path, levels);
545    expansion.with_group(path, by_default != toggled, levels)
546}
547
548/// One column of a layout, by the column's id.
549fn layout_column<C: GridColumns>(
550    ix: ColumnIx,
551    width: Pixels,
552    visible: bool,
553    zone: ColumnZone,
554) -> LayoutColumn {
555    LayoutColumn::new(column_of::<C>(ix).column_id(), width)
556        .with_visible(visible)
557        .with_zone(zone)
558}
559
560/// Every column of a column layout in display order, hidden ones included.
561fn layout_columns<C: GridColumns>(
562    columns: &ColumnLayout<C>,
563) -> impl Iterator<Item = LayoutColumn> + '_ {
564    columns.order().iter().map(|ix| {
565        layout_column::<C>(
566            *ix,
567            columns.width(*ix),
568            columns.is_visible(*ix),
569            columns.zone_of(*ix),
570        )
571    })
572}
573
574/// A stable sort by zone, left zone first.
575fn sort_by_zone(order: &mut [ColumnIx], zones: &[ColumnZone]) {
576    order.sort_by_key(|ix| zones[ix.get()].slot());
577}
578
579/// The columns that moved between two orders of the same columns: those
580/// outside a longest run that kept its relative order.
581fn moved_columns(before: &[ColumnIx], after: &[ColumnIx]) -> ColumnSet {
582    let mut moved = ColumnSet::EMPTY;
583    if before == after {
584        return moved;
585    }
586    let mut position = [usize::MAX; 128];
587    for (place, ix) in before.iter().enumerate() {
588        position[ix.get()] = place;
589    }
590    let places: Vec<usize> = after.iter().map(|ix| position[ix.get()]).collect();
591    // The longest increasing run of old places, by patience sorting.
592    let mut tails: Vec<usize> = Vec::new();
593    let mut previous: Vec<Option<usize>> = vec![None; places.len()];
594    for (i, place) in places.iter().enumerate() {
595        let k = tails.partition_point(|&tail| places[tail] < *place);
596        previous[i] = k.checked_sub(1).map(|k| tails[k]);
597        if k == tails.len() {
598            tails.push(i);
599        } else {
600            tails[k] = i;
601        }
602    }
603    let mut kept = ColumnSet::EMPTY;
604    let mut at = tails.last().copied();
605    while let Some(i) = at {
606        kept.insert(after[i]);
607        at = previous[i];
608    }
609    for ix in after {
610        if !kept.contains(*ix) {
611            moved.insert(*ix);
612        }
613    }
614    moved
615}
616
617impl<D: LiveGridDelegate> LiveGridState<D> {
618    /// Names the kind of grid ("trades") for saved layouts and the layout
619    /// library. Every grid of a view shares the view id; a layout saved for
620    /// another view is refused ([`LayoutError::ViewMismatch`]).
621    pub fn with_view_id(mut self, view_id: impl Into<SharedString>) -> Self {
622        self.layout.view_id = Some(view_id.into());
623        self.apply_initial_layout()
624    }
625
626    /// Applies a saved layout at construction, without events: its columns,
627    /// its sort in place of the sort of `with_view` (an empty sort too; the
628    /// filter stays), its grouping in place of the grouping of `with_view`
629    /// (no grouping too; a tree of `with_view` stays when the layout has no
630    /// grouping), and its expansion in place of the depth of
631    /// `with_expand_depth` (a tree keeps that depth). A layout this build
632    /// does not read, or one saved for another view, is ignored; call
633    /// [`Self::set_layout`] to see why. Called again, the new layout
634    /// replaces the old one.
635    ///
636    /// The builders it depends on (`with_view`, `with_view_id`,
637    /// `with_expand_depth`, `with_column_move_policy` and
638    /// `with_layout_merge`) may come before or after it: each applies the
639    /// layout again after its own change.
640    pub fn with_layout(mut self, layout: GridLayout) -> Self {
641        self.layout.initial = Some(layout);
642        self.apply_initial_layout()
643    }
644
645    /// Applies the layout given to [`Self::with_layout`] to the columns, the
646    /// sort, the grouping and the expansion, or the default layout if it is
647    /// refused; does nothing without one. Every builder the result depends
648    /// on calls it after its own change, so the builder order does not
649    /// matter. Each part is set from the layout and what the builders
650    /// recorded (`LayoutSlot`), never from an earlier application, so
651    /// nothing of one is left.
652    pub(super) fn apply_initial_layout(mut self) -> Self {
653        let Some(layout) = &self.layout.initial else {
654            return self;
655        };
656        let resolved = match self.layout.check(layout) {
657            Ok(()) => {
658                self.layout
659                    .resolve(&self.columns, &*self.column_move, layout)
660                    .0
661            }
662            Err(_) => self
663                .layout
664                .default_resolved(&self.columns, &*self.column_move),
665        };
666        // No scroll anchor: a grid being built shows its columns from the
667        // start.
668        resolved.apply(&mut self.columns);
669        let sort: Vec<(ColumnOf<D>, SortDirection)> = resolved
670            .sort
671            .iter()
672            .map(|(ix, direction)| (column_of(*ix), *direction))
673            .collect();
674        let view = std::mem::take(&mut self.view).with_sort(sort);
675        let view = match resolved.arrangement {
676            Arrangement::Groups(levels) => view.with_group_by(levels),
677            Arrangement::Tree(tree) => view.with_tree(tree),
678            // No levels keep the tree of `with_view`.
679            Arrangement::Flat => match &self.layout.view_arrangement {
680                Arrangement::Tree(tree) => view.with_tree(tree.clone()),
681                Arrangement::Flat | Arrangement::Groups(_) => view.with_group_by([]),
682            },
683        };
684        // The expansion of a tree is the tree's: a layout does not hold it,
685        // as row keys are not serializable.
686        let expansion = match view.tree() {
687            Some(_) => Expansion::new(self.layout.view_depth),
688            None => toggle_groups(
689                Expansion::new(resolved.depth),
690                resolved.toggled,
691                view.group_by().len(),
692            ),
693        };
694        // Nothing was laid out yet: no expansion revision to count.
695        self.view = view.with_expansion(expansion);
696        // A quick filter of the visible columns follows them.
697        self.recompile_quick_filter();
698        self
699    }
700
701    /// Sets the text format of [`Self::export_layout`] and
702    /// [`Self::import_layout`] (default [`JsonLayoutCodec`]).
703    pub fn with_layout_codec(mut self, codec: impl LayoutCodec) -> Self {
704        self.layout.codec = Box::new(codec);
705        self
706    }
707
708    /// Sets how a saved layout meets the current schema (default
709    /// [`AppendNewColumns`]). The grid validates what the merge returns.
710    pub fn with_layout_merge(mut self, merge: impl LayoutMerge) -> Self {
711        self.layout.merge = Box::new(merge);
712        self.apply_initial_layout()
713    }
714
715    /// The view id given to [`Self::with_view_id`].
716    pub fn view_id(&self) -> Option<&SharedString> {
717        self.layout.view_id.as_ref()
718    }
719
720    /// The current layout: every schema column in display order (hidden ones
721    /// at their place) with its width in logical pixels, visibility and
722    /// zone, the sort, the grouping levels, and the expansion of groups: the
723    /// default depth ([`Self::expand_depth`]) and the groups of the current
724    /// grouping toggled against it. The quick filter, scrolling, selection
725    /// and focus are not part of it.
726    ///
727    /// A grouping with a level by a function of the row (`GroupKey`) is not
728    /// saved, nor the expansion: a function cannot be read back from a file.
729    /// A tree is not saved either, nor its expansion: the application sets
730    /// its tree, and row keys are the application's own type. Toggled groups
731    /// of another grouping are kept by the grid until it is dropped, not
732    /// saved.
733    ///
734    /// Reads state only, so it works for a grid that was never painted, such
735    /// as one in a hidden dock tab.
736    pub fn layout(&self) -> GridLayout {
737        let layout = layout_columns(&self.columns)
738            .fold(self.layout.empty_layout(), GridLayout::with_column)
739            .with_sort(
740                self.view
741                    .sort()
742                    .iter()
743                    .map(|(column, direction)| LayoutSort::new(column.column_id(), *direction)),
744            );
745        match self.view.tree() {
746            Some(_) => layout,
747            None => with_saved_grouping(layout, self.view.group_by(), self.view.expansion()),
748        }
749    }
750
751    /// The layout of the schema: its order normalized by the column move
752    /// policy, its widths, visibility and zones, the sort and the grouping
753    /// of the view given to `with_view`, and the depth given to
754    /// `with_expand_depth` with no group toggled. Saved as [`Self::layout`]
755    /// saves: without a grouping by a function, or a tree.
756    /// [`Self::reset_layout`] restores it.
757    pub fn default_layout(&self) -> GridLayout {
758        self.layout
759            .default_resolved(&self.columns, &*self.column_move)
760            .to_layout(&self.layout)
761    }
762
763    /// The current layout as text, through the layout codec.
764    pub fn export_layout(&self) -> Result<String, LayoutError> {
765        self.layout.codec.encode(&self.layout())
766    }
767
768    /// Restores a saved layout: the columns, then the sort, the grouping and
769    /// the expansion. Saved columns the schema no longer has are ignored and
770    /// new schema columns are added (by the layout merge, default
771    /// [`AppendNewColumns`]); the report lists both. Widths are kept within
772    /// the columns' limits.
773    ///
774    /// Grouping levels on columns the schema no longer has, cannot group or
775    /// repeat are dropped, and the levels below move up; toggled groups
776    /// deeper than the levels left are dropped (the report lists both). A
777    /// layout without grouping turns grouping off and leaves a tree alone; a
778    /// layout with grouping replaces a tree. The expansion of a tree is not
779    /// restored: a layout does not hold it. The toggles of other groupings
780    /// stay while the depth is the same.
781    ///
782    /// Rebuilds the order once if the sort or the grouping changed, or the
783    /// visible columns that a quick filter searches; a new expansion alone
784    /// only lays the lines out again. Emits, when everything has changed:
785    /// `OrderHeldChange { held: false }` if that rebuild drops a held order,
786    /// `SortChange` if the sort changed, `GroupChange` if the grouping did,
787    /// `ExpansionChange { target: None }` if the expansion did,
788    /// `LayoutChange` if the columns changed (all `Programmatic`), then
789    /// `FocusChange` if the focused column is now hidden. A layout equal to
790    /// the current one emits nothing. Fails, changing nothing, for a version
791    /// this build does not read or a layout of another view.
792    pub fn set_layout(
793        &mut self,
794        layout: &GridLayout,
795        cx: &mut Context<Self>,
796    ) -> Result<LayoutReport, LayoutError> {
797        self.restore_layout(layout, LayoutOrigin::Programmatic, cx)
798    }
799
800    /// Reads a layout through the layout codec and restores it with
801    /// [`Self::set_layout`].
802    pub fn import_layout(
803        &mut self,
804        text: &str,
805        cx: &mut Context<Self>,
806    ) -> Result<LayoutReport, LayoutError> {
807        let layout = self.layout.codec.decode(text)?;
808        self.set_layout(&layout, cx)
809    }
810
811    /// Restores [`Self::default_layout`] as [`Self::set_layout`] does, with
812    /// the grouping or tree of the view given to `with_view` as it was given:
813    /// a level by a function too. The `ResetLayout` Action does the same with
814    /// the `User` origin.
815    pub fn reset_layout(&mut self, cx: &mut Context<Self>) {
816        self.reset_layout_from(LayoutOrigin::Programmatic, cx);
817    }
818
819    fn reset_layout_from(&mut self, origin: LayoutOrigin, cx: &mut Context<Self>) {
820        let resolved = self
821            .layout
822            .default_resolved(&self.columns, &*self.column_move);
823        self.apply_resolved(resolved, origin, cx);
824    }
825
826    fn restore_layout(
827        &mut self,
828        layout: &GridLayout,
829        origin: LayoutOrigin,
830        cx: &mut Context<Self>,
831    ) -> Result<LayoutReport, LayoutError> {
832        self.layout.check(layout)?;
833        let (resolved, report) = self
834            .layout
835            .resolve(&self.columns, &*self.column_move, layout);
836        self.apply_resolved(resolved, origin, cx);
837        Ok(report)
838    }
839
840    /// Applies a resolved layout in this order: the columns without an
841    /// event, the sort, the grouping, the expansion, last the
842    /// `LayoutChange`. Each event comes when everything has changed, and one
843    /// rebuild covers all of it.
844    fn apply_resolved(
845        &mut self,
846        resolved: Resolved<D::Row>,
847        origin: LayoutOrigin,
848        cx: &mut Context<Self>,
849    ) {
850        let visible = self.columns.visible_set();
851        // The columns first, without an event, keeping the scroll anchor.
852        let delta = self.relayout(|columns, _| resolved.apply(columns));
853        // A quick filter of the visible columns follows them. Compiled
854        // before the sort, so that one rebuild covers both.
855        let refilter = self.columns.visible_set() != visible && self.follow_visible_columns();
856        // Then the sort and the grouping, as user actions of their paths.
857        let (sort_origin, group_origin) = match origin {
858            LayoutOrigin::User => (SortOrigin::User, GroupOrigin::User),
859            LayoutOrigin::Programmatic => (SortOrigin::Programmatic, GroupOrigin::Programmatic),
860        };
861        let sorted = self.apply_sort(
862            resolved
863                .sort
864                .iter()
865                .map(|(ix, direction)| (column_of(*ix), *direction)),
866            sort_origin,
867            cx,
868        );
869        let grouped = self.apply_arrangement(resolved.arrangement, group_origin, cx);
870        if refilter && !sorted && !grouped {
871            // The rebuild a sort change would have started. The scroll
872            // position stays: the columns changed, not the view.
873            self.restart_projection(cx);
874        }
875        // Then the expansion, in one change: the pending rebuild lays it out.
876        // The expansion of a tree is the tree's.
877        if self.view.tree().is_none() {
878            let expansion = self.restored_expansion(resolved.depth, resolved.toggled);
879            self.apply_expansion(expansion, None, group_origin, cx);
880        }
881        if !delta.is_empty() {
882            // A focus on a column the layout hid moves off it after the
883            // `LayoutChange`.
884            self.emit_layout_change(delta, origin, cx);
885            cx.notify();
886        }
887    }
888
889    /// Restores a grouping on the one grouping path (`apply_group_by`) or
890    /// a tree on the one tree path (`apply_tree`): `GroupChange` if it
891    /// changed, after an `OrderHeldChange { held: false }` if a held order
892    /// is dropped. Returns true if it changed.
893    fn apply_arrangement(
894        &mut self,
895        arrangement: Arrangement<D::Row>,
896        origin: GroupOrigin,
897        cx: &mut Context<Self>,
898    ) -> bool {
899        match arrangement {
900            Arrangement::Flat => self.apply_group_by(Vec::new(), origin, cx),
901            Arrangement::Groups(levels) => self.apply_group_by(levels, origin, cx),
902            // The tree of `with_view`, back after a grouping replaced it: only
903            // `reset_layout` restores a tree, on the one tree path.
904            Arrangement::Tree(tree) => {
905                if self
906                    .view
907                    .tree()
908                    .is_some_and(|shown| shown.column() == tree.column())
909                {
910                    return false;
911                }
912                self.apply_tree(Some(tree), origin, cx)
913            }
914        }
915    }
916
917    /// The expansion a restored layout leaves on the grouping now in the
918    /// view: the groups of `toggled` toggled against `depth`. The same depth
919    /// keeps the toggles of other groupings and of tree rows, so going back
920    /// to a grouping still brings its expansion back; a new depth clears
921    /// them, as `set_expand_depth` does.
922    fn restored_expansion(
923        &self,
924        depth: ExpandDepth,
925        toggled: Vec<GroupPath>,
926    ) -> Expansion<KeyOf<D>> {
927        let current = self.view.expansion();
928        let levels = self.view.group_by();
929        let kept = if current.depth() == depth {
930            let own: Vec<GroupPath> = current
931                .toggled_groups()
932                .filter(|path| is_group_of(path, levels))
933                .cloned()
934                .collect();
935            own.into_iter().fold(current.clone(), |expansion, path| {
936                set_toggled(expansion, path, false, levels.len())
937            })
938        } else {
939            Expansion::new(depth)
940        };
941        toggle_groups(kept, toggled, levels.len())
942    }
943
944    pub(super) fn on_reset_layout(
945        &mut self,
946        _: &ResetLayout,
947        _: &mut Window,
948        cx: &mut Context<Self>,
949    ) {
950        self.reset_layout_from(LayoutOrigin::User, cx);
951    }
952}
953
954#[cfg(test)]
955mod tests {
956    use gpui_kit::px;
957
958    use super::{Arrangement, LayoutSlot, cmp_values, moved_columns, with_saved_grouping};
959    use crate::{
960        engine::{ExpandDepth, Expansion, GroupBy, GroupKey, GroupPath, SortDirection},
961        model::{CellValue, ColumnIx, ColumnSet, GridColumns, GridRow, OwnedValue},
962        persist::{
963            GridLayout, LayoutColumn, LayoutExpansion, LayoutGroup, LayoutMerge,
964            LayoutMergeContext, LayoutSort,
965        },
966        policy::{BandedMove, ColumnMovePolicy, FreeMove},
967        state::{
968            ColumnZone, MIN_COLUMN_WIDTH,
969            columns::{ColumnBand, ColumnLayout, GridColumn, GridSchema},
970            events::LayoutPart,
971        },
972    };
973
974    /// Up to 16 columns `c0`..`c15`.
975    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
976    struct TestColumn(u16);
977
978    static ALL: [TestColumn; 16] = {
979        let mut all = [TestColumn(0); 16];
980        let mut ix = 0;
981        while ix < 16 {
982            all[ix] = TestColumn(ix as u16);
983            ix += 1;
984        }
985        all
986    };
987
988    const IDS: [&str; 16] = [
989        "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13",
990        "c14", "c15",
991    ];
992    const BANDS: [&str; 12] = [
993        "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "b10", "b11",
994    ];
995
996    impl GridColumns for TestColumn {
997        fn all() -> &'static [Self] {
998            &ALL
999        }
1000
1001        fn id(self) -> &'static str {
1002            IDS[self.0 as usize]
1003        }
1004
1005        fn ix(self) -> ColumnIx {
1006            ColumnIx::new(self.0)
1007        }
1008    }
1009
1010    /// Rows of [`TestColumn`]s: a slot reads the row's types only.
1011    struct TestRow;
1012
1013    impl GridRow for TestRow {
1014        type Key = u32;
1015        type Column = TestColumn;
1016
1017        fn key(&self) -> &u32 {
1018            &0
1019        }
1020
1021        fn value(&self, _: TestColumn) -> CellValue<'_> {
1022            CellValue::Empty
1023        }
1024    }
1025
1026    fn ix(i: u16) -> ColumnIx {
1027        ColumnIx::new(i)
1028    }
1029
1030    fn column(i: u16) -> GridColumn<TestColumn> {
1031        GridColumn::new(TestColumn(i), IDS[i as usize])
1032    }
1033
1034    /// A column layout as a grid builds it: the schema normalized by
1035    /// `policy`.
1036    fn built(
1037        schema: &GridSchema<TestColumn>,
1038        policy: &dyn ColumnMovePolicy,
1039    ) -> ColumnLayout<TestColumn> {
1040        let mut columns = ColumnLayout::new(schema.clone());
1041        columns.normalize(policy);
1042        columns
1043    }
1044
1045    fn exported(columns: &ColumnLayout<TestColumn>) -> Vec<LayoutColumn> {
1046        super::layout_columns(columns).collect()
1047    }
1048
1049    fn order(columns: &ColumnLayout<TestColumn>) -> Vec<&'static str> {
1050        columns
1051            .order()
1052            .iter()
1053            .map(|ix| IDS[ix.get()])
1054            .collect::<Vec<_>>()
1055    }
1056
1057    fn ids(ids: &[crate::model::ColumnId]) -> Vec<&str> {
1058        ids.iter().map(|id| id.as_str()).collect()
1059    }
1060
1061    /// A merge that ignores the rules: it returns `self.0` as it is.
1062    struct Verbatim(Vec<LayoutColumn>);
1063
1064    impl LayoutMerge for Verbatim {
1065        fn merge(&self, _: &LayoutMergeContext<'_>, _: &GridLayout) -> Vec<LayoutColumn> {
1066            self.0.clone()
1067        }
1068    }
1069
1070    #[test]
1071    fn test_layout_validation() {
1072        // c0 frozen left; band b0 over c1, c2 (c2 has limits); c3; band b1
1073        // over c4, c5; c6 frozen right; c7.
1074        let schema = GridSchema::new()
1075            .column(
1076                column(0)
1077                    .with_width(px(50.))
1078                    .with_zone(ColumnZone::FrozenLeft),
1079            )
1080            .band(
1081                ColumnBand::new("b0", "B0").column(column(1)).column(
1082                    column(2)
1083                        .with_width(px(80.))
1084                        .with_min_width(px(40.))
1085                        .with_max_width(px(120.)),
1086                ),
1087            )
1088            .column(column(3).with_width(px(70.)))
1089            .band(
1090                ColumnBand::new("b1", "B1")
1091                    .column(column(4))
1092                    .column(column(5)),
1093            )
1094            .column(
1095                column(6)
1096                    .with_width(px(60.))
1097                    .with_zone(ColumnZone::FrozenRight),
1098            )
1099            .column(column(7).with_width(px(40.)));
1100        let mut columns = built(&schema, &BandedMove);
1101        let mut slot = LayoutSlot::<TestRow>::new(&schema);
1102        assert_eq!(slot.column_ix("c4"), Some(ix(4)));
1103        assert_eq!(slot.column_ix("c8"), None, "not in the schema");
1104
1105        // A merge that breaks every rule: an unknown id, repeats, c0, c1, c3
1106        // and c7 missing, bands split across zones and broken up, widths out
1107        // of the limits or not numbers, every column hidden.
1108        let hidden =
1109            |id: &'static str, width: f32| LayoutColumn::new(id, width).with_visible(false);
1110        slot.merge = Box::new(Verbatim(vec![
1111            hidden("c5", f32::NAN).with_zone(ColumnZone::FrozenLeft),
1112            hidden("gone", 70.),
1113            hidden("c2", 500.),
1114            hidden("c4", 3.),
1115            hidden("c2", 90.),
1116            hidden("c6", -5.).with_zone(ColumnZone::FrozenRight),
1117            hidden("c5", 77.),
1118        ]));
1119        let saved = GridLayout::new()
1120            .with_column(LayoutColumn::new("c3", 70.))
1121            .with_column(LayoutColumn::new("gone", 70.))
1122            .with_sort([
1123                LayoutSort::new("c4", SortDirection::Descending),
1124                LayoutSort::new("gone", SortDirection::Ascending),
1125                LayoutSort::new("c4", SortDirection::Ascending),
1126                LayoutSort::new("c0", SortDirection::Ascending),
1127            ]);
1128        let (resolved, report) = slot.resolve(&columns, &BandedMove, &saved);
1129
1130        // Every schema column once; the missing ones end their zone in
1131        // schema order (c3 before c7). Band b1 takes the zone of its first
1132        // column (c5, frozen left) and gathers there; band b0 gathers at c2.
1133        let resolved_order: Vec<&str> = resolved.order.iter().map(|ix| IDS[ix.get()]).collect();
1134        assert_eq!(
1135            resolved_order,
1136            ["c5", "c4", "c0", "c2", "c1", "c3", "c7", "c6"]
1137        );
1138        assert_eq!(resolved.zones[4], ColumnZone::FrozenLeft);
1139        // Widths within the limits; NaN and negative give the schema width.
1140        assert_eq!(resolved.widths[2], px(120.), "max width");
1141        assert_eq!(resolved.widths[4], MIN_COLUMN_WIDTH, "min width");
1142        assert_eq!(resolved.widths[5], px(100.), "NaN: schema width");
1143        assert_eq!(resolved.widths[6], px(60.), "negative: schema width");
1144        // The missing columns come with their schema visibility.
1145        assert_eq!(resolved.visible, set([0, 1, 3, 7]));
1146        // Sort keys on schema columns, the first key of a column wins.
1147        assert_eq!(
1148            resolved.sort,
1149            [
1150                (ix(4), SortDirection::Descending),
1151                (ix(0), SortDirection::Ascending)
1152            ]
1153        );
1154        // The report compares the saved layout with the schema, not the
1155        // merge's result.
1156        assert_eq!(ids(report.ignored_columns()), ["gone"]);
1157        assert_eq!(
1158            ids(report.added_columns()),
1159            ["c0", "c1", "c2", "c4", "c5", "c7", "c6"]
1160        );
1161        assert_eq!(ids(report.dropped_sort()), ["gone", "c4"]);
1162
1163        // Applying gives exactly the resolved layout.
1164        let delta = resolved.apply(&mut columns);
1165        assert_eq!(order(&columns), resolved_order);
1166        assert_eq!(columns.width(ix(2)), px(120.));
1167        assert_eq!(columns.zone_of(ix(4)), ColumnZone::FrozenLeft);
1168        assert_eq!(columns.visible_set(), set([0, 1, 3, 7]));
1169        for part in [
1170            LayoutPart::Width,
1171            LayoutPart::Order,
1172            LayoutPart::Visibility,
1173            LayoutPart::Zone,
1174        ] {
1175            assert!(delta.parts.contains(part), "{part:?}");
1176        }
1177
1178        // With every column hidden and none missing, the first in display
1179        // order is shown.
1180        slot.merge = Box::new(Verbatim(
1181            (0..8)
1182                .map(|i| hidden(IDS[i], 50.))
1183                .collect::<Vec<LayoutColumn>>(),
1184        ));
1185        let (resolved, _) = slot.resolve(&columns, &FreeMove, &GridLayout::new());
1186        assert_eq!(resolved.order[0], ix(0));
1187        assert_eq!(resolved.visible, set([0]));
1188        resolved.apply(&mut columns);
1189        assert_eq!(columns.visible_set(), set([0]));
1190
1191        // A layout equal to the current one changes nothing.
1192        let (resolved, _) = slot.resolve(&columns, &FreeMove, &GridLayout::new());
1193        assert!(resolved.apply(&mut columns).is_empty());
1194    }
1195
1196    fn set(columns: impl IntoIterator<Item = u16>) -> ColumnSet {
1197        columns.into_iter().fold(ColumnSet::EMPTY, |mut set, i| {
1198            set.insert(ix(i));
1199            set
1200        })
1201    }
1202
1203    #[test]
1204    fn test_moved_columns() {
1205        let order = |ids: &[u16]| ids.iter().map(|i| ix(*i)).collect::<Vec<_>>();
1206        let before = order(&[0, 1, 2, 3, 4]);
1207        assert!(moved_columns(&before, &before).is_empty());
1208        // One column dragged to the front: only it moved.
1209        assert_eq!(moved_columns(&before, &order(&[4, 0, 1, 2, 3])), set([4]));
1210        assert_eq!(moved_columns(&before, &order(&[1, 2, 0, 3, 4])), set([0]));
1211        // Two swapped: one of them moved.
1212        assert_eq!(moved_columns(&before, &order(&[0, 3, 2, 1, 4])).len(), 2);
1213        assert_eq!(
1214            moved_columns(&before, &order(&[4, 3, 2, 1, 0])).len(),
1215            4,
1216            "a reversal keeps one column"
1217        );
1218    }
1219
1220    /// A random schema of up to 12 columns `c0`.. in bands up to two deep,
1221    /// with random zones, widths, limits and visibility.
1222    fn random_schema(rng: &mut fastrand::Rng) -> (GridSchema<TestColumn>, usize) {
1223        fn items(
1224            rng: &mut fastrand::Rng,
1225            level: usize,
1226            next_column: &mut u16,
1227            next_band: &mut usize,
1228            limit: u16,
1229        ) -> Vec<Result<GridColumn<TestColumn>, ColumnBand<TestColumn>>> {
1230            let mut out = Vec::new();
1231            for _ in 0..rng.usize(1..=4) {
1232                if *next_column >= limit {
1233                    break;
1234                }
1235                if level < 2 && *next_band < BANDS.len() && rng.u8(..3) == 0 {
1236                    let id = BANDS[*next_band];
1237                    *next_band += 1;
1238                    let children = items(rng, level + 1, next_column, next_band, limit);
1239                    out.push(Err(children.into_iter().fold(
1240                        ColumnBand::new(id, id),
1241                        |band, child| match child {
1242                            Ok(column) => band.column(column),
1243                            Err(nested) => band.band(nested),
1244                        },
1245                    )));
1246                } else {
1247                    let i = *next_column;
1248                    *next_column += 1;
1249                    out.push(Ok(random_column(rng, i)));
1250                }
1251            }
1252            out
1253        }
1254        let limit = rng.u16(1..=12);
1255        let (mut next_column, mut next_band) = (0, 0);
1256        let mut schema = GridSchema::new();
1257        while next_column < limit {
1258            for item in items(rng, 0, &mut next_column, &mut next_band, limit) {
1259                schema = match item {
1260                    Ok(column) => schema.column(column),
1261                    Err(band) => schema.band(band),
1262                };
1263            }
1264        }
1265        (schema, next_band)
1266    }
1267
1268    fn random_column(rng: &mut fastrand::Rng, i: u16) -> GridColumn<TestColumn> {
1269        let zone = match rng.u8(..6) {
1270            0 => ColumnZone::FrozenLeft,
1271            1 => ColumnZone::FrozenRight,
1272            _ => ColumnZone::Scrolling,
1273        };
1274        let column = column(i)
1275            .with_width(px(rng.u16(10..200) as f32))
1276            .with_zone(zone)
1277            .with_visible(rng.u8(..5) != 0);
1278        match rng.u8(..4) {
1279            0 => column.with_min_width(px(rng.u16(10..80) as f32)),
1280            1 => column.with_max_width(px(rng.u16(80..250) as f32)),
1281            _ => column,
1282        }
1283    }
1284
1285    /// The naive model of restoring `saved` on a schema without bands:
1286    /// saved columns the schema has, in order, the first of a repeat;
1287    /// then the schema's other columns with their defaults; split by zone;
1288    /// one column visible. Widths within the schema's limits. A schema that
1289    /// hides every column shows its first one (in its own display order) by
1290    /// default, as a grid builds it.
1291    fn reference_restore(
1292        schema: &[GridColumn<TestColumn>],
1293        saved: &GridLayout,
1294    ) -> Vec<LayoutColumn> {
1295        let find = |id: &str| schema.iter().find(|c| c.column().id() == id);
1296        let mut schema_order: Vec<&GridColumn<TestColumn>> = schema.iter().collect();
1297        schema_order.sort_by_key(|c| c.zone().slot());
1298        let shown_by_default =
1299            (!schema.iter().any(GridColumn::is_visible)).then(|| schema_order[0].column());
1300        let mut out: Vec<LayoutColumn> = Vec::new();
1301        let mut push = |entry: LayoutColumn, column: &GridColumn<TestColumn>| {
1302            if out.iter().any(|c| c.id() == entry.id()) {
1303                return;
1304            }
1305            let min = f32::from(column.min_width().max(MIN_COLUMN_WIDTH));
1306            let width = entry.width().min(f32::from(column.max_width())).max(min);
1307            out.push(entry.with_width(width));
1308        };
1309        for entry in saved.columns() {
1310            if let Some(column) = find(entry.id().as_str()) {
1311                push(entry.clone(), column);
1312            }
1313        }
1314        for column in schema {
1315            let visible = column.is_visible() || shown_by_default == Some(column.column());
1316            let entry = LayoutColumn::new(column.column().id(), column.width())
1317                .with_visible(visible)
1318                .with_zone(column.zone());
1319            push(entry, column);
1320        }
1321        out.sort_by_key(|c| c.zone().slot());
1322        if !out.iter().any(LayoutColumn::is_visible) {
1323            out[0] = out[0].clone().with_visible(true);
1324        }
1325        out
1326    }
1327
1328    #[test]
1329    fn test_layout_round_trip_matches_reference() {
1330        for seed in 0..60 {
1331            for banded in [true, false] {
1332                let mut rng = fastrand::Rng::with_seed(seed);
1333                let policy: &dyn ColumnMovePolicy = if banded { &BandedMove } else { &FreeMove };
1334                let (schema, _) = random_schema(&mut rng);
1335                let slot = LayoutSlot::<TestRow>::new(&schema);
1336                let mut columns = built(&schema, policy);
1337                let all: Vec<ColumnIx> = columns.order().to_vec();
1338                for step in 0..40 {
1339                    // A random operation of the column layout.
1340                    let target = all[rng.usize(..all.len())];
1341                    let zone = columns.zone_of(target);
1342                    match rng.u8(..5) {
1343                        0 => {
1344                            let before = Some(all[rng.usize(..all.len())])
1345                                .filter(|before| columns.zone_of(*before) == zone);
1346                            columns.move_before(zone, vec![target], before, policy);
1347                        }
1348                        1 => {
1349                            columns.set_zone(target, ColumnZone::ALL[rng.usize(..3)], policy);
1350                        }
1351                        2 => {
1352                            columns.set_visible(target, !columns.is_visible(target));
1353                        }
1354                        3 => {
1355                            columns.set_width(target, px(rng.u16(0..300) as f32));
1356                        }
1357                        _ => {
1358                            if let Some(&band) = columns.bands_of(target).first() {
1359                                let members = columns.band_members(zone, band);
1360                                let before = Some(all[rng.usize(..all.len())])
1361                                    .filter(|before| columns.zone_of(*before) == zone);
1362                                columns.move_before(zone, members, before, policy);
1363                            }
1364                        }
1365                    }
1366
1367                    // Export, apply to a new grid of the schema, export: the
1368                    // same layout, and an exact report.
1369                    let saved = exported(&columns)
1370                        .into_iter()
1371                        .fold(GridLayout::new(), GridLayout::with_column);
1372                    let mut restored = built(&schema, policy);
1373                    let (resolved, report) = slot.resolve(&restored, policy, &saved);
1374                    resolved.apply(&mut restored);
1375                    assert_eq!(
1376                        exported(&restored),
1377                        saved.columns(),
1378                        "seed {seed} step {step} banded {banded}"
1379                    );
1380                    assert!(report.is_exact());
1381                    // Applied to itself, the layout changes nothing.
1382                    let (resolved, _) = slot.resolve(&columns, policy, &saved);
1383                    assert!(resolved.apply(&mut columns).is_empty());
1384                }
1385
1386                // A changed schema without bands: some columns removed, new
1387                // ones (`c12`..) added, in a random order with new defaults.
1388                let saved = exported(&columns)
1389                    .into_iter()
1390                    .fold(GridLayout::new(), GridLayout::with_column)
1391                    .with_sort(
1392                        (0..rng.usize(..4))
1393                            .map(|_| {
1394                                let i = rng.u16(..16);
1395                                LayoutSort::new(IDS[i as usize], SortDirection::Ascending)
1396                            })
1397                            .collect::<Vec<_>>(),
1398                    );
1399                let mut kept: Vec<u16> = (0..16)
1400                    .filter(|i| {
1401                        let kept = all.contains(&ix(*i)) && rng.u8(..4) != 0;
1402                        kept || (*i >= 12 && rng.bool())
1403                    })
1404                    .collect();
1405                if kept.is_empty() {
1406                    kept.push(12);
1407                }
1408                let mut changed: Vec<GridColumn<TestColumn>> = kept
1409                    .into_iter()
1410                    .map(|i| random_column(&mut rng, i))
1411                    .collect();
1412                rng.shuffle(&mut changed);
1413                let new_schema = changed
1414                    .iter()
1415                    .cloned()
1416                    .fold(GridSchema::new(), GridSchema::column);
1417                let new_slot = LayoutSlot::<TestRow>::new(&new_schema);
1418                let mut restored = built(&new_schema, policy);
1419                let (resolved, report) = new_slot.resolve(&restored, policy, &saved);
1420                resolved.apply(&mut restored);
1421                assert_eq!(
1422                    exported(&restored),
1423                    reference_restore(&changed, &saved),
1424                    "seed {seed} banded {banded}"
1425                );
1426                let known = |id: &str| changed.iter().any(|c| c.column().id() == id);
1427                let mut ignored: Vec<&str> = Vec::new();
1428                for column in saved.columns() {
1429                    let id = column.id().as_str();
1430                    if !known(id) && !ignored.contains(&id) {
1431                        ignored.push(id);
1432                    }
1433                }
1434                assert_eq!(ids(report.ignored_columns()), ignored);
1435                let mut sorted: Vec<ColumnIx> = Vec::new();
1436                for key in saved.sort() {
1437                    let i = IDS.iter().position(|id| *id == key.column().as_str());
1438                    let i = ix(i.expect("a test id") as u16);
1439                    if known(key.column().as_str()) && !sorted.contains(&i) {
1440                        sorted.push(i);
1441                    }
1442                }
1443                let resolved_sort: Vec<ColumnIx> =
1444                    resolved.sort.iter().map(|(ix, _)| *ix).collect();
1445                assert_eq!(resolved_sort, sorted);
1446            }
1447        }
1448    }
1449
1450    fn text(value: &str) -> OwnedValue {
1451        OwnedValue::Text(value.into())
1452    }
1453
1454    /// A group path on columns of [`TestColumn`].
1455    fn path(levels: &[(u16, OwnedValue)]) -> GroupPath {
1456        GroupPath::new(
1457            levels
1458                .iter()
1459                .map(|(i, value)| (TestColumn(*i).column_id(), value.clone())),
1460        )
1461    }
1462
1463    fn by(i: u16, direction: SortDirection) -> GroupBy<TestRow> {
1464        GroupBy::column(TestColumn(i)).with_direction(direction)
1465    }
1466
1467    #[test]
1468    fn test_layout_grouping_resolution() {
1469        use SortDirection::{Ascending, Descending};
1470
1471        // `c4` refuses grouping.
1472        let schema = (0..6).fold(GridSchema::new(), |schema, i| {
1473            schema.column(column(i).with_groupable(i != 4))
1474        });
1475        let columns = built(&schema, &FreeMove);
1476        let mut slot = LayoutSlot::<TestRow>::new(&schema);
1477
1478        // An unknown level, a level that cannot group and a repeat are
1479        // dropped; the levels below move up. Paths deeper than the two kept
1480        // levels and empty ones are dropped; the rest keep their values.
1481        let saved = GridLayout::new()
1482            .with_group_by([
1483                LayoutGroup::new("c1", Ascending),
1484                LayoutGroup::new("gone", Ascending),
1485                LayoutGroup::new("c4", Descending),
1486                LayoutGroup::new("c2", Descending),
1487                LayoutGroup::new("c1", Descending),
1488            ])
1489            .with_expansion(
1490                LayoutExpansion::new(ExpandDepth::Levels(1))
1491                    .with_toggled([text("b")])
1492                    .with_toggled([text("a"), OwnedValue::Int(12)])
1493                    .with_toggled([text("b"), text("y"), text("z")])
1494                    .with_toggled([])
1495                    .with_toggled([text("a")]),
1496            );
1497        let (resolved, report) = slot.resolve(&columns, &FreeMove, &saved);
1498        let levels: Vec<(Option<TestColumn>, SortDirection)> = resolved
1499            .arrangement
1500            .levels()
1501            .iter()
1502            .map(|level| (level.as_column(), level.direction()))
1503            .collect();
1504        assert_eq!(
1505            levels,
1506            [
1507                (Some(TestColumn(1)), Ascending),
1508                (Some(TestColumn(2)), Descending)
1509            ]
1510        );
1511        assert_eq!(
1512            resolved.toggled,
1513            [
1514                path(&[(1, text("b"))]),
1515                path(&[(1, text("a")), (2, OwnedValue::Int(12))]),
1516                path(&[(1, text("a"))]),
1517            ]
1518        );
1519        assert_eq!(resolved.depth, ExpandDepth::Levels(1));
1520        assert_eq!(ids(report.dropped_group_by()), ["gone", "c4", "c1"]);
1521        assert_eq!(report.dropped_expansion(), 2);
1522
1523        // Written back: the kept levels, the paths in the order of their
1524        // values.
1525        let layout = resolved.to_layout(&slot);
1526        assert_eq!(
1527            layout.group_by(),
1528            [
1529                LayoutGroup::new("c1", Ascending),
1530                LayoutGroup::new("c2", Descending)
1531            ]
1532        );
1533        assert_eq!(
1534            layout.expansion(),
1535            Some(
1536                &LayoutExpansion::new(ExpandDepth::Levels(1))
1537                    .with_toggled([text("a")])
1538                    .with_toggled([text("a"), OwnedValue::Int(12)])
1539                    .with_toggled([text("b")])
1540            )
1541        );
1542
1543        // Without levels a layout keeps its depth and no toggled group.
1544        let flat = GridLayout::new()
1545            .with_expansion(LayoutExpansion::new(ExpandDepth::Captions).with_toggled([text("a")]));
1546        let (resolved, report) = slot.resolve(&columns, &FreeMove, &flat);
1547        assert!(matches!(resolved.arrangement, Arrangement::Flat));
1548        assert_eq!(resolved.depth, ExpandDepth::Captions);
1549        assert!(resolved.toggled.is_empty());
1550        assert_eq!(report.dropped_expansion(), 1);
1551        let layout = resolved.to_layout(&slot);
1552        assert!(layout.group_by().is_empty());
1553        assert_eq!(
1554            layout.expansion(),
1555            Some(&LayoutExpansion::new(ExpandDepth::Captions))
1556        );
1557
1558        // The default layout: the grouping of `with_view` and the depth of
1559        // `with_expand_depth`, nothing toggled.
1560        slot.set_view(&crate::engine::ViewSpec::new().with_group_by([by(3, Descending)]));
1561        slot.set_view_depth(ExpandDepth::Levels(0));
1562        let default = slot.default_resolved(&columns, &FreeMove).to_layout(&slot);
1563        assert_eq!(default.group_by(), [LayoutGroup::new("c3", Descending)]);
1564        assert_eq!(
1565            default.expansion(),
1566            Some(&LayoutExpansion::new(ExpandDepth::Levels(0)))
1567        );
1568        // A level by a function: neither the grouping nor the expansion.
1569        let bucket = GroupKey::new("bucket", ColumnSet::EMPTY, |_: &TestRow| CellValue::Empty);
1570        slot.set_view(
1571            &crate::engine::ViewSpec::new().with_group_by([by(3, Ascending), GroupBy::key(bucket)]),
1572        );
1573        let default = slot.default_resolved(&columns, &FreeMove).to_layout(&slot);
1574        assert!(default.group_by().is_empty());
1575        assert_eq!(default.expansion(), None);
1576        // A tree: no expansion either.
1577        slot.set_view(&crate::engine::ViewSpec::new().with_tree(crate::engine::TreeSpec::new()));
1578        let default = slot.default_resolved(&columns, &FreeMove).to_layout(&slot);
1579        assert!(default.group_by().is_empty());
1580        assert_eq!(default.expansion(), None);
1581    }
1582
1583    #[test]
1584    fn test_saved_grouping() {
1585        use SortDirection::{Ascending, Descending};
1586
1587        let levels = [by(1, Ascending), by(2, Descending)];
1588        // Toggles of this grouping in a hash set, of a deeper one (c1, c2,
1589        // c3) and of another one (c2 first).
1590        let expansion = [
1591            path(&[(1, text("b"))]),
1592            path(&[(1, OwnedValue::Float(2.5))]),
1593            path(&[(1, OwnedValue::Int(2)), (2, text("x"))]),
1594            path(&[(1, OwnedValue::Int(2))]),
1595            path(&[(1, OwnedValue::Empty)]),
1596            path(&[(1, OwnedValue::Bool(true)), (2, text("x")), (3, text("y"))]),
1597            path(&[(2, text("x"))]),
1598        ]
1599        .into_iter()
1600        .fold(
1601            Expansion::<u32>::new(ExpandDepth::All),
1602            |expansion, path| expansion.with_group(path, false, 3),
1603        );
1604        let layout = with_saved_grouping(GridLayout::new(), &levels, &expansion);
1605        assert_eq!(
1606            layout.group_by(),
1607            [
1608                LayoutGroup::new("c1", Ascending),
1609                LayoutGroup::new("c2", Descending)
1610            ]
1611        );
1612        // Empty, then numbers, then text; a path before its children.
1613        let toggled: Vec<Vec<OwnedValue>> = vec![
1614            vec![OwnedValue::Empty],
1615            vec![OwnedValue::Int(2)],
1616            vec![OwnedValue::Int(2), text("x")],
1617            vec![OwnedValue::Float(2.5)],
1618            vec![text("b")],
1619        ];
1620        assert_eq!(
1621            layout.expansion().map(LayoutExpansion::toggled),
1622            Some(toggled.as_slice())
1623        );
1624
1625        // No grouping: the depth alone, which a layout omits when it is the
1626        // default.
1627        let flat: [GroupBy<TestRow>; 0] = [];
1628        let layout = with_saved_grouping(GridLayout::new(), &flat, &expansion);
1629        assert!(layout.group_by().is_empty());
1630        assert_eq!(layout.expansion(), None);
1631        let layout = with_saved_grouping(
1632            GridLayout::new(),
1633            &flat,
1634            &Expansion::<u32>::new(ExpandDepth::Levels(2)),
1635        );
1636        assert_eq!(
1637            layout.expansion(),
1638            Some(&LayoutExpansion::new(ExpandDepth::Levels(2)))
1639        );
1640    }
1641
1642    #[test]
1643    fn test_cmp_values() {
1644        use std::cmp::Ordering::{Equal, Greater, Less};
1645
1646        let int = OwnedValue::Int;
1647        assert_eq!(cmp_values(&[], &[]), Equal);
1648        assert_eq!(cmp_values(&[], &[text("a")]), Less);
1649        assert_eq!(cmp_values(&[int(1)], &[int(1), text("a")]), Less);
1650        assert_eq!(
1651            cmp_values(&[int(2), text("a")], &[int(1), text("b")]),
1652            Greater
1653        );
1654        assert_eq!(
1655            cmp_values(&[OwnedValue::Float(1.5)], &[int(2)]),
1656            Less,
1657            "numbers by value"
1658        );
1659        assert_eq!(cmp_values(&[text("a")], &[int(9)]), Greater);
1660    }
1661
1662    /// Microseconds of the UI-thread part of restoring a layout of 128
1663    /// columns with three grouping levels and ten toggled groups: resolve,
1664    /// apply the columns, build the expansion, and write the layout back.
1665    /// Run with `cargo test --release -p gpui-kit-grid --lib -- --ignored
1666    /// --nocapture test_restore_timing`.
1667    #[test]
1668    #[ignore = "timing; run in release with --ignored --nocapture"]
1669    #[cfg(not(target_family = "wasm"))]
1670    fn test_restore_timing() {
1671        use std::{hint::black_box, sync::LazyLock, time::Instant};
1672
1673        use super::{toggle_groups, with_saved_grouping};
1674
1675        static IDS: LazyLock<Vec<String>> =
1676            LazyLock::new(|| (0..128).map(|ix| format!("column_{ix}")).collect());
1677        static ALL: [WideColumn; 128] = {
1678            let mut all = [WideColumn(0); 128];
1679            let mut ix = 0;
1680            while ix < 128 {
1681                all[ix] = WideColumn(ix as u16);
1682                ix += 1;
1683            }
1684            all
1685        };
1686
1687        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1688        struct WideColumn(u16);
1689
1690        impl GridColumns for WideColumn {
1691            fn all() -> &'static [Self] {
1692                &ALL
1693            }
1694
1695            fn id(self) -> &'static str {
1696                IDS[self.0 as usize].as_str()
1697            }
1698
1699            fn ix(self) -> ColumnIx {
1700                ColumnIx::new(self.0)
1701            }
1702        }
1703
1704        struct WideRow;
1705
1706        impl GridRow for WideRow {
1707            type Key = u32;
1708            type Column = WideColumn;
1709
1710            fn key(&self) -> &u32 {
1711                &0
1712            }
1713
1714            fn value(&self, _: WideColumn) -> CellValue<'_> {
1715                CellValue::Empty
1716            }
1717        }
1718
1719        let schema = ALL.iter().fold(GridSchema::new(), |schema, column| {
1720            schema.column(GridColumn::new(*column, column.id()).with_width(px(80.)))
1721        });
1722        let slot = LayoutSlot::<WideRow>::new(&schema);
1723        let mut columns = ColumnLayout::new(schema.clone());
1724        columns.normalize(&FreeMove);
1725        // Two layouts that differ in every column, so that each restore
1726        // moves and resizes them all.
1727        let layout = |reversed: bool, width: f32| {
1728            let mut expansion = LayoutExpansion::new(ExpandDepth::Levels(1));
1729            for ix in 0..10 {
1730                expansion = expansion.with_toggled([
1731                    text(&format!("instrument {ix}")),
1732                    text("Buy"),
1733                    OwnedValue::Float(100.25 + f64::from(ix)),
1734                ]);
1735            }
1736            let ids: Vec<&String> = match reversed {
1737                true => IDS.iter().rev().collect(),
1738                false => IDS.iter().collect(),
1739            };
1740            ids.into_iter()
1741                .map(|id| LayoutColumn::new(id.clone(), width))
1742                .fold(GridLayout::new(), GridLayout::with_column)
1743                .with_sort([LayoutSort::new("column_3", SortDirection::Descending)])
1744                .with_group_by(
1745                    IDS[..3]
1746                        .iter()
1747                        .map(|id| LayoutGroup::new(id.clone(), SortDirection::Ascending)),
1748                )
1749                .with_expansion(expansion)
1750        };
1751        let layouts = [layout(true, 96.5), layout(false, 80.)];
1752        let started = Instant::now();
1753        let rounds = 1_000;
1754        let mut best = f64::MAX;
1755        for _ in 0..5 {
1756            let started_round = Instant::now();
1757            for round in 0..rounds {
1758                let saved = &layouts[round % 2];
1759                let (resolved, report) = slot.resolve(&columns, &FreeMove, black_box(saved));
1760                assert!(report.is_exact());
1761                black_box(resolved.apply(&mut columns));
1762                let levels = resolved.arrangement.levels();
1763                let expansion = toggle_groups(
1764                    Expansion::<u32>::new(resolved.depth),
1765                    resolved.toggled.iter().cloned(),
1766                    levels.len(),
1767                );
1768                black_box(with_saved_grouping(GridLayout::new(), levels, &expansion));
1769            }
1770            best = best.min(started_round.elapsed().as_secs_f64() * 1e6 / rounds as f64);
1771        }
1772        println!(
1773            "restore of 128 columns, 3 levels, 10 groups: {best:.1} µs ({:.0} ms in all)",
1774            started.elapsed().as_secs_f64() * 1e3
1775        );
1776    }
1777}