1use 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
38pub(super) struct LayoutSlot<R: GridRow> {
42 view_id: Option<SharedString>,
43 codec: Box<dyn LayoutCodec>,
44 merge: Box<dyn LayoutMerge>,
45 index: FxHashMap<&'static str, usize>,
48 schema: Vec<ColumnIx>,
51 defaults: Vec<LayoutColumn>,
54 bands: Vec<Box<[&'static str]>>,
56 ungroupable: Vec<ColumnId>,
60 view_sort: Vec<(ColumnIx, SortDirection)>,
62 view_arrangement: Arrangement<R>,
66 view_depth: ExpandDepth,
69 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 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 pub(super) fn set_view_depth(&mut self, depth: ExpandDepth) {
130 self.view_depth = depth;
131 }
132
133 fn context(&self) -> LayoutMergeContext<'_> {
135 LayoutMergeContext::new(&self.defaults).with_ungroupable(&self.ungroupable)
136 }
137
138 fn column_ix(&self, id: &str) -> Option<ColumnIx> {
140 self.index.get(id).map(|place| self.schema[*place])
141 }
142
143 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 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 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 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 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 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 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 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 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 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
305enum Arrangement<R: GridRow> {
307 Flat,
310 Groups(Vec<GroupBy<R>>),
313 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 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 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 fn levels(&self) -> &[GroupBy<R>] {
349 match self {
350 Self::Groups(levels) => levels,
351 Self::Flat | Self::Tree(_) => &[],
352 }
353 }
354}
355
356struct Resolved<R: GridRow> {
359 order: Vec<ColumnIx>,
361 zones: Vec<ColumnZone>,
363 widths: Vec<Pixels>,
365 visible: ColumnSet,
366 sort: Vec<(ColumnIx, SortDirection)>,
368 arrangement: Arrangement<R>,
371 depth: ExpandDepth,
373 toggled: Vec<GroupPath>,
376}
377
378impl<R: GridRow> Resolved<R> {
379 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 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 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 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 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
460fn 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
502fn 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
514fn 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
524fn 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
536fn set_toggled<K: Clone + Eq + Hash>(
538 expansion: Expansion<K>,
539 path: GroupPath,
540 toggled: bool,
541 levels: usize,
542) -> Expansion<K> {
543 let by_default = Expansion::<()>::new(expansion.depth()).is_group_expanded(&path, levels);
545 expansion.with_group(path, by_default != toggled, levels)
546}
547
548fn 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
560fn 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
574fn sort_by_zone(order: &mut [ColumnIx], zones: &[ColumnZone]) {
576 order.sort_by_key(|ix| zones[ix.get()].slot());
577}
578
579fn 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 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 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 pub fn with_layout(mut self, layout: GridLayout) -> Self {
641 self.layout.initial = Some(layout);
642 self.apply_initial_layout()
643 }
644
645 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 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 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 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 self.view = view.with_expansion(expansion);
696 self.recompile_quick_filter();
698 self
699 }
700
701 pub fn with_layout_codec(mut self, codec: impl LayoutCodec) -> Self {
704 self.layout.codec = Box::new(codec);
705 self
706 }
707
708 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 pub fn view_id(&self) -> Option<&SharedString> {
717 self.layout.view_id.as_ref()
718 }
719
720 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 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 pub fn export_layout(&self) -> Result<String, LayoutError> {
765 self.layout.codec.encode(&self.layout())
766 }
767
768 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 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 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 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 let delta = self.relayout(|columns, _| resolved.apply(columns));
853 let refilter = self.columns.visible_set() != visible && self.follow_visible_columns();
856 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 self.restart_projection(cx);
874 }
875 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 self.emit_layout_change(delta, origin, cx);
885 cx.notify();
886 }
887 }
888
889 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 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 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 #[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 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 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 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 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 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 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 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 assert_eq!(resolved.visible, set([0, 1, 3, 7]));
1146 assert_eq!(
1148 resolved.sort,
1149 [
1150 (ix(4), SortDirection::Descending),
1151 (ix(0), SortDirection::Ascending)
1152 ]
1153 );
1154 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 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 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 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 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 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 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 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 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 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 let (resolved, _) = slot.resolve(&columns, policy, &saved);
1383 assert!(resolved.apply(&mut columns).is_empty());
1384 }
1385
1386 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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}