Skip to main content

gpui_kit_grid/state/
columns.rs

1//! The column schema and the column layout: one display order split in
2//! three zones (frozen left, scrolling, frozen right), widths, visibility,
3//! header bands, and the per-zone prefix sums that hit tests and horizontal
4//! culling use.
5//!
6//! Everything that outlives a frame is keyed by [`ColumnIx`]; positions
7//! ([`ViewColumnIx`], places in a zone) are computed, never stored.
8
9use std::{ops::Range, sync::Arc};
10
11use gpui_kit::component::IconNamed;
12use gpui_kit::{App, Context, Pixels, Point, SharedString, Window, px};
13
14use super::{
15    CELL_PADDING, ColumnOf, LiveGridDelegate, LiveGridState, SelectionOrigin,
16    disclosure::INDENT,
17    events::{LayoutOrigin, LayoutPart, LayoutParts},
18    frame::{LEAD_ICON_LANE, cell_icon_size, cell_image_size},
19    header::{HEADER_SORT_LANE, header_icon_lane, header_label_style},
20    images::ImageFit,
21    pinned::{BodyLine, EDGES},
22    style::AggregateLine,
23    summary::{SummaryLine, SummaryMeasure, TOTALS_WEIGHT},
24};
25use crate::{
26    actions::{
27        AutofitColumn, AutofitColumns, FreezeColumnLeft, FreezeColumnRight, HideColumn,
28        MoveColumnLeft, MoveColumnRight, ShowAllColumns, UnfreezeColumn,
29    },
30    engine::DisplayRow,
31    model::{CellValue, ColumnIx, ColumnSet, GridColumns, GridRow, NumberFormat, display_text_in},
32    policy::{AutofitContext, AutofitPolicy, ColumnMoveContext, ColumnMovePolicy, ColumnOrder},
33};
34
35/// The smallest width a column can have, whatever its own minimum.
36pub const MIN_COLUMN_WIDTH: Pixels = px(10.);
37
38/// Horizontal alignment of a column.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub enum ColumnAlign {
41    /// Numbers right, everything else left.
42    #[default]
43    Auto,
44    /// Left in left-to-right layouts.
45    Start,
46    /// Right in left-to-right layouts.
47    End,
48}
49
50/// Where a column is shown.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
52pub enum ColumnZone {
53    /// Frozen at the left edge; ignores horizontal scrolling.
54    FrozenLeft,
55    /// Scrolls horizontally.
56    #[default]
57    Scrolling,
58    /// Frozen at the right edge; ignores horizontal scrolling.
59    FrozenRight,
60}
61
62impl ColumnZone {
63    /// Every zone, left to right.
64    pub(crate) const ALL: [Self; 3] = [Self::FrozenLeft, Self::Scrolling, Self::FrozenRight];
65
66    /// The zone's place, left to right: an index into per-zone arrays.
67    pub(crate) const fn slot(self) -> usize {
68        self as usize
69    }
70}
71
72/// The on-screen position of a visible column, left to right across all
73/// zones. Transient: never stored and never persisted; the column itself (or
74/// its [`ColumnIx`]) is the identity.
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
76pub struct ViewColumnIx(u16);
77
78impl ViewColumnIx {
79    /// Creates a view position.
80    pub const fn new(ix: u16) -> Self {
81        Self(ix)
82    }
83
84    /// Returns the position as `usize`.
85    pub const fn get(self) -> usize {
86        self.0 as usize
87    }
88}
89
90/// What a column paints in its cells.
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum CellContent {
94    /// The value, painted by the body: text, an icon for
95    /// [`CellValue::Icon`](crate::CellValue::Icon), or an image for
96    /// [`CellValue::Image`](crate::CellValue::Image).
97    #[default]
98    Text,
99    /// A real element from `LiveGridDelegate::render_cell` for each visible
100    /// cell (progress, tag, sparkline). The cell stays a grid cell for the
101    /// keyboard; presses reach the element and, unless it stops them, the
102    /// cell.
103    Element,
104    /// An interactive element (checkbox, button): as `Element`, and Space
105    /// on the focused cell activates it through the handler it registers
106    /// (`CellElementContext::on_activate`). The keyboard focus stays with
107    /// the grid: give the element `tab_stop(false)`.
108    Control,
109}
110
111impl CellContent {
112    /// The value of the root's [`CELL_CONTENT_KEY`] context attribute.
113    ///
114    /// [`CELL_CONTENT_KEY`]: crate::actions::CELL_CONTENT_KEY
115    pub(crate) const fn key_value(self) -> &'static str {
116        match self {
117            Self::Text => "text",
118            Self::Element => "element",
119            Self::Control => "control",
120        }
121    }
122}
123
124/// What a column's header cell shows. Wherever the header shows no text,
125/// the title still names the column: its tooltip, its accessible name, the
126/// column chooser, the menus and an export's header.
127#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
128#[non_exhaustive]
129pub enum HeaderLabel {
130    /// The title.
131    #[default]
132    Title,
133    /// An icon (its asset path) before the title.
134    IconAndTitle(SharedString),
135    /// The icon (its asset path) alone.
136    Icon(SharedString),
137}
138
139/// A column of the grid schema.
140#[derive(Clone, Debug)]
141pub struct GridColumn<C> {
142    pub(super) column: C,
143    pub(super) title: SharedString,
144    pub(super) width: Pixels,
145    pub(super) align: ColumnAlign,
146    pub(super) min_width: Pixels,
147    pub(super) max_width: Pixels,
148    pub(super) zone: ColumnZone,
149    pub(super) visible: bool,
150    pub(super) resizable: bool,
151    pub(super) movable: bool,
152    pub(super) sortable: bool,
153    pub(super) groupable: bool,
154    // What the cells paint and what the header shows.
155    pub(super) content: CellContent,
156    pub(super) header_label: HeaderLabel,
157    pub(super) hideable: bool,
158    /// Never shown, offered or exported by default: a helper column.
159    pub(super) internal: bool,
160    /// How the column shows its numbers.
161    pub(super) number_format: NumberFormat,
162    // How an image fits its cell, and its corners.
163    pub(super) image_fit: ImageFit,
164    pub(super) image_corner_radius: Pixels,
165}
166
167impl<C: GridColumns> GridColumn<C> {
168    /// A column with a title, 100 px wide.
169    pub fn new(column: C, title: impl Into<SharedString>) -> Self {
170        Self {
171            column,
172            title: title.into(),
173            width: px(100.),
174            align: ColumnAlign::Auto,
175            min_width: MIN_COLUMN_WIDTH,
176            max_width: Pixels::MAX,
177            zone: ColumnZone::Scrolling,
178            visible: true,
179            resizable: true,
180            movable: true,
181            sortable: true,
182            groupable: true,
183            content: CellContent::Text,
184            header_label: HeaderLabel::Title,
185            hideable: true,
186            internal: false,
187            number_format: NumberFormat::new(),
188            image_fit: ImageFit::Contain,
189            image_corner_radius: px(0.),
190        }
191    }
192
193    /// Sets the initial width, kept within the column's limits.
194    pub fn with_width(mut self, width: Pixels) -> Self {
195        self.width = width;
196        self
197    }
198
199    /// Sets the alignment.
200    pub fn with_align(mut self, align: ColumnAlign) -> Self {
201        self.align = align;
202        self
203    }
204
205    /// Sets the smallest width; raised to [`MIN_COLUMN_WIDTH`].
206    pub fn with_min_width(mut self, width: Pixels) -> Self {
207        self.min_width = width.max(MIN_COLUMN_WIDTH);
208        self
209    }
210
211    /// Sets the largest width (default: no limit).
212    pub fn with_max_width(mut self, width: Pixels) -> Self {
213        self.max_width = width;
214        self
215    }
216
217    /// Sets the zone (default [`ColumnZone::Scrolling`]).
218    pub fn with_zone(mut self, zone: ColumnZone) -> Self {
219        self.zone = zone;
220        self
221    }
222
223    /// Shows or hides the column initially (default shown).
224    pub fn with_visible(mut self, visible: bool) -> Self {
225        self.visible = visible;
226        self
227    }
228
229    /// Lets the user resize the column with the mouse (default true).
230    pub fn with_resizable(mut self, resizable: bool) -> Self {
231        self.resizable = resizable;
232        self
233    }
234
235    /// Lets the user drag the column's header to reorder it (default true).
236    pub fn with_movable(mut self, movable: bool) -> Self {
237        self.movable = movable;
238        self
239    }
240
241    /// Lets a click on the column's header sort by it (default true).
242    /// Sorting through `LiveGridState::set_sort` is always allowed.
243    pub fn with_sortable(mut self, sortable: bool) -> Self {
244        self.sortable = sortable;
245        self
246    }
247
248    /// Lets the user group by the column (default true): drag its header
249    /// into the group panel ([`crate::LiveGrid::group_panel`]) or use
250    /// "Group by this column". Grouping through
251    /// `LiveGridState::set_group_by` is always allowed.
252    pub fn with_groupable(mut self, groupable: bool) -> Self {
253        self.groupable = groupable;
254        self
255    }
256
257    /// Sets what the cells paint: the value as text (the default), or an
258    /// element of `LiveGridDelegate::render_cell`.
259    pub fn with_content(mut self, content: CellContent) -> Self {
260        self.content = content;
261        self
262    }
263
264    /// Shows `icon` in the header before the title.
265    pub fn with_icon(mut self, icon: impl IconNamed) -> Self {
266        self.header_label = HeaderLabel::IconAndTitle(icon.path());
267        self
268    }
269
270    /// Shows only `icon` in the header. The title stays the column's name:
271    /// its tooltip, its accessible name, and its label in the column
272    /// chooser, the menus and an export's header.
273    pub fn with_icon_only(mut self, icon: impl IconNamed) -> Self {
274        self.header_label = HeaderLabel::Icon(icon.path());
275        self
276    }
277
278    /// Sets whether the user may hide the column: "Hide column",
279    /// `HideColumn` and the column chooser. Default `true`.
280    /// `LiveGridState::set_column_visible` and a restored layout hide
281    /// any column.
282    pub fn with_hideable(mut self, hideable: bool) -> Self {
283        self.hideable = hideable;
284        self
285    }
286
287    /// Makes the column internal (default `false`): a helper the grid
288    /// sorts, filters, groups and aggregates by, but never shows. It is
289    /// hidden whatever `with_visible`, a restored layout or
290    /// `LiveGridState::set_column_visible` say, and it is not in the column
291    /// chooser, "Show all columns", a copy or an export of the visible
292    /// columns. An export that names it (`ExportRequest::with_columns`)
293    /// writes it.
294    pub fn with_internal(mut self, internal: bool) -> Self {
295        self.internal = internal;
296        self
297    }
298
299    /// Sets how the column shows its numbers (default
300    /// [`NumberFormat::new`], two decimals): its cells, its aggregates, a
301    /// copy and an export's text and number hint.
302    pub fn with_number_format(mut self, format: NumberFormat) -> Self {
303        self.number_format = format;
304        self
305    }
306
307    /// Sets how an image (`CellValue::Image`) fits the room of its cell.
308    /// Default [`ImageFit::Contain`].
309    pub fn with_image_fit(mut self, fit: ImageFit) -> Self {
310        self.image_fit = fit;
311        self
312    }
313
314    /// Rounds the corners of the images in the cells by `radius`: half the
315    /// image's height draws a round avatar. Default 0.
316    pub fn with_image_corner_radius(mut self, radius: Pixels) -> Self {
317        self.image_corner_radius = radius;
318        self
319    }
320
321    /// The column.
322    pub fn column(&self) -> C {
323        self.column
324    }
325
326    /// The title.
327    pub fn title(&self) -> &SharedString {
328        &self.title
329    }
330
331    /// The initial width.
332    pub fn width(&self) -> Pixels {
333        self.width
334    }
335
336    /// The smallest width.
337    pub fn min_width(&self) -> Pixels {
338        self.min_width
339    }
340
341    /// The largest width.
342    pub fn max_width(&self) -> Pixels {
343        self.max_width
344    }
345
346    /// The alignment.
347    pub fn align(&self) -> ColumnAlign {
348        self.align
349    }
350
351    /// The initial zone.
352    pub fn zone(&self) -> ColumnZone {
353        self.zone
354    }
355
356    /// Returns true if the column is shown initially.
357    pub fn is_visible(&self) -> bool {
358        self.visible
359    }
360
361    /// Returns true if the user can resize the column.
362    pub fn is_resizable(&self) -> bool {
363        self.resizable
364    }
365
366    /// Returns true if the user can drag the column's header.
367    pub fn is_movable(&self) -> bool {
368        self.movable
369    }
370
371    /// Returns true if a click on the column's header sorts by it.
372    pub fn is_sortable(&self) -> bool {
373        self.sortable
374    }
375
376    /// Returns true if the user can group by the column.
377    pub fn is_groupable(&self) -> bool {
378        self.groupable
379    }
380
381    /// What the cells paint.
382    pub fn content(&self) -> CellContent {
383        self.content
384    }
385
386    /// What the header shows: the title, an icon and the title, or the
387    /// icon alone.
388    pub fn header_label(&self) -> &HeaderLabel {
389        &self.header_label
390    }
391
392    /// The header's icon (its asset path), if it shows one.
393    pub fn icon(&self) -> Option<&SharedString> {
394        match &self.header_label {
395            HeaderLabel::Title => None,
396            HeaderLabel::IconAndTitle(icon) | HeaderLabel::Icon(icon) => Some(icon),
397        }
398    }
399
400    /// Returns true if the user may hide the column.
401    pub fn is_hideable(&self) -> bool {
402        self.hideable
403    }
404
405    /// How the column shows its numbers.
406    pub fn number_format(&self) -> NumberFormat {
407        self.number_format
408    }
409
410    /// Returns true if the column is internal: never shown.
411    pub fn is_internal(&self) -> bool {
412        self.internal
413    }
414
415    /// How an image fits the room of its cell.
416    pub fn image_fit(&self) -> ImageFit {
417        self.image_fit
418    }
419
420    /// The corner radius of the images in the cells.
421    pub fn image_corner_radius(&self) -> Pixels {
422        self.image_corner_radius
423    }
424
425    /// `width` within `max(MIN_COLUMN_WIDTH, min_width) ..= max_width`; NaN,
426    /// infinite, zero and negative widths give the minimum.
427    pub(crate) fn clamp_width(&self, width: Pixels) -> Pixels {
428        let min = self.min_width.max(MIN_COLUMN_WIDTH);
429        if !f32::from(width).is_finite() || width <= px(0.) {
430            return min;
431        }
432        width.min(self.max_width).max(min)
433    }
434}
435
436/// A titled header band over columns and nested bands (a multi-level
437/// header).
438#[derive(Clone, Debug)]
439pub struct ColumnBand<C> {
440    id: &'static str,
441    title: SharedString,
442    children: Vec<BandChild<C>>,
443}
444
445#[derive(Clone, Debug)]
446enum BandChild<C> {
447    Column(GridColumn<C>),
448    Band(ColumnBand<C>),
449}
450
451impl<C: GridColumns> ColumnBand<C> {
452    /// A band; `id` names it for element identity and must be unique within
453    /// the schema.
454    pub fn new(id: &'static str, title: impl Into<SharedString>) -> Self {
455        Self {
456            id,
457            title: title.into(),
458            children: Vec::new(),
459        }
460    }
461
462    /// Appends a column under the band.
463    pub fn column(mut self, column: GridColumn<C>) -> Self {
464        self.children.push(BandChild::Column(column));
465        self
466    }
467
468    /// Appends a nested band.
469    pub fn band(mut self, band: ColumnBand<C>) -> Self {
470        self.children.push(BandChild::Band(band));
471        self
472    }
473
474    /// The id.
475    pub fn id(&self) -> &'static str {
476        self.id
477    }
478
479    /// The title.
480    pub fn title(&self) -> &SharedString {
481        &self.title
482    }
483}
484
485/// The columns shown by a grid, in their initial display order, with their
486/// header bands.
487#[derive(Clone, Debug)]
488pub struct GridSchema<C> {
489    /// Declaration order: depth first through bands.
490    columns: Vec<GridColumn<C>>,
491    /// The bands of `columns[i]`, outermost first.
492    chains: Vec<Box<[&'static str]>>,
493    /// Every band's id and title.
494    bands: Vec<(&'static str, SharedString)>,
495    frozen: usize,
496}
497
498impl<C: GridColumns> Default for GridSchema<C> {
499    fn default() -> Self {
500        Self {
501            columns: Vec::new(),
502            chains: Vec::new(),
503            bands: Vec::new(),
504            frozen: 0,
505        }
506    }
507}
508
509impl<C: GridColumns> GridSchema<C> {
510    /// An empty schema.
511    pub fn new() -> Self {
512        Self::default()
513    }
514
515    /// Appends a column.
516    ///
517    /// # Panics
518    ///
519    /// If the column, or another column with the same [`GridColumns::id`],
520    /// is already in the schema.
521    pub fn column(mut self, column: GridColumn<C>) -> Self {
522        self.push_column(column, &[]);
523        self
524    }
525
526    /// Appends a band and its columns, depth first.
527    ///
528    /// # Panics
529    ///
530    /// If a band id or a column repeats one already in the schema.
531    pub fn band(mut self, band: ColumnBand<C>) -> Self {
532        self.push_band(band, &mut Vec::new());
533        self
534    }
535
536    /// Freezes the first `count` declared columns on the left, over their
537    /// own [`GridColumn::with_zone`].
538    pub fn with_frozen(mut self, count: usize) -> Self {
539        self.frozen = count;
540        self
541    }
542
543    fn push_column(&mut self, column: GridColumn<C>, chain: &[&'static str]) {
544        let ix = column.column.ix();
545        assert!(ix.get() < 128, "a grid supports at most 128 columns");
546        assert!(
547            !self
548                .columns
549                .iter()
550                .any(|c| c.column == column.column || c.column.id() == column.column.id()),
551            "column {:?} (id {:?}) is already in the schema",
552            column.column,
553            column.column.id(),
554        );
555        self.columns.push(column);
556        self.chains.push(chain.into());
557    }
558
559    fn push_band(&mut self, band: ColumnBand<C>, chain: &mut Vec<&'static str>) {
560        assert!(
561            !self.bands.iter().any(|(id, _)| *id == band.id),
562            "band {:?} is already in the schema",
563            band.id,
564        );
565        self.bands.push((band.id, band.title));
566        chain.push(band.id);
567        for child in band.children {
568            match child {
569                BandChild::Column(column) => self.push_column(column, chain),
570                BandChild::Band(band) => self.push_band(band, chain),
571            }
572        }
573        chain.pop();
574    }
575}
576
577/// The visible columns of one zone, left to right, with prefix sums.
578#[derive(Clone, Debug, Default)]
579pub(crate) struct ZoneColumns {
580    pub(crate) columns: Vec<ColumnIx>,
581    /// `offsets[i]` is the left edge of `columns[i]` from the zone's left
582    /// edge; `offsets[n]` is the zone's content width.
583    pub(crate) offsets: Vec<Pixels>,
584}
585
586impl ZoneColumns {
587    pub(crate) fn len(&self) -> usize {
588        self.columns.len()
589    }
590
591    /// The zone's content width: the sum of its visible columns.
592    pub(crate) fn content_width(&self) -> Pixels {
593        self.offsets.last().copied().unwrap_or_default()
594    }
595
596    /// The width of `columns[i]`.
597    pub(crate) fn width(&self, i: usize) -> Pixels {
598        self.offsets[i + 1] - self.offsets[i]
599    }
600
601    /// Columns intersecting `left..right` of the zone content: a binary
602    /// search over the prefix sums.
603    pub(crate) fn range(&self, left: Pixels, right: Pixels) -> Range<usize> {
604        let n = self.len();
605        if n == 0 || right <= left {
606            return 0..0;
607        }
608        let start = self.offsets[1..].partition_point(|&end| end <= left);
609        let end = self.offsets[..n].partition_point(|&start| start < right);
610        start..end.max(start)
611    }
612
613    /// The column containing `x` of the zone content.
614    pub(crate) fn index_at(&self, x: Pixels) -> Option<usize> {
615        let ix = self.offsets.partition_point(|&o| o <= x);
616        (ix >= 1 && ix <= self.len()).then(|| ix - 1)
617    }
618}
619
620/// A run of consecutive visible columns of one band in one zone: one band
621/// cell of the header.
622#[derive(Clone, Debug)]
623pub(crate) struct BandRun {
624    pub(crate) band: &'static str,
625    pub(crate) title: SharedString,
626    pub(crate) zone: ColumnZone,
627    /// The header row of the band cell (0 is the top row).
628    pub(crate) level: usize,
629    /// The columns under the run: a range of the zone's [`ZoneColumns`].
630    pub(crate) columns: Range<usize>,
631    /// Every column under the run can be dragged.
632    pub(crate) movable: bool,
633}
634
635/// One zone as laid out in a frame: its geometry.
636#[derive(Clone, Debug)]
637pub(crate) struct ZoneFrame {
638    pub(crate) zone: ColumnZone,
639    /// The zone's left edge from the body's left edge.
640    pub(crate) x: Pixels,
641    /// The zone's on-screen width.
642    pub(crate) width: Pixels,
643    /// The horizontal scroll of the zone's content (zero for frozen zones).
644    pub(crate) scroll: Pixels,
645    /// The zone's visible columns that intersect it: a range of its
646    /// [`ZoneColumns`].
647    pub(crate) columns: Range<usize>,
648}
649
650impl ZoneFrame {
651    /// The left edge of `zone.columns[i]` from the body's left edge.
652    pub(crate) fn column_x(&self, zone: &ZoneColumns, i: usize) -> Pixels {
653        self.x + zone.offsets[i] - self.scroll
654    }
655
656    /// The zone's right edge from the body's left edge.
657    pub(crate) fn right(&self) -> Pixels {
658        self.x + self.width
659    }
660}
661
662/// The three zones as laid out in a frame, left to right.
663#[derive(Clone, Debug)]
664pub(crate) struct ZoneGeometry {
665    pub(crate) zones: [ZoneFrame; 3],
666}
667
668impl ZoneGeometry {
669    pub(crate) fn zone(&self, zone: ColumnZone) -> &ZoneFrame {
670        &self.zones[zone.slot()]
671    }
672
673    /// The zone under `x` (from the body's left edge). The right zone does
674    /// not scroll, so it is found without the scroll offset.
675    pub(crate) fn zone_at(&self, x: Pixels) -> Option<ColumnZone> {
676        let [left, scrolling, right] = &self.zones;
677        if right.width > px(0.) && x >= right.x && x < right.right() {
678            Some(ColumnZone::FrozenRight)
679        } else if left.width > px(0.) && x >= left.x && x < left.right() {
680            Some(ColumnZone::FrozenLeft)
681        } else if scrolling.width > px(0.) && x >= scrolling.x && x < scrolling.right() {
682            Some(ColumnZone::Scrolling)
683        } else {
684            None
685        }
686    }
687}
688
689/// How a visible-column boundary resolves for a block being moved.
690#[derive(Clone, Copy, Debug, PartialEq, Eq)]
691pub(crate) enum Boundary {
692    /// Insert the block at this gap of the zone order.
693    Gap(usize),
694    /// The block is already there.
695    Here,
696    /// The move policy refuses every gap at this boundary.
697    Refused,
698}
699
700/// A block of one zone being moved: its members and where they are.
701pub(crate) struct ZoneMove<'a> {
702    context: ColumnMoveContext<'a>,
703    members: Vec<ColumnIx>,
704    /// From the first to the last member, in the zone order.
705    span: Range<usize>,
706    contiguous: bool,
707    /// The zone-order position of every visible column of the zone.
708    visible_at: Vec<usize>,
709}
710
711impl ZoneMove<'_> {
712    /// The policy's verdict on putting the block at visible boundary `k`
713    /// (before the zone's `k`-th visible column, or after the last one).
714    /// Gaps across hidden columns are tried nearest to the visible column
715    /// first: the block lands right before it, or right after the last one.
716    pub(crate) fn resolve(&self, k: usize, policy: &dyn ColumnMovePolicy) -> Boundary {
717        let n = self.visible_at.len();
718        let check = |gap: usize| {
719            if self.contiguous && self.span.start <= gap && gap <= self.span.end {
720                Some(Boundary::Here)
721            } else if policy.is_allowed(&self.context, self.span.clone(), gap) {
722                Some(Boundary::Gap(gap))
723            } else {
724                None
725            }
726        };
727        let found = if k < n {
728            let low = if k > 0 { self.visible_at[k - 1] + 1 } else { 0 };
729            (low..=self.visible_at[k]).rev().find_map(check)
730        } else if k == n {
731            let low = n.checked_sub(1).map_or(0, |last| self.visible_at[last] + 1);
732            (low..=self.context.columns().len()).find_map(check)
733        } else {
734            None
735        };
736        found.unwrap_or(Boundary::Refused)
737    }
738}
739
740/// The column layout of a grid: the schema plus the user's order, widths,
741/// visibility and zones, and what is derived from them. Cloned for a dry run
742/// of a move (the column chooser's drop targets).
743#[derive(Clone)]
744pub(crate) struct ColumnLayout<C> {
745    /// Schema columns by `ColumnIx`; `None` for columns of `C` not in it.
746    schema: Vec<Option<GridColumn<C>>>,
747    /// Schema columns in declaration order, with the zone each starts in.
748    declared: Vec<(ColumnIx, ColumnZone)>,
749    /// The bands of each column, outermost first, by `ColumnIx`.
750    chains: Vec<Box<[&'static str]>>,
751    bands: Vec<(&'static str, SharedString)>,
752    /// The deepest band nesting: the header has `depth + 1` rows.
753    depth: usize,
754    /// Every schema column in display order, sorted by zone.
755    order: Vec<ColumnIx>,
756    /// `order[ends[z - 1]..ends[z]]` is zone `z`.
757    ends: [usize; 3],
758    widths: Vec<Pixels>,
759    zones: Vec<ColumnZone>,
760    visible: ColumnSet,
761    /// The internal columns: never visible.
762    internal: ColumnSet,
763    // Derived by `settle`.
764    zone_columns: [ZoneColumns; 3],
765    /// The first `ViewColumnIx` of each zone.
766    zone_starts: [usize; 3],
767    view_ix: Vec<u16>,
768    runs: Vec<BandRun>,
769}
770
771const NO_VIEW_IX: u16 = u16::MAX;
772
773impl<C: GridColumns> ColumnLayout<C> {
774    pub(crate) fn new(schema: GridSchema<C>) -> Self {
775        let count = C::all().len();
776        let mut columns: Vec<Option<GridColumn<C>>> = (0..count).map(|_| None).collect();
777        let mut chains: Vec<Box<[&'static str]>> = vec![Box::default(); count];
778        let mut widths = vec![px(0.); count];
779        let mut zones = vec![ColumnZone::Scrolling; count];
780        let mut visible = ColumnSet::EMPTY;
781        let mut internal = ColumnSet::EMPTY;
782        let mut declared = Vec::with_capacity(schema.columns.len());
783        let depth = schema.chains.iter().map(|chain| chain.len()).max();
784        for (position, (column, chain)) in schema.columns.into_iter().zip(schema.chains).enumerate()
785        {
786            let ix = column.column.ix();
787            let zone = if position < schema.frozen {
788                ColumnZone::FrozenLeft
789            } else {
790                column.zone
791            };
792            widths[ix.get()] = column.clamp_width(column.width);
793            zones[ix.get()] = zone;
794            if column.internal {
795                internal.insert(ix);
796            } else if column.visible {
797                visible.insert(ix);
798            }
799            chains[ix.get()] = chain;
800            declared.push((ix, zone));
801            columns[ix.get()] = Some(column);
802        }
803        let mut layout = Self {
804            schema: columns,
805            declared,
806            chains,
807            bands: schema.bands,
808            depth: depth.unwrap_or(0),
809            order: Vec::new(),
810            ends: [0; 3],
811            widths,
812            zones,
813            visible,
814            internal,
815            zone_columns: Default::default(),
816            zone_starts: [0; 3],
817            view_ix: vec![NO_VIEW_IX; count],
818            runs: Vec::new(),
819        };
820        layout.reset_order();
821        layout
822    }
823
824    /// The order and zones of the schema.
825    fn reset_order(&mut self) {
826        self.order.clear();
827        for &(ix, zone) in &self.declared {
828            self.order.push(ix);
829            self.zones[ix.get()] = zone;
830        }
831        self.settle();
832        // At least one column is always shown.
833        if self.visible.is_empty()
834            && let Some(&first) = self.order.iter().find(|ix| !self.internal.contains(**ix))
835        {
836            self.visible.insert(first);
837            self.settle();
838        }
839    }
840
841    /// Resets the order and zones to the schema, normalized by `policy`.
842    pub(crate) fn reset(&mut self, policy: &dyn ColumnMovePolicy) {
843        self.reset_order();
844        self.normalize(policy);
845    }
846
847    /// Restores `policy`'s invariants; a restored layout calls it too.
848    pub(crate) fn normalize(&mut self, policy: &dyn ColumnMovePolicy) {
849        policy.normalize(&mut ColumnOrder::new(
850            &mut self.order,
851            &mut self.zones,
852            &self.chains,
853        ));
854        self.settle();
855    }
856
857    /// Sorts the order by zone and derives the zone columns, prefix sums,
858    /// view positions and band runs.
859    fn settle(&mut self) {
860        let zones = &self.zones;
861        self.order.sort_by_key(|ix| zones[ix.get()].slot());
862        for zone in ColumnZone::ALL {
863            self.ends[zone.slot()] = self
864                .order
865                .partition_point(|ix| zones[ix.get()].slot() <= zone.slot());
866        }
867        self.view_ix.fill(NO_VIEW_IX);
868        let mut view = 0;
869        for zone in ColumnZone::ALL {
870            let range = self.zone_range(zone);
871            let columns = &mut self.zone_columns[zone.slot()];
872            columns.columns.clear();
873            columns.offsets.clear();
874            self.zone_starts[zone.slot()] = view;
875            let mut x = px(0.);
876            columns.offsets.push(x);
877            for &ix in &self.order[range] {
878                if self.visible.contains(ix) {
879                    columns.columns.push(ix);
880                    x += self.widths[ix.get()];
881                    columns.offsets.push(x);
882                    self.view_ix[ix.get()] = view as u16;
883                    view += 1;
884                }
885            }
886        }
887        self.runs.clear();
888        for zone in ColumnZone::ALL {
889            let columns = &self.zone_columns[zone.slot()];
890            for level in 0..self.depth {
891                let mut i = 0;
892                while i < columns.len() {
893                    let Some(&band) = self.chains[columns.columns[i].get()].get(level) else {
894                        i += 1;
895                        continue;
896                    };
897                    let start = i;
898                    while i < columns.len()
899                        && self.chains[columns.columns[i].get()].get(level) == Some(&band)
900                    {
901                        i += 1;
902                    }
903                    let title = self
904                        .bands
905                        .iter()
906                        .find(|(id, _)| *id == band)
907                        .map(|(_, title)| title.clone())
908                        .unwrap_or_default();
909                    let movable = columns.columns[start..i]
910                        .iter()
911                        .all(|ix| self.column(*ix).movable);
912                    self.runs.push(BandRun {
913                        band,
914                        title,
915                        zone,
916                        level,
917                        columns: start..i,
918                        movable,
919                    });
920                }
921            }
922        }
923    }
924
925    fn zone_range(&self, zone: ColumnZone) -> Range<usize> {
926        let start = match zone.slot() {
927            0 => 0,
928            slot => self.ends[slot - 1],
929        };
930        start..self.ends[zone.slot()]
931    }
932
933    /// The schema column `ix`, if it is in the schema.
934    pub(crate) fn get(&self, ix: ColumnIx) -> Option<&GridColumn<C>> {
935        self.schema.get(ix.get()).and_then(Option::as_ref)
936    }
937
938    /// The schema column `ix`. Panics if it is not in the schema.
939    pub(crate) fn column(&self, ix: ColumnIx) -> &GridColumn<C> {
940        self.get(ix).expect("the column is in the schema")
941    }
942
943    /// Every schema column in display order, hidden ones included.
944    pub(crate) fn order(&self) -> &[ColumnIx] {
945        &self.order
946    }
947
948    /// The display order of one zone, hidden columns included.
949    pub(crate) fn zone_order(&self, zone: ColumnZone) -> &[ColumnIx] {
950        &self.order[self.zone_range(zone)]
951    }
952
953    pub(crate) fn width(&self, ix: ColumnIx) -> Pixels {
954        self.widths.get(ix.get()).copied().unwrap_or_default()
955    }
956
957    pub(crate) fn is_visible(&self, ix: ColumnIx) -> bool {
958        self.visible.contains(ix)
959    }
960
961    pub(crate) fn zone_of(&self, ix: ColumnIx) -> ColumnZone {
962        self.zones.get(ix.get()).copied().unwrap_or_default()
963    }
964
965    /// The visible columns (the quick filter's default columns).
966    pub(crate) fn visible_set(&self) -> ColumnSet {
967        self.visible
968    }
969
970    /// How the column `ix` shows its numbers; the default for a column not
971    /// in the schema.
972    #[inline]
973    pub(crate) fn number_format(&self, ix: ColumnIx) -> NumberFormat {
974        self.schema
975            .get(ix.get())
976            .and_then(Option::as_ref)
977            .map_or_else(NumberFormat::new, |column| column.number_format)
978    }
979
980    /// The number formats of every column, by `ColumnIx`: what an export
981    /// takes to its thread.
982    pub(crate) fn number_formats(&self) -> Arc<[NumberFormat]> {
983        (0..self.schema.len())
984            .map(|ix| self.number_format(ColumnIx::new(ix as u16)))
985            .collect()
986    }
987
988    /// Returns true if `ix` is an internal column, never shown.
989    pub(crate) fn is_internal(&self, ix: ColumnIx) -> bool {
990        self.internal.contains(ix)
991    }
992
993    /// Returns true if a column the user may show is hidden: what "Show
994    /// all columns" would show.
995    pub(crate) fn any_hidden(&self) -> bool {
996        self.order
997            .iter()
998            .any(|ix| !self.visible.contains(*ix) && !self.internal.contains(*ix))
999    }
1000
1001    /// Visible columns in display order, left zone to right zone.
1002    pub(crate) fn visible(&self) -> impl Iterator<Item = ColumnIx> + '_ {
1003        self.zone_columns
1004            .iter()
1005            .flat_map(|zone| zone.columns.iter().copied())
1006    }
1007
1008    pub(crate) fn view_column_ix(&self, ix: ColumnIx) -> Option<ViewColumnIx> {
1009        let view = *self.view_ix.get(ix.get())?;
1010        (view != NO_VIEW_IX).then_some(ViewColumnIx(view))
1011    }
1012
1013    pub(crate) fn column_at(&self, view: ViewColumnIx) -> Option<ColumnIx> {
1014        let mut view = view.get();
1015        for zone in &self.zone_columns {
1016            if view < zone.len() {
1017                return Some(zone.columns[view]);
1018            }
1019            view -= zone.len();
1020        }
1021        None
1022    }
1023
1024    /// The zone of a visible column and its index in the zone's visible
1025    /// columns.
1026    pub(crate) fn zone_position(&self, ix: ColumnIx) -> Option<(ColumnZone, usize)> {
1027        let view = self.view_column_ix(ix)?.get();
1028        let zone = self.zone_of(ix);
1029        Some((zone, view - self.zone_starts[zone.slot()]))
1030    }
1031
1032    pub(crate) fn zone_columns(&self, zone: ColumnZone) -> &ZoneColumns {
1033        &self.zone_columns[zone.slot()]
1034    }
1035
1036    /// Header band cells, zone by zone.
1037    pub(crate) fn runs(&self) -> &[BandRun] {
1038        &self.runs
1039    }
1040
1041    /// The deepest band nesting: the header has `depth + 1` rows.
1042    pub(crate) fn depth(&self) -> usize {
1043        self.depth
1044    }
1045
1046    /// The bands of `ix`, outermost first.
1047    pub(crate) fn bands_of(&self, ix: ColumnIx) -> &[&'static str] {
1048        self.chains.get(ix.get()).map_or(&[], |chain| chain)
1049    }
1050
1051    /// The title of the band `id`, shown or not (an export's band titles,
1052    /// the column chooser).
1053    pub(crate) fn band_title(&self, id: &str) -> Option<&SharedString> {
1054        self.bands
1055            .iter()
1056            .find(|(band, _)| *band == id)
1057            .map(|(_, title)| title)
1058    }
1059
1060    /// The total width of the visible columns.
1061    pub(crate) fn total_width(&self) -> Pixels {
1062        self.zone_columns
1063            .iter()
1064            .map(ZoneColumns::content_width)
1065            .fold(px(0.), |total, width| total + width)
1066    }
1067
1068    /// The zones laid out in a body `viewport_width` wide: the left zone at
1069    /// the left edge, the right zone at the right edge, the scrolling zone
1070    /// between them. Frozen zones wider than the body are cut by its edge;
1071    /// the left one first gets `min(left, viewport)`.
1072    pub(crate) fn geometry(&self, viewport_width: Pixels, scroll_x: Pixels) -> ZoneGeometry {
1073        let viewport = viewport_width.max(px(0.));
1074        let [left, scrolling, right] = &self.zone_columns;
1075        let left_width = left.content_width().min(viewport);
1076        let right_width = right.content_width().min(viewport - left_width);
1077        let scroll_width = (viewport - left_width - right_width).max(px(0.));
1078        ZoneGeometry {
1079            zones: [
1080                ZoneFrame {
1081                    zone: ColumnZone::FrozenLeft,
1082                    x: px(0.),
1083                    width: left_width,
1084                    scroll: px(0.),
1085                    columns: left.range(px(0.), left_width),
1086                },
1087                ZoneFrame {
1088                    zone: ColumnZone::Scrolling,
1089                    x: left_width,
1090                    width: scroll_width,
1091                    scroll: scroll_x,
1092                    columns: scrolling.range(scroll_x, scroll_x + scroll_width),
1093                },
1094                ZoneFrame {
1095                    zone: ColumnZone::FrozenRight,
1096                    x: viewport - right_width,
1097                    width: right_width,
1098                    scroll: px(0.),
1099                    columns: right.range(px(0.), right_width),
1100                },
1101            ],
1102        }
1103    }
1104
1105    /// The column at `x` from the body's left edge: right zone without the
1106    /// scroll offset, then left zone, then the scrolling zone with it.
1107    pub(crate) fn column_at_x(
1108        &self,
1109        x: Pixels,
1110        viewport_width: Pixels,
1111        scroll_x: Pixels,
1112    ) -> Option<ColumnIx> {
1113        let geometry = self.geometry(viewport_width, scroll_x);
1114        let zone = geometry.zone_at(x)?;
1115        let frame = geometry.zone(zone);
1116        let columns = self.zone_columns(zone);
1117        columns
1118            .index_at(x - frame.x + frame.scroll)
1119            .map(|i| columns.columns[i])
1120    }
1121
1122    pub(crate) fn move_context(&self, zone: ColumnZone) -> ColumnMoveContext<'_> {
1123        ColumnMoveContext::new(zone, self.zone_order(zone), self.visible, &self.chains)
1124    }
1125
1126    /// A move of `members` (columns of `zone`, in zone order).
1127    pub(crate) fn zone_move(&self, zone: ColumnZone, members: Vec<ColumnIx>) -> ZoneMove<'_> {
1128        let order = self.zone_order(zone);
1129        let positions: Vec<usize> = members
1130            .iter()
1131            .filter_map(|member| order.iter().position(|ix| ix == member))
1132            .collect();
1133        let span = match (positions.iter().min(), positions.iter().max()) {
1134            (Some(&first), Some(&last)) => first..last + 1,
1135            _ => 0..0,
1136        };
1137        let visible_at = order
1138            .iter()
1139            .enumerate()
1140            .filter(|(_, ix)| self.visible.contains(**ix))
1141            .map(|(position, _)| position)
1142            .collect();
1143        ZoneMove {
1144            context: self.move_context(zone),
1145            contiguous: span.len() == members.len(),
1146            members,
1147            span,
1148            visible_at,
1149        }
1150    }
1151
1152    /// The columns of `band` in `zone` that lie in one unbroken run of the
1153    /// zone order with `first`, hidden ones included: what a drag of the
1154    /// band's header cell moves.
1155    pub(crate) fn band_segment(
1156        &self,
1157        zone: ColumnZone,
1158        band: &'static str,
1159        first: ColumnIx,
1160    ) -> Vec<ColumnIx> {
1161        let order = self.zone_order(zone);
1162        let is_member = |ix: &ColumnIx| self.bands_of(*ix).contains(&band);
1163        let Some(position) = order.iter().position(|ix| *ix == first) else {
1164            return Vec::new();
1165        };
1166        if !is_member(&first) {
1167            return Vec::new();
1168        }
1169        let start = order[..position]
1170            .iter()
1171            .rposition(|ix| !is_member(ix))
1172            .map_or(0, |p| p + 1);
1173        let end = order[position..]
1174            .iter()
1175            .position(|ix| !is_member(ix))
1176            .map_or(order.len(), |p| position + p);
1177        order[start..end].to_vec()
1178    }
1179
1180    /// Every column of `band` in `zone`, in order.
1181    pub(crate) fn band_members(&self, zone: ColumnZone, band: &'static str) -> Vec<ColumnIx> {
1182        self.zone_order(zone)
1183            .iter()
1184            .copied()
1185            .filter(|ix| self.bands_of(*ix).contains(&band))
1186            .collect()
1187    }
1188
1189    /// Sets a width within the column's limits. Returns true if it changed.
1190    pub(crate) fn set_width(&mut self, ix: ColumnIx, width: Pixels) -> bool {
1191        let Some(column) = self.get(ix) else {
1192            return false;
1193        };
1194        let width = column.clamp_width(width);
1195        if self.widths[ix.get()] == width {
1196            return false;
1197        }
1198        self.widths[ix.get()] = width;
1199        self.settle();
1200        true
1201    }
1202
1203    /// Shows or hides a column at its place. `None` if refused: the last
1204    /// visible column cannot be hidden, an internal one cannot be shown.
1205    /// Otherwise whether it changed.
1206    pub(crate) fn set_visible(&mut self, ix: ColumnIx, visible: bool) -> Option<bool> {
1207        self.get(ix)?;
1208        if visible && self.internal.contains(ix) {
1209            return None;
1210        }
1211        if self.visible.contains(ix) == visible {
1212            return Some(false);
1213        }
1214        if visible {
1215            self.visible.insert(ix);
1216        } else {
1217            if self.visible.len() == 1 {
1218                return None;
1219            }
1220            let visible = self.visible;
1221            self.visible = column_set(
1222                self.order
1223                    .iter()
1224                    .copied()
1225                    .filter(|c| *c != ix && visible.contains(*c)),
1226            );
1227        }
1228        self.settle();
1229        Some(true)
1230    }
1231
1232    /// Shows every column but the internal ones. Returns the columns that
1233    /// were hidden.
1234    pub(crate) fn show_all(&mut self) -> ColumnSet {
1235        let mut shown = ColumnSet::EMPTY;
1236        for &ix in &self.order {
1237            if !self.visible.contains(ix) && !self.internal.contains(ix) {
1238                shown.insert(ix);
1239                self.visible.insert(ix);
1240            }
1241        }
1242        if !shown.is_empty() {
1243            self.settle();
1244        }
1245        shown
1246    }
1247
1248    /// Moves `column` (with the columns `policy` moves along) to `zone`, at
1249    /// the edge of the new zone nearest to its old place: the end of a zone
1250    /// to the left, the start of a zone to the right. Returns the columns
1251    /// that changed zone.
1252    pub(crate) fn set_zone(
1253        &mut self,
1254        ix: ColumnIx,
1255        zone: ColumnZone,
1256        policy: &dyn ColumnMovePolicy,
1257    ) -> Vec<ColumnIx> {
1258        if self.get(ix).is_none() {
1259            return Vec::new();
1260        }
1261        let current = self.zone_of(ix);
1262        if current == zone {
1263            return Vec::new();
1264        }
1265        let order = self.zone_order(current);
1266        let position = order
1267            .iter()
1268            .position(|c| *c == ix)
1269            .expect("a schema column is in the order");
1270        let block = policy.zone_block(&self.move_context(current), ix);
1271        let block = if block.start <= position && position < block.end && block.end <= order.len() {
1272            block
1273        } else {
1274            position..position + 1
1275        };
1276        let members = order[block].to_vec();
1277        self.order.retain(|c| !members.contains(c));
1278        for member in &members {
1279            self.zones[member.get()] = zone;
1280        }
1281        let zones = &self.zones;
1282        let at = if zone.slot() < current.slot() {
1283            self.order
1284                .partition_point(|c| zones[c.get()].slot() <= zone.slot())
1285        } else {
1286            self.order
1287                .partition_point(|c| zones[c.get()].slot() < zone.slot())
1288        };
1289        self.order.splice(at..at, members.iter().copied());
1290        self.settle();
1291        members
1292    }
1293
1294    /// Moves `members` (columns of `zone`, in zone order) to `gap` of the
1295    /// zone order, counted before they are removed. Returns true if the
1296    /// order changed.
1297    pub(crate) fn move_members(
1298        &mut self,
1299        zone: ColumnZone,
1300        members: &[ColumnIx],
1301        gap: usize,
1302    ) -> bool {
1303        let range = self.zone_range(zone);
1304        let order = &self.order[range.clone()];
1305        let gap = gap.min(order.len());
1306        let before = order[..gap].iter().filter(|c| members.contains(c)).count();
1307        let mut moved: Vec<ColumnIx> = order
1308            .iter()
1309            .copied()
1310            .filter(|c| !members.contains(c))
1311            .collect();
1312        let at = gap - before;
1313        moved.splice(at..at, members.iter().copied());
1314        if moved == order {
1315            return false;
1316        }
1317        self.order.splice(range, moved);
1318        self.settle();
1319        true
1320    }
1321
1322    /// Moves `members` before `before` (`None`: to the end of the zone) if
1323    /// `policy` allows it. `None` if refused, else whether it changed.
1324    pub(crate) fn move_before(
1325        &mut self,
1326        zone: ColumnZone,
1327        members: Vec<ColumnIx>,
1328        before: Option<ColumnIx>,
1329        policy: &dyn ColumnMovePolicy,
1330    ) -> Option<bool> {
1331        if members.is_empty() || members.iter().any(|ix| self.zone_of(*ix) != zone) {
1332            return None;
1333        }
1334        let order = self.zone_order(zone);
1335        let gap = match before {
1336            Some(before) => order.iter().position(|c| *c == before)?,
1337            None => order.len(),
1338        };
1339        let shift = self.zone_move(zone, members);
1340        if shift.contiguous && shift.span.start <= gap && gap <= shift.span.end {
1341            return Some(false);
1342        }
1343        if !policy.is_allowed(&shift.context, shift.span.clone(), gap) {
1344            return None;
1345        }
1346        let members = shift.members;
1347        Some(self.move_members(zone, &members, gap))
1348    }
1349}
1350
1351/// What a layout change changed, and what to scroll into view after it.
1352#[derive(Clone, Copy, Debug, Default)]
1353pub(super) struct LayoutDelta {
1354    pub(super) parts: LayoutParts,
1355    pub(super) columns: ColumnSet,
1356    /// Moved columns to scroll into view (order and zone changes).
1357    pub(super) reveal: ColumnSet,
1358}
1359
1360impl LayoutDelta {
1361    pub(super) fn new(part: LayoutPart, columns: impl IntoIterator<Item = ColumnIx>) -> Self {
1362        let columns = column_set(columns);
1363        Self {
1364            parts: part.into(),
1365            columns,
1366            reveal: ColumnSet::EMPTY,
1367        }
1368    }
1369
1370    pub(super) fn revealing(mut self) -> Self {
1371        self.reveal = self.columns;
1372        self
1373    }
1374
1375    pub(super) fn is_empty(&self) -> bool {
1376        self.parts.is_empty()
1377    }
1378
1379    pub(super) fn union_with(&mut self, other: Self) {
1380        self.parts.union_with(other.parts);
1381        self.columns.union_with(other.columns);
1382        self.reveal.union_with(other.reveal);
1383    }
1384}
1385
1386fn column_set(columns: impl IntoIterator<Item = ColumnIx>) -> ColumnSet {
1387    columns.into_iter().fold(ColumnSet::EMPTY, |mut set, ix| {
1388        set.insert(ix);
1389        set
1390    })
1391}
1392
1393pub(super) fn column_of<C: GridColumns>(ix: ColumnIx) -> C {
1394    C::all()[ix.get()]
1395}
1396
1397impl<D: LiveGridDelegate> LiveGridState<D> {
1398    /// Sets how columns and bands move (default [`BandedMove`]; see also
1399    /// [`FreeMove`]). The schema's order is normalized by the new policy.
1400    ///
1401    /// [`BandedMove`]: crate::BandedMove
1402    /// [`FreeMove`]: crate::FreeMove
1403    pub fn with_column_move_policy(mut self, policy: impl ColumnMovePolicy) -> Self {
1404        self.column_move = Box::new(policy);
1405        self.columns.reset(&*self.column_move);
1406        // A layout given to `with_layout` is resolved by the new policy.
1407        self.apply_initial_layout()
1408    }
1409
1410    /// Sets how autofit measures a column (default [`VisibleRowsAutofit`]).
1411    ///
1412    /// [`VisibleRowsAutofit`]: crate::VisibleRowsAutofit
1413    pub fn with_autofit_policy(mut self, policy: impl AutofitPolicy) -> Self {
1414        self.autofit = Box::new(policy);
1415        self
1416    }
1417
1418    /// Every schema column in display order (left, scrolling, right zone),
1419    /// hidden ones included at their place.
1420    pub fn columns(&self) -> impl Iterator<Item = ColumnOf<D>> + '_ {
1421        self.columns.order().iter().map(|ix| column_of(*ix))
1422    }
1423
1424    /// Visible columns in display order; the n-th has `ViewColumnIx::new(n)`.
1425    pub fn visible_columns(&self) -> impl Iterator<Item = ColumnOf<D>> + '_ {
1426        self.columns.visible().map(column_of)
1427    }
1428
1429    /// The width of `column` (zero if it is not in the schema).
1430    pub fn column_width(&self, column: ColumnOf<D>) -> Pixels {
1431        self.columns.width(column.ix())
1432    }
1433
1434    /// Returns true if `column` is shown.
1435    pub fn is_column_visible(&self, column: ColumnOf<D>) -> bool {
1436        self.columns.is_visible(column.ix())
1437    }
1438
1439    /// The zone of `column`.
1440    pub fn column_zone(&self, column: ColumnOf<D>) -> ColumnZone {
1441        self.columns.zone_of(column.ix())
1442    }
1443
1444    /// The on-screen position of `column`; `None` if it is hidden.
1445    pub fn view_column_ix(&self, column: ColumnOf<D>) -> Option<ViewColumnIx> {
1446        self.columns.view_column_ix(column.ix())
1447    }
1448
1449    /// The visible column at an on-screen position.
1450    pub fn column_at(&self, ix: ViewColumnIx) -> Option<ColumnOf<D>> {
1451        self.columns.column_at(ix).map(column_of)
1452    }
1453
1454    /// The column that column commands (the column Actions) act on: the last
1455    /// header cell or band pressed, or the column the focus moved to,
1456    /// whichever came last. `None` if there is none or it is hidden.
1457    pub fn current_column(&self) -> Option<ColumnOf<D>> {
1458        self.current_column
1459            .filter(|ix| self.columns.is_visible(*ix))
1460            .map(column_of)
1461    }
1462
1463    /// Writes the current column. The last writer wins: a header press, the
1464    /// focused cell, the header menu.
1465    pub(super) fn set_current_column(&mut self, column: ColumnIx) {
1466        self.current_column = Some(column);
1467    }
1468
1469    /// Sets the width of `column`, kept within its limits. Emits one
1470    /// `LayoutChange` if it changed.
1471    pub fn set_column_width(&mut self, column: ColumnOf<D>, width: Pixels, cx: &mut Context<Self>) {
1472        self.change_layout(LayoutOrigin::Programmatic, cx, |layout, _| {
1473            if layout.set_width(column.ix(), width) {
1474                LayoutDelta::new(LayoutPart::Width, [column.ix()])
1475            } else {
1476                LayoutDelta::default()
1477            }
1478        });
1479    }
1480
1481    /// Shows or hides `column`; a shown column returns to its place.
1482    /// Returns false if refused: the last visible column cannot be hidden.
1483    pub fn set_column_visible(
1484        &mut self,
1485        column: ColumnOf<D>,
1486        visible: bool,
1487        cx: &mut Context<Self>,
1488    ) -> bool {
1489        self.set_column_visible_from(column.ix(), visible, LayoutOrigin::Programmatic, cx)
1490    }
1491
1492    fn set_column_visible_from(
1493        &mut self,
1494        ix: ColumnIx,
1495        visible: bool,
1496        origin: LayoutOrigin,
1497        cx: &mut Context<Self>,
1498    ) -> bool {
1499        let mut refused = false;
1500        self.change_layout(origin, cx, |layout, _| {
1501            match layout.set_visible(ix, visible) {
1502                Some(true) => LayoutDelta::new(LayoutPart::Visibility, [ix]),
1503                Some(false) => LayoutDelta::default(),
1504                None => {
1505                    refused = true;
1506                    LayoutDelta::default()
1507                }
1508            }
1509        });
1510        !refused
1511    }
1512
1513    /// Shows every column, with one `LayoutChange`.
1514    pub fn show_all_columns(&mut self, cx: &mut Context<Self>) {
1515        self.show_all_columns_from(LayoutOrigin::Programmatic, cx);
1516    }
1517
1518    pub(super) fn show_all_columns_from(&mut self, origin: LayoutOrigin, cx: &mut Context<Self>) {
1519        self.change_layout(origin, cx, |layout, _| {
1520            let shown = layout.show_all();
1521            if shown.is_empty() {
1522                LayoutDelta::default()
1523            } else {
1524                LayoutDelta {
1525                    parts: LayoutPart::Visibility.into(),
1526                    columns: shown,
1527                    reveal: ColumnSet::EMPTY,
1528                }
1529            }
1530        });
1531    }
1532
1533    /// Moves `column` to `zone`, at the new zone's edge nearest to its old
1534    /// place. The [`ColumnMovePolicy`] decides what moves along (with
1535    /// `BandedMove`, its whole top-level band).
1536    pub fn set_column_zone(
1537        &mut self,
1538        column: ColumnOf<D>,
1539        zone: ColumnZone,
1540        cx: &mut Context<Self>,
1541    ) {
1542        self.set_column_zone_from(column.ix(), zone, LayoutOrigin::Programmatic, cx);
1543    }
1544
1545    fn set_column_zone_from(
1546        &mut self,
1547        ix: ColumnIx,
1548        zone: ColumnZone,
1549        origin: LayoutOrigin,
1550        cx: &mut Context<Self>,
1551    ) {
1552        self.change_layout(origin, cx, |layout, policy| {
1553            let moved = layout.set_zone(ix, zone, policy);
1554            if moved.is_empty() {
1555                return LayoutDelta::default();
1556            }
1557            let mut delta = LayoutDelta::new(LayoutPart::Zone, moved);
1558            delta.parts = delta.parts.with(LayoutPart::Order);
1559            delta.revealing()
1560        });
1561    }
1562
1563    /// Moves `column` before `before` (`None`: to the end of its zone).
1564    /// Returns false if the zones differ or the move policy refuses.
1565    pub fn move_column(
1566        &mut self,
1567        column: ColumnOf<D>,
1568        before: Option<ColumnOf<D>>,
1569        cx: &mut Context<Self>,
1570    ) -> bool {
1571        let ix = column.ix();
1572        if self.columns.get(ix).is_none() {
1573            return false;
1574        }
1575        let zone = self.columns.zone_of(ix);
1576        self.move_before(zone, vec![ix], before.map(|c| c.ix()), cx)
1577    }
1578
1579    /// Moves every column of `band` in its zone before `before` (`None`: to
1580    /// the end of the zone). Returns false if the band is unknown, the zones
1581    /// differ or the move policy refuses.
1582    pub fn move_band(
1583        &mut self,
1584        band: &'static str,
1585        before: Option<ColumnOf<D>>,
1586        cx: &mut Context<Self>,
1587    ) -> bool {
1588        let Some(&first) = self
1589            .columns
1590            .order()
1591            .iter()
1592            .find(|ix| self.columns.bands_of(**ix).contains(&band))
1593        else {
1594            return false;
1595        };
1596        let zone = self.columns.zone_of(first);
1597        let members = self.columns.band_members(zone, band);
1598        self.move_before(zone, members, before.map(|c| c.ix()), cx)
1599    }
1600
1601    fn move_before(
1602        &mut self,
1603        zone: ColumnZone,
1604        members: Vec<ColumnIx>,
1605        before: Option<ColumnIx>,
1606        cx: &mut Context<Self>,
1607    ) -> bool {
1608        let mut refused = false;
1609        self.change_layout(LayoutOrigin::Programmatic, cx, |layout, policy| {
1610            let moved = members.clone();
1611            match layout.move_before(zone, members, before, policy) {
1612                Some(true) => LayoutDelta::new(LayoutPart::Order, moved).revealing(),
1613                Some(false) => LayoutDelta::default(),
1614                None => {
1615                    refused = true;
1616                    LayoutDelta::default()
1617                }
1618            }
1619        });
1620        !refused
1621    }
1622
1623    /// Fits the width of `column` to its header and the rows painted in the
1624    /// next painted frame; the `LayoutChange` comes with that frame.
1625    pub fn autofit_column(&mut self, column: ColumnOf<D>, cx: &mut Context<Self>) {
1626        self.request_autofit(column_set([column.ix()]), LayoutOrigin::Programmatic, cx);
1627    }
1628
1629    /// Fits every visible column the same way, with one `LayoutChange`.
1630    pub fn autofit_columns(&mut self, cx: &mut Context<Self>) {
1631        self.request_autofit(self.columns.visible_set(), LayoutOrigin::Programmatic, cx);
1632    }
1633
1634    pub(super) fn request_autofit(
1635        &mut self,
1636        columns: ColumnSet,
1637        origin: LayoutOrigin,
1638        cx: &mut Context<Self>,
1639    ) {
1640        let pending = self
1641            .pending_autofit
1642            .get_or_insert((ColumnSet::EMPTY, origin));
1643        pending.0.union_with(columns);
1644        pending.1 = origin;
1645        cx.notify();
1646    }
1647
1648    /// Measures and applies a requested autofit at the start of a frame
1649    /// plan, over the lines this frame paints: data rows, pinned rows, and
1650    /// the aggregates of captions, footers and the totals row, whose
1651    /// `aggregate_style` runs here for their weight. Returns the change, to be
1652    /// emitted after the frame. While the first rows of a data set are still
1653    /// being projected, the request waits for the frame that paints them.
1654    pub(super) fn apply_pending_autofit(
1655        &mut self,
1656        needed_lines: usize,
1657        window: &Window,
1658        cx: &App,
1659    ) -> Option<(LayoutDelta, LayoutOrigin)> {
1660        let loading = self.projection.is_empty()
1661            && !self.snapshot.is_empty()
1662            && (self.rebuild_task.is_some() || self.pending != super::PendingWork::None);
1663        if loading {
1664            return None;
1665        }
1666        let (requested, origin) = self.pending_autofit.take()?;
1667        let projection = self.projection.clone();
1668        let snapshot = self.snapshot.clone();
1669        let start = self.scroll_row.min(projection.len());
1670        let mut end = start;
1671        let mut lines = 0;
1672        // Group captions and footers take a line each.
1673        let mut summaries = Vec::new();
1674        while lines < needed_lines && end < projection.len() {
1675            match projection.line(end) {
1676                Some(DisplayRow::Data { row, .. }) => {
1677                    lines += usize::from(snapshot.row(row).is_some());
1678                }
1679                Some(
1680                    line @ (DisplayRow::GroupCaption { group, .. }
1681                    | DisplayRow::GroupFooter { group, .. }),
1682                ) => {
1683                    lines += 1;
1684                    if let Some(group) = projection.groups().get(group) {
1685                        summaries.push(SummaryLine {
1686                            line: if matches!(line, DisplayRow::GroupFooter { .. }) {
1687                                AggregateLine::Footer(group)
1688                            } else {
1689                                AggregateLine::Caption(group)
1690                            },
1691                            aggregates: group.aggregates(),
1692                            weight: None,
1693                            label: None,
1694                        });
1695                    }
1696                }
1697                None => {}
1698            }
1699            end += 1;
1700        }
1701
1702        // The pinned rows this frame paints, and the totals row with its
1703        // label in the first visible column, if it shows.
1704        let bands = self.body_bands();
1705        let mut pinned: Vec<Arc<D::Row>> = Vec::new();
1706        for edge in EDGES {
1707            for (line, _) in bands.visible_lines(edge) {
1708                match line {
1709                    BodyLine::Pinned(_, ix) => {
1710                        pinned.extend(snapshot.pinned_rows(edge).get(ix).cloned());
1711                    }
1712                    BodyLine::Totals(_) => summaries.push(SummaryLine {
1713                        line: AggregateLine::Totals(edge),
1714                        aggregates: projection.totals(),
1715                        weight: Some(TOTALS_WEIGHT),
1716                        label: self
1717                            .columns
1718                            .visible()
1719                            .next()
1720                            .map(|first| (self.summary_texts.total(), first)),
1721                    }),
1722                    BodyLine::Data(_) => {}
1723                }
1724            }
1725        }
1726
1727        let (font, font_size) = header_label_style(window);
1728        let mut widths = Vec::new();
1729        let mut buffer = String::new();
1730        let mut arena = Vec::new();
1731        // An icon or an image is measured as itself, not its text; a tree
1732        // row's indent and chevron lane are part of its cell.
1733        let icon_width = cell_icon_size(self.row_height);
1734        let image_width = cell_image_size(self.row_height);
1735        let tree_column = projection.is_tree().then(|| self.tree_column()).flatten();
1736        let targets: Vec<ColumnIx> = self
1737            .columns
1738            .visible()
1739            .filter(|ix| requested.contains(*ix))
1740            .collect();
1741        for ix in targets {
1742            let column: ColumnOf<D> = column_of(ix);
1743            let schema_column = self.columns.column(ix);
1744            let title = schema_column.title.clone();
1745            // A sortable column keeps room for the sort indicator, so a
1746            // later sort does not truncate a fitted title.
1747            let lane = if schema_column.sortable {
1748                HEADER_SORT_LANE
1749            } else {
1750                px(0.)
1751            };
1752            // The header's icon, and its gap before the title; a header that
1753            // shows the icon alone fits the icon.
1754            let icon = header_icon_lane(&schema_column.header_label, self.row_height);
1755            let title_width = match schema_column.header_label {
1756                HeaderLabel::Icon(_) => px(0.),
1757                HeaderLabel::Title | HeaderLabel::IconAndTitle(_) => self
1758                    .header
1759                    .labels
1760                    .width(ix, &title, &font, font_size, window),
1761            };
1762            let header = icon + title_width + lane;
1763            let summary_width = SummaryMeasure {
1764                delegate: &self.delegate,
1765                app: cx,
1766                text_cache: &mut self.text_cache,
1767                buffer: &mut buffer,
1768                arena: &mut arena,
1769                format: self.columns.number_format(ix),
1770            }
1771            .width(&summaries, column, window);
1772            let text_cache = &mut self.text_cache;
1773            let format = self.columns.number_format(ix);
1774            let mut measure_text = |row: &D::Row| {
1775                let value = row.value(column);
1776                match value {
1777                    CellValue::Icon(_) => return icon_width,
1778                    CellValue::Image(_) => return image_width,
1779                    _ => {}
1780                }
1781                let text = display_text_in(value, format, &mut buffer);
1782                if text.is_empty() {
1783                    return px(0.);
1784                }
1785                arena.clear();
1786                text_cache
1787                    .regular()
1788                    .layout(text, window, &mut arena)
1789                    .width()
1790            };
1791            let pinned_width = pinned
1792                .iter()
1793                .map(|row| measure_text(row))
1794                .fold(px(0.), Pixels::max);
1795            let mut measure = |display_ix: usize| {
1796                let DisplayRow::Data { row, depth } = projection.line(display_ix)? else {
1797                    return None;
1798                };
1799                let row = snapshot.row(row)?;
1800                let indent = if tree_column == Some(ix) {
1801                    INDENT * f32::from(depth) + LEAD_ICON_LANE
1802                } else {
1803                    px(0.)
1804                };
1805                Some(measure_text(row) + indent)
1806            };
1807            let mut context =
1808                AutofitContext::new(header, start..end, projection.len(), &mut measure)
1809                    .with_pinned_width(pinned_width)
1810                    .with_summary_width(summary_width);
1811            let content = self.autofit.content_width(&mut context);
1812            widths.push((ix, content.ceil() + CELL_PADDING * 2. + px(1.)));
1813        }
1814        let delta = self.relayout(|layout, _| {
1815            let mut delta = LayoutDelta::default();
1816            for (ix, width) in widths {
1817                if layout.set_width(ix, width) {
1818                    delta.union_with(LayoutDelta::new(LayoutPart::Width, [ix]));
1819                }
1820            }
1821            delta
1822        });
1823        (!delta.is_empty()).then_some((delta, origin))
1824    }
1825
1826    /// Applies a layout change with the scroll anchor kept, then emits one
1827    /// `LayoutChange` and repaints. Returns false if nothing changed.
1828    pub(super) fn change_layout(
1829        &mut self,
1830        origin: LayoutOrigin,
1831        cx: &mut Context<Self>,
1832        change: impl FnOnce(&mut ColumnLayout<ColumnOf<D>>, &dyn ColumnMovePolicy) -> LayoutDelta,
1833    ) -> bool {
1834        let visible = self.columns.visible_set();
1835        let delta = self.relayout(change);
1836        if delta.is_empty() {
1837            return false;
1838        }
1839        self.emit_layout_change(delta, origin, cx);
1840        // After the event: a focus moved off a hidden column follows
1841        // its `LayoutChange`.
1842        if self.columns.visible_set() != visible {
1843            self.visible_columns_changed(cx);
1844        }
1845        cx.notify();
1846        true
1847    }
1848
1849    /// Applies a layout change keeping the first visible scrolling column at
1850    /// its place on screen, then scrolls moved columns into view. Does not
1851    /// notify: frame planning calls it.
1852    pub(super) fn relayout(
1853        &mut self,
1854        change: impl FnOnce(&mut ColumnLayout<ColumnOf<D>>, &dyn ColumnMovePolicy) -> LayoutDelta,
1855    ) -> LayoutDelta {
1856        let anchor = self.scroll_anchor();
1857        let delta = change(&mut self.columns, &*self.column_move);
1858        if delta.is_empty() {
1859            return delta;
1860        }
1861        self.restore_scroll_anchor(anchor);
1862        if !delta.reveal.is_empty() {
1863            self.reveal_columns(delta.reveal);
1864        }
1865        self.clamp_scroll();
1866        delta
1867    }
1868
1869    /// Emits a `LayoutChange`. A focus on a column it hid then moves to the
1870    /// next visible column, so `FocusChange` follows with the matching origin.
1871    pub(super) fn emit_layout_change(
1872        &mut self,
1873        delta: LayoutDelta,
1874        origin: LayoutOrigin,
1875        cx: &mut Context<Self>,
1876    ) {
1877        cx.emit(super::LiveGridEvent::LayoutChange {
1878            parts: delta.parts,
1879            columns: delta.columns,
1880            origin,
1881        });
1882        let origin = match origin {
1883            LayoutOrigin::User => SelectionOrigin::User,
1884            LayoutOrigin::Programmatic => SelectionOrigin::Programmatic,
1885        };
1886        self.move_focus_off_hidden(origin, cx);
1887    }
1888
1889    /// Called when the set of visible columns changed, after its
1890    /// `LayoutChange`: the quick filter of the default columns is rebuilt,
1891    /// and a focus left on a hidden column moves off it. A restored layout
1892    /// does the same in its own order, with one rebuild for its sort and
1893    /// its columns (`restore_layout`).
1894    pub(super) fn visible_columns_changed(&mut self, cx: &mut Context<Self>) {
1895        self.move_focus_off_hidden(SelectionOrigin::Programmatic, cx);
1896        self.quick_filter_columns_changed(cx);
1897    }
1898
1899    /// The column under a window position, for hit tests of the body.
1900    pub(crate) fn column_at_position(&self, position: Point<Pixels>) -> Option<ColumnIx> {
1901        let x = position.x - self.body_bounds.origin.x;
1902        self.columns
1903            .column_at_x(x, self.body_bounds.size.width, self.scroll_x)
1904    }
1905
1906    /// Moves the current column one visible place left (`-1`) or right
1907    /// (`1`) within its zone, if the move policy allows it.
1908    fn move_current_column(&mut self, step: isize, cx: &mut Context<Self>) {
1909        let Some(ix) = self
1910            .current_column
1911            .filter(|ix| self.columns.is_visible(*ix))
1912        else {
1913            return;
1914        };
1915        let Some((zone, i)) = self.columns.zone_position(ix) else {
1916            return;
1917        };
1918        let boundary = if step < 0 {
1919            match i.checked_sub(1) {
1920                Some(k) => k,
1921                None => return,
1922            }
1923        } else {
1924            i + 2
1925        };
1926        let Boundary::Gap(gap) = self
1927            .columns
1928            .zone_move(zone, vec![ix])
1929            .resolve(boundary, &*self.column_move)
1930        else {
1931            return;
1932        };
1933        self.change_layout(LayoutOrigin::User, cx, |layout, _| {
1934            if layout.move_members(zone, &[ix], gap) {
1935                LayoutDelta::new(LayoutPart::Order, [ix]).revealing()
1936            } else {
1937                LayoutDelta::default()
1938            }
1939        });
1940    }
1941
1942    fn current_visible_column(&self) -> Option<ColumnIx> {
1943        self.current_column
1944            .filter(|ix| self.columns.is_visible(*ix))
1945    }
1946
1947    pub(super) fn on_autofit_columns(
1948        &mut self,
1949        _: &AutofitColumns,
1950        _: &mut Window,
1951        cx: &mut Context<Self>,
1952    ) {
1953        self.request_autofit(self.columns.visible_set(), LayoutOrigin::User, cx);
1954    }
1955
1956    pub(super) fn on_autofit_column(
1957        &mut self,
1958        _: &AutofitColumn,
1959        _: &mut Window,
1960        cx: &mut Context<Self>,
1961    ) {
1962        if let Some(ix) = self.current_visible_column() {
1963            self.request_autofit(column_set([ix]), LayoutOrigin::User, cx);
1964        }
1965    }
1966
1967    pub(super) fn on_hide_column(
1968        &mut self,
1969        _: &HideColumn,
1970        _: &mut Window,
1971        cx: &mut Context<Self>,
1972    ) {
1973        // The user does not hide a column that is not hideable;
1974        // `set_column_visible` does.
1975        if let Some(ix) = self.current_visible_column()
1976            && self.columns.column(ix).hideable
1977        {
1978            self.set_column_visible_from(ix, false, LayoutOrigin::User, cx);
1979        }
1980    }
1981
1982    pub(super) fn on_show_all_columns(
1983        &mut self,
1984        _: &ShowAllColumns,
1985        _: &mut Window,
1986        cx: &mut Context<Self>,
1987    ) {
1988        self.show_all_columns_from(LayoutOrigin::User, cx);
1989    }
1990
1991    pub(super) fn on_freeze_column_left(
1992        &mut self,
1993        _: &FreezeColumnLeft,
1994        _: &mut Window,
1995        cx: &mut Context<Self>,
1996    ) {
1997        if let Some(ix) = self.current_visible_column() {
1998            self.set_column_zone_from(ix, ColumnZone::FrozenLeft, LayoutOrigin::User, cx);
1999        }
2000    }
2001
2002    pub(super) fn on_freeze_column_right(
2003        &mut self,
2004        _: &FreezeColumnRight,
2005        _: &mut Window,
2006        cx: &mut Context<Self>,
2007    ) {
2008        if let Some(ix) = self.current_visible_column() {
2009            self.set_column_zone_from(ix, ColumnZone::FrozenRight, LayoutOrigin::User, cx);
2010        }
2011    }
2012
2013    pub(super) fn on_unfreeze_column(
2014        &mut self,
2015        _: &UnfreezeColumn,
2016        _: &mut Window,
2017        cx: &mut Context<Self>,
2018    ) {
2019        if let Some(ix) = self.current_visible_column() {
2020            self.set_column_zone_from(ix, ColumnZone::Scrolling, LayoutOrigin::User, cx);
2021        }
2022    }
2023
2024    pub(super) fn on_move_column_left(
2025        &mut self,
2026        _: &MoveColumnLeft,
2027        _: &mut Window,
2028        cx: &mut Context<Self>,
2029    ) {
2030        self.move_current_column(-1, cx);
2031    }
2032
2033    pub(super) fn on_move_column_right(
2034        &mut self,
2035        _: &MoveColumnRight,
2036        _: &mut Window,
2037        cx: &mut Context<Self>,
2038    ) {
2039        self.move_current_column(1, cx);
2040    }
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045    use std::time::Duration;
2046
2047    use gpui_kit::px;
2048
2049    use super::{
2050        ColumnAlign, ColumnBand, ColumnLayout, ColumnZone, GridColumn, GridSchema,
2051        MIN_COLUMN_WIDTH, ViewColumnIx,
2052    };
2053    use crate::{
2054        highlight::HighlightPolicy,
2055        model::{ColumnIx, GridColumns},
2056        policy::{BandedMove, ColumnMovePolicy, FreeMove},
2057        state::GridTuning,
2058        store::StoreOptions,
2059    };
2060
2061    /// Up to 12 columns `c0`..`c11`.
2062    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2063    struct TestColumn(u16);
2064
2065    static ALL: [TestColumn; 12] = {
2066        let mut all = [TestColumn(0); 12];
2067        let mut ix = 0;
2068        while ix < 12 {
2069            all[ix] = TestColumn(ix as u16);
2070            ix += 1;
2071        }
2072        all
2073    };
2074
2075    const IDS: [&str; 12] = [
2076        "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11",
2077    ];
2078    const BANDS: [&str; 12] = [
2079        "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "b10", "b11",
2080    ];
2081
2082    impl GridColumns for TestColumn {
2083        fn all() -> &'static [Self] {
2084            &ALL
2085        }
2086
2087        fn id(self) -> &'static str {
2088            IDS[self.0 as usize]
2089        }
2090
2091        fn ix(self) -> ColumnIx {
2092            ColumnIx::new(self.0)
2093        }
2094    }
2095
2096    fn ix(i: u16) -> ColumnIx {
2097        ColumnIx::new(i)
2098    }
2099
2100    fn column(i: u16) -> GridColumn<TestColumn> {
2101        GridColumn::new(TestColumn(i), IDS[i as usize])
2102    }
2103
2104    fn zone_order(layout: &ColumnLayout<TestColumn>, zone: ColumnZone) -> Vec<u16> {
2105        layout
2106            .zone_order(zone)
2107            .iter()
2108            .map(|c| c.get() as u16)
2109            .collect()
2110    }
2111
2112    fn visible(layout: &ColumnLayout<TestColumn>) -> Vec<u16> {
2113        layout.visible().map(|c| c.get() as u16).collect()
2114    }
2115
2116    #[test]
2117    fn test_grid_schema_builder() {
2118        let bid = GridColumn::new(TestColumn(1), "Bid")
2119            .with_width(px(80.))
2120            .with_align(ColumnAlign::End)
2121            .with_min_width(px(40.))
2122            .with_max_width(px(300.))
2123            .with_zone(ColumnZone::FrozenRight)
2124            .with_visible(false)
2125            .with_resizable(false)
2126            .with_movable(false)
2127            .with_sortable(false)
2128            .with_groupable(false);
2129        assert_eq!(bid.column(), TestColumn(1));
2130        assert_eq!(bid.title().as_ref(), "Bid");
2131        assert_eq!(bid.width(), px(80.));
2132        assert_eq!(bid.align(), ColumnAlign::End);
2133        assert_eq!(bid.min_width(), px(40.));
2134        assert_eq!(bid.max_width(), px(300.));
2135        assert_eq!(bid.zone(), ColumnZone::FrozenRight);
2136        assert!(!bid.is_visible());
2137        assert!(!bid.is_resizable());
2138        assert!(!bid.is_movable());
2139        assert!(!bid.is_sortable());
2140        assert!(!bid.is_groupable());
2141        // Defaults: 100 px wide, numbers right and text left, scrolling,
2142        // shown, resizable, movable, sortable, groupable, no upper width
2143        // limit.
2144        let plain = column(2);
2145        assert_eq!(plain.width(), px(100.));
2146        assert_eq!(plain.align(), ColumnAlign::Auto);
2147        assert_eq!(plain.min_width(), MIN_COLUMN_WIDTH);
2148        assert_eq!(plain.zone(), ColumnZone::Scrolling);
2149        assert!(plain.is_visible() && plain.is_resizable() && plain.is_movable());
2150        assert!(plain.is_sortable() && plain.is_groupable());
2151
2152        let band = ColumnBand::new("quote", "Quote")
2153            .column(column(2))
2154            .band(ColumnBand::new("depth", "Depth").column(column(3)));
2155        assert_eq!(band.id(), "quote");
2156        assert_eq!(band.title().as_ref(), "Quote");
2157
2158        // `with_frozen` freezes the first declared columns over `with_zone`.
2159        let layout = ColumnLayout::new(
2160            GridSchema::new()
2161                .with_frozen(2)
2162                .column(column(0).with_zone(ColumnZone::FrozenRight))
2163                .band(band)
2164                .column(column(4).with_zone(ColumnZone::FrozenRight)),
2165        );
2166        assert_eq!(zone_order(&layout, ColumnZone::FrozenLeft), [0, 2]);
2167        assert_eq!(zone_order(&layout, ColumnZone::Scrolling), [3]);
2168        assert_eq!(zone_order(&layout, ColumnZone::FrozenRight), [4]);
2169        assert_eq!(layout.bands_of(ix(3)), ["quote", "depth"]);
2170        assert_eq!(layout.depth(), 2);
2171
2172        let tuning = GridTuning::default();
2173        assert_eq!(tuning.min_rebuild_interval, Duration::from_millis(100));
2174        assert_eq!(tuning.pull_interval, Duration::from_millis(8));
2175        let tuning = tuning
2176            .with_min_rebuild_interval(Duration::from_millis(250))
2177            .with_pull_interval(Duration::from_millis(16));
2178        assert_eq!(tuning.min_rebuild_interval, Duration::from_millis(250));
2179        assert_eq!(tuning.pull_interval, Duration::from_millis(16));
2180
2181        let options = StoreOptions::default();
2182        assert_eq!(options.highlight(), HighlightPolicy::default());
2183        // The store publishes no more often than a grid pulls.
2184        assert_eq!(options.publish_interval(), Duration::from_millis(8));
2185        assert_eq!(
2186            options.publish_interval(),
2187            GridTuning::default().pull_interval
2188        );
2189        let highlight = HighlightPolicy::new(Duration::from_secs(2), 4);
2190        let options = options
2191            .with_highlight(highlight)
2192            .with_publish_interval(Duration::from_millis(10));
2193        assert_eq!(options.highlight(), highlight);
2194        assert_eq!(options.publish_interval(), Duration::from_millis(10));
2195    }
2196
2197    #[test]
2198    #[should_panic(expected = "already in the schema")]
2199    fn test_schema_rejects_repeated_column() {
2200        let _ = GridSchema::new()
2201            .column(column(1))
2202            .band(ColumnBand::new("b", "B").column(column(1)));
2203    }
2204
2205    #[test]
2206    #[should_panic(expected = "band \"b\" is already in the schema")]
2207    fn test_schema_rejects_repeated_band() {
2208        let _ = GridSchema::new()
2209            .band(ColumnBand::new("b", "B").column(column(1)))
2210            .band(ColumnBand::new("b", "B").column(column(2)));
2211    }
2212
2213    /// c0 50 px frozen left; c1 100, c2 80, c3 120 scrolling; c4 60 and
2214    /// c5 40 frozen right.
2215    fn zoned() -> ColumnLayout<TestColumn> {
2216        ColumnLayout::new(
2217            GridSchema::new()
2218                .column(
2219                    column(0)
2220                        .with_width(px(50.))
2221                        .with_zone(ColumnZone::FrozenLeft),
2222                )
2223                .column(column(1))
2224                .column(column(2).with_width(px(80.)))
2225                .column(column(3).with_width(px(120.)))
2226                .column(
2227                    column(4)
2228                        .with_width(px(60.))
2229                        .with_zone(ColumnZone::FrozenRight),
2230                )
2231                .column(
2232                    column(5)
2233                        .with_width(px(40.))
2234                        .with_zone(ColumnZone::FrozenRight),
2235                ),
2236        )
2237    }
2238
2239    #[test]
2240    fn test_column_layout_zones_and_offsets() {
2241        let layout = zoned();
2242        let offsets = |zone| layout.zone_columns(zone).offsets.clone();
2243        assert_eq!(offsets(ColumnZone::FrozenLeft), [px(0.), px(50.)]);
2244        assert_eq!(
2245            offsets(ColumnZone::Scrolling),
2246            [px(0.), px(100.), px(180.), px(300.)]
2247        );
2248        assert_eq!(
2249            offsets(ColumnZone::FrozenRight),
2250            [px(0.), px(60.), px(100.)]
2251        );
2252        assert_eq!(layout.total_width(), px(450.));
2253
2254        // A 300 px body: the right zone is pinned to the right edge and the
2255        // scrolling zone, 150 px, lies between.
2256        let geometry = layout.geometry(px(300.), px(0.));
2257        let [left, scrolling, right] = &geometry.zones;
2258        assert_eq!((left.x, left.width), (px(0.), px(50.)));
2259        assert_eq!((scrolling.x, scrolling.width), (px(50.), px(150.)));
2260        assert_eq!((right.x, right.width), (px(200.), px(100.)));
2261        assert_eq!(scrolling.columns, 0..2);
2262        assert_eq!(right.columns, 0..2);
2263        // c1 ends exactly at the scroll offset: it is out.
2264        assert_eq!(layout.geometry(px(300.), px(100.)).zones[1].columns, 1..3);
2265        assert_eq!(layout.geometry(px(300.), px(160.)).zones[1].columns, 1..3);
2266
2267        // Hit tests: the right zone does not depend on the scroll offset.
2268        for scroll_x in [px(0.), px(100.)] {
2269            assert_eq!(layout.column_at_x(px(10.), px(300.), scroll_x), Some(ix(0)));
2270            assert_eq!(
2271                layout.column_at_x(px(210.), px(300.), scroll_x),
2272                Some(ix(4))
2273            );
2274            assert_eq!(
2275                layout.column_at_x(px(270.), px(300.), scroll_x),
2276                Some(ix(5))
2277            );
2278            assert_eq!(layout.column_at_x(px(300.), px(300.), scroll_x), None);
2279            assert_eq!(layout.column_at_x(px(-1.), px(300.), scroll_x), None);
2280        }
2281        assert_eq!(layout.column_at_x(px(60.), px(300.), px(0.)), Some(ix(1)));
2282        // 60 px on screen is 110 px into the scrolling content: c2.
2283        assert_eq!(layout.column_at_x(px(60.), px(300.), px(100.)), Some(ix(2)));
2284        assert_eq!(layout.column_at_x(px(199.), px(300.), px(0.)), Some(ix(2)));
2285
2286        // Frozen zones wider than the body: the left one first, the right
2287        // one gets the rest, the scrolling zone none; the edge cuts them.
2288        let geometry = layout.geometry(px(120.), px(0.));
2289        let [left, scrolling, right] = &geometry.zones;
2290        assert_eq!((left.x, left.width), (px(0.), px(50.)));
2291        assert_eq!((scrolling.width, scrolling.columns.clone()), (px(0.), 0..0));
2292        assert_eq!((right.x, right.width), (px(50.), px(70.)));
2293        assert_eq!(layout.column_at_x(px(60.), px(120.), px(0.)), Some(ix(4)));
2294        assert_eq!(layout.column_at_x(px(115.), px(120.), px(0.)), Some(ix(5)));
2295        let geometry = layout.geometry(px(30.), px(0.));
2296        assert_eq!(geometry.zones[0].width, px(30.));
2297        assert_eq!(geometry.zones[2].width, px(0.));
2298        assert_eq!(layout.column_at_x(px(29.), px(30.), px(0.)), Some(ix(0)));
2299
2300        // No frozen columns: everything scrolls, from the left edge.
2301        let layout = ColumnLayout::new(
2302            GridSchema::new()
2303                .column(column(0).with_width(px(50.)))
2304                .column(column(1)),
2305        );
2306        let geometry = layout.geometry(px(120.), px(60.));
2307        assert_eq!(
2308            (geometry.zones[1].x, geometry.zones[1].width),
2309            (px(0.), px(120.))
2310        );
2311        assert_eq!(geometry.zones[1].columns, 1..2);
2312        assert_eq!(layout.column_at_x(px(10.), px(120.), px(60.)), Some(ix(1)));
2313
2314        let empty = ColumnLayout::new(GridSchema::<TestColumn>::new().with_frozen(2));
2315        assert_eq!(empty.total_width(), px(0.));
2316        assert_eq!(empty.geometry(px(200.), px(0.)).zones[1].columns, 0..0);
2317        assert_eq!(empty.column_at_x(px(0.), px(200.), px(0.)), None);
2318    }
2319
2320    #[test]
2321    fn test_zone_change_places_column_at_nearest_edge() {
2322        // c0 c1 | c2 c3 c4 | c5 c6
2323        let layout = || {
2324            ColumnLayout::new(
2325                GridSchema::new()
2326                    .with_frozen(2)
2327                    .column(column(0))
2328                    .column(column(1))
2329                    .column(column(2))
2330                    .column(column(3))
2331                    .column(column(4))
2332                    .column(column(5).with_zone(ColumnZone::FrozenRight))
2333                    .column(column(6).with_zone(ColumnZone::FrozenRight)),
2334            )
2335        };
2336        let all = |layout: &ColumnLayout<TestColumn>| {
2337            ColumnZone::ALL.map(|zone| zone_order(layout, zone))
2338        };
2339        use ColumnZone::{FrozenLeft as L, FrozenRight as R, Scrolling as S};
2340        for (column, zone, expected) in [
2341            // Scrolling to left: the end of the left zone, next to the scrolling one.
2342            (3, L, [vec![0, 1, 3], vec![2, 4], vec![5, 6]]),
2343            // Scrolling to right: the start of the right zone.
2344            (3, R, [vec![0, 1], vec![2, 4], vec![3, 5, 6]]),
2345            // Left to scrolling: the start of the scrolling zone.
2346            (0, S, [vec![1], vec![0, 2, 3, 4], vec![5, 6]]),
2347            // Right to scrolling: the end of the scrolling zone.
2348            (6, S, [vec![0, 1], vec![2, 3, 4, 6], vec![5]]),
2349            // Left to right: the start of the right zone.
2350            (0, R, [vec![1], vec![2, 3, 4], vec![0, 5, 6]]),
2351            // Right to left: the end of the left zone.
2352            (6, L, [vec![0, 1, 6], vec![2, 3, 4], vec![5]]),
2353        ] {
2354            let mut layout = layout();
2355            assert_eq!(layout.set_zone(ix(column), zone, &FreeMove), [ix(column)]);
2356            assert_eq!(all(&layout), expected, "c{column} to {zone:?}");
2357            assert_eq!(layout.zone_of(ix(column)), zone);
2358            // The same zone again changes nothing.
2359            assert!(layout.set_zone(ix(column), zone, &FreeMove).is_empty());
2360        }
2361    }
2362
2363    #[test]
2364    fn test_column_width_limits() {
2365        let narrow = column(0).with_min_width(px(5.));
2366        assert_eq!(
2367            narrow.min_width(),
2368            MIN_COLUMN_WIDTH,
2369            "raised to the minimum"
2370        );
2371        let limited = column(1)
2372            .with_width(px(2.))
2373            .with_min_width(px(30.))
2374            .with_max_width(px(200.));
2375        for (width, expected) in [
2376            (px(50.), px(50.)),
2377            (px(1000.), px(200.)),
2378            (px(12.), px(30.)),
2379            (px(0.), px(30.)),
2380            (px(-5.), px(30.)),
2381            (px(f32::NAN), px(30.)),
2382            (px(f32::INFINITY), px(30.)),
2383        ] {
2384            assert_eq!(limited.clamp_width(width), expected, "{width:?}");
2385        }
2386        assert_eq!(column(2).clamp_width(px(3.)), MIN_COLUMN_WIDTH);
2387
2388        let mut layout = ColumnLayout::new(GridSchema::new().column(limited).column(column(2)));
2389        assert_eq!(
2390            layout.width(ix(1)),
2391            px(30.),
2392            "the schema width is clamped too"
2393        );
2394        assert!(layout.set_width(ix(1), px(120.)));
2395        assert!(!layout.set_width(ix(1), px(120.)), "unchanged");
2396        assert!(layout.set_width(ix(2), px(f32::NAN)));
2397        assert_eq!(layout.width(ix(2)), MIN_COLUMN_WIDTH);
2398        assert!(!layout.set_width(ix(9), px(50.)), "not in the schema");
2399        assert_eq!(
2400            layout.zone_columns(ColumnZone::Scrolling).offsets[2],
2401            px(130.)
2402        );
2403    }
2404
2405    #[test]
2406    fn test_hide_show_keeps_position() {
2407        let mut layout =
2408            ColumnLayout::new((0..4).fold(GridSchema::new(), |schema, i| schema.column(column(i))));
2409        assert_eq!(layout.set_visible(ix(1), false), Some(true));
2410        assert_eq!(layout.set_visible(ix(1), false), Some(false));
2411        assert_eq!(visible(&layout), [0, 2, 3]);
2412        assert_eq!(zone_order(&layout, ColumnZone::Scrolling), [0, 1, 2, 3]);
2413        // Moves around a hidden column leave it at its place.
2414        assert_eq!(
2415            layout.move_before(ColumnZone::Scrolling, vec![ix(3)], Some(ix(0)), &FreeMove),
2416            Some(true)
2417        );
2418        assert_eq!(zone_order(&layout, ColumnZone::Scrolling), [3, 0, 1, 2]);
2419        assert_eq!(layout.set_visible(ix(1), true), Some(true));
2420        assert_eq!(visible(&layout), [3, 0, 1, 2]);
2421
2422        // The last visible column cannot be hidden.
2423        for i in [0, 1, 2] {
2424            assert_eq!(layout.set_visible(ix(i), false), Some(true));
2425        }
2426        assert_eq!(layout.set_visible(ix(3), false), None);
2427        assert_eq!(visible(&layout), [3]);
2428        let shown = layout.show_all();
2429        assert_eq!(shown.len(), 3);
2430        assert!(shown.contains(ix(0)) && !shown.contains(ix(3)));
2431        assert_eq!(visible(&layout), [3, 0, 1, 2]);
2432        assert!(layout.show_all().is_empty());
2433        assert_eq!(layout.set_visible(ix(9), false), None, "not in the schema");
2434
2435        // A schema with every column hidden shows the first.
2436        let layout = ColumnLayout::new(
2437            GridSchema::new()
2438                .column(column(5).with_visible(false))
2439                .column(column(6).with_visible(false)),
2440        );
2441        assert_eq!(visible(&layout), [5]);
2442    }
2443
2444    #[test]
2445    fn test_view_column_ix_round_trip() {
2446        let mut layout = zoned();
2447        layout.set_visible(ix(2), false);
2448        layout.set_visible(ix(5), false);
2449        // Left zone first, then scrolling, then right; hidden ones skipped.
2450        assert_eq!(visible(&layout), [0, 1, 3, 4]);
2451        for (n, column) in layout.visible().collect::<Vec<_>>().into_iter().enumerate() {
2452            let view = layout.view_column_ix(column).expect("visible");
2453            assert_eq!(view, ViewColumnIx::new(n as u16));
2454            assert_eq!(view.get(), n);
2455            assert_eq!(layout.column_at(view), Some(column));
2456        }
2457        assert_eq!(layout.view_column_ix(ix(2)), None);
2458        assert_eq!(layout.view_column_ix(ix(5)), None);
2459        assert_eq!(layout.view_column_ix(ix(11)), None, "not in the schema");
2460        assert_eq!(layout.column_at(ViewColumnIx::new(4)), None);
2461        assert_eq!(
2462            layout.zone_position(ix(3)),
2463            Some((ColumnZone::Scrolling, 1))
2464        );
2465        assert_eq!(
2466            layout.zone_position(ix(4)),
2467            Some((ColumnZone::FrozenRight, 0))
2468        );
2469    }
2470
2471    /// c0, band b0 [c1, band b1 [c2, c3], c4], c5.
2472    fn banded() -> GridSchema<TestColumn> {
2473        GridSchema::new()
2474            .column(column(0))
2475            .band(
2476                ColumnBand::new("b0", "Outer")
2477                    .column(column(1))
2478                    .band(
2479                        ColumnBand::new("b1", "Inner")
2480                            .column(column(2))
2481                            .column(column(3)),
2482                    )
2483                    .column(column(4)),
2484            )
2485            .column(column(5))
2486    }
2487
2488    #[test]
2489    fn test_banded_move_keeps_bands_together() {
2490        let mut layout = ColumnLayout::new(banded());
2491        layout.normalize(&BandedMove);
2492        let scrolling =
2493            |layout: &ColumnLayout<TestColumn>| zone_order(layout, ColumnZone::Scrolling);
2494        let zone = ColumnZone::Scrolling;
2495        // Out of the band: refused; within it: done.
2496        assert_eq!(
2497            layout.move_before(zone, vec![ix(2)], Some(ix(0)), &BandedMove),
2498            None
2499        );
2500        assert_eq!(
2501            layout.move_before(zone, vec![ix(3)], Some(ix(2)), &BandedMove),
2502            Some(true)
2503        );
2504        assert_eq!(scrolling(&layout), [0, 1, 3, 2, 4, 5]);
2505        // A band moves whole.
2506        let members = layout.band_members(zone, "b0");
2507        assert_eq!(members, [1, 3, 2, 4].map(ix));
2508        assert_eq!(
2509            layout.move_before(zone, members, None, &BandedMove),
2510            Some(true)
2511        );
2512        assert_eq!(scrolling(&layout), [0, 5, 1, 3, 2, 4]);
2513        // Its zone changes for the whole top-level band.
2514        assert_eq!(
2515            layout.set_zone(ix(2), ColumnZone::FrozenLeft, &BandedMove),
2516            [1, 3, 2, 4].map(ix)
2517        );
2518        assert_eq!(zone_order(&layout, ColumnZone::FrozenLeft), [1, 3, 2, 4]);
2519        // Before itself: nothing to do.
2520        assert_eq!(
2521            layout.move_before(
2522                ColumnZone::FrozenLeft,
2523                vec![ix(1)],
2524                Some(ix(1)),
2525                &BandedMove
2526            ),
2527            Some(false)
2528        );
2529        // `normalize` gathers a band that a free order split.
2530        let mut layout = ColumnLayout::new(banded());
2531        layout.set_zone(ix(4), ColumnZone::FrozenRight, &FreeMove);
2532        layout.move_before(zone, vec![ix(0)], Some(ix(3)), &FreeMove);
2533        assert_eq!(scrolling(&layout), [1, 2, 0, 3, 5]);
2534        layout.normalize(&BandedMove);
2535        assert_eq!(scrolling(&layout), [1, 2, 3, 4, 0, 5]);
2536        assert!(zone_order(&layout, ColumnZone::FrozenRight).is_empty());
2537        // A valid order stays as it is.
2538        let before = layout.order().to_vec();
2539        layout.normalize(&BandedMove);
2540        assert_eq!(layout.order(), before);
2541    }
2542
2543    #[test]
2544    fn test_free_move_splits_band_runs() {
2545        let mut layout = ColumnLayout::new(banded());
2546        let zone = ColumnZone::Scrolling;
2547        assert_eq!(
2548            layout.move_before(zone, vec![ix(1)], None, &BandedMove),
2549            None
2550        );
2551        assert_eq!(
2552            layout.move_before(zone, vec![ix(1)], None, &FreeMove),
2553            Some(true)
2554        );
2555        assert_eq!(zone_order(&layout, zone), [0, 2, 3, 4, 5, 1]);
2556        let outer: Vec<_> = layout
2557            .runs()
2558            .iter()
2559            .filter(|run| run.band == "b0")
2560            .map(|run| (run.columns.clone(), run.title.clone()))
2561            .collect();
2562        assert_eq!(
2563            outer,
2564            [(1..4, "Outer".into()), (5..6, "Outer".into())],
2565            "two runs with one title"
2566        );
2567        // A band cell drags its own run only.
2568        assert_eq!(layout.band_segment(zone, "b0", ix(2)), [2, 3, 4].map(ix));
2569        assert_eq!(layout.band_segment(zone, "b0", ix(1)), [ix(1)]);
2570        assert!(layout.band_segment(zone, "b1", ix(1)).is_empty());
2571    }
2572
2573    #[test]
2574    fn test_band_runs_and_header_rows() {
2575        let mut layout = ColumnLayout::new(banded());
2576        // Two band levels: the header has three rows.
2577        assert_eq!(layout.depth(), 2);
2578        // A column cell starts under its deepest band.
2579        assert_eq!(layout.bands_of(ix(0)).len(), 0);
2580        assert_eq!(layout.bands_of(ix(1)).len(), 1);
2581        assert_eq!(layout.bands_of(ix(2)).len(), 2);
2582        let runs = |layout: &ColumnLayout<TestColumn>| {
2583            layout
2584                .runs()
2585                .iter()
2586                .map(|run| (run.band, run.level, run.columns.clone()))
2587                .collect::<Vec<_>>()
2588        };
2589        assert_eq!(runs(&layout), [("b0", 0, 1..5), ("b1", 1, 2..4)]);
2590        // A band with no visible column draws nothing; hidden columns shrink
2591        // the runs over them.
2592        layout.set_visible(ix(2), false);
2593        layout.set_visible(ix(3), false);
2594        assert_eq!(runs(&layout), [("b0", 0, 1..3)]);
2595        // Runs follow the zones.
2596        layout.set_zone(ix(1), ColumnZone::FrozenRight, &BandedMove);
2597        assert_eq!(runs(&layout), [("b0", 0, 0..2)]);
2598        assert_eq!(layout.runs()[0].zone, ColumnZone::FrozenRight);
2599
2600        let flat = ColumnLayout::new(GridSchema::new().column(column(0)));
2601        assert_eq!(flat.depth(), 0);
2602        assert!(flat.runs().is_empty());
2603    }
2604
2605    /// A random schema of up to 12 columns in bands up to two deep, random
2606    /// zones, widths and visibility.
2607    fn random_schema(rng: &mut fastrand::Rng) -> (GridSchema<TestColumn>, usize) {
2608        fn items(
2609            rng: &mut fastrand::Rng,
2610            level: usize,
2611            next_column: &mut u16,
2612            next_band: &mut usize,
2613            limit: u16,
2614        ) -> Vec<Result<GridColumn<TestColumn>, ColumnBand<TestColumn>>> {
2615            let mut out = Vec::new();
2616            for _ in 0..rng.usize(1..=4) {
2617                if *next_column >= limit {
2618                    break;
2619                }
2620                if level < 2 && *next_band < BANDS.len() && rng.u8(..3) == 0 {
2621                    let id = BANDS[*next_band];
2622                    *next_band += 1;
2623                    let children = items(rng, level + 1, next_column, next_band, limit);
2624                    let band = children
2625                        .into_iter()
2626                        .fold(ColumnBand::new(id, id), |band, child| match child {
2627                            Ok(column) => band.column(column),
2628                            Err(nested) => band.band(nested),
2629                        });
2630                    out.push(Err(band));
2631                } else {
2632                    let i = *next_column;
2633                    *next_column += 1;
2634                    let zone = match rng.u8(..6) {
2635                        0 => ColumnZone::FrozenLeft,
2636                        1 => ColumnZone::FrozenRight,
2637                        _ => ColumnZone::Scrolling,
2638                    };
2639                    out.push(Ok(column(i)
2640                        .with_width(px(rng.u16(10..200) as f32))
2641                        .with_zone(zone)
2642                        .with_visible(rng.u8(..5) != 0)));
2643                }
2644            }
2645            out
2646        }
2647        let limit = rng.u16(1..=12);
2648        let (mut next_column, mut next_band) = (0, 0);
2649        let mut schema = GridSchema::new();
2650        while next_column < limit {
2651            for item in items(rng, 0, &mut next_column, &mut next_band, limit) {
2652                schema = match item {
2653                    Ok(column) => schema.column(column),
2654                    Err(band) => schema.band(band),
2655                };
2656            }
2657        }
2658        (schema, next_band)
2659    }
2660
2661    fn check_invariants(layout: &ColumnLayout<TestColumn>, banded: bool, bands: usize) {
2662        let order = layout.order();
2663        let mut seen = order.to_vec();
2664        seen.sort();
2665        seen.dedup();
2666        assert_eq!(seen.len(), order.len(), "no column twice: {order:?}");
2667        assert!(
2668            order.iter().all(|c| layout.get(*c).is_some()),
2669            "only schema columns"
2670        );
2671        assert!(
2672            order
2673                .windows(2)
2674                .all(|w| layout.zone_of(w[0]).slot() <= layout.zone_of(w[1]).slot()),
2675            "split by zone: {order:?}"
2676        );
2677        assert!(!layout.visible_set().is_empty(), "one column is visible");
2678        if banded {
2679            for band in &BANDS[..bands] {
2680                let positions: Vec<usize> = order
2681                    .iter()
2682                    .enumerate()
2683                    .filter(|(_, c)| layout.bands_of(**c).contains(band))
2684                    .map(|(p, _)| p)
2685                    .collect();
2686                if let (Some(first), Some(last)) = (positions.first(), positions.last()) {
2687                    assert_eq!(last - first + 1, positions.len(), "{band} is contiguous");
2688                    let zone = layout.zone_of(order[*first]);
2689                    assert!(positions.iter().all(|p| layout.zone_of(order[*p]) == zone));
2690                }
2691            }
2692        }
2693        for zone in ColumnZone::ALL {
2694            let columns = layout.zone_columns(zone);
2695            let mut x = px(0.);
2696            for (i, column) in columns.columns.iter().enumerate() {
2697                assert_eq!(columns.offsets[i], x);
2698                x += layout.width(*column);
2699            }
2700            assert_eq!(columns.content_width(), x);
2701        }
2702        let visible: Vec<ColumnIx> = layout.visible().collect();
2703        assert_eq!(visible.len(), layout.visible_set().len());
2704        for (n, column) in visible.iter().enumerate() {
2705            assert_eq!(
2706                layout.view_column_ix(*column),
2707                Some(ViewColumnIx::new(n as u16))
2708            );
2709            assert_eq!(layout.column_at(ViewColumnIx::new(n as u16)), Some(*column));
2710        }
2711        for column in order.iter().filter(|c| !layout.is_visible(**c)) {
2712            assert_eq!(layout.view_column_ix(*column), None);
2713        }
2714    }
2715
2716    #[test]
2717    fn test_column_ops_match_reference_model() {
2718        for seed in 0..60 {
2719            for banded in [true, false] {
2720                let mut rng = fastrand::Rng::with_seed(seed);
2721                let (schema, bands) = random_schema(&mut rng);
2722                let mut layout = ColumnLayout::new(schema);
2723                let policy: &dyn ColumnMovePolicy = if banded { &BandedMove } else { &FreeMove };
2724                layout.normalize(policy);
2725                // The naive model of a free order: a `Vec` of (column, zone).
2726                let mut model: Vec<(ColumnIx, ColumnZone)> = layout
2727                    .order()
2728                    .iter()
2729                    .map(|c| (*c, layout.zone_of(*c)))
2730                    .collect();
2731                check_invariants(&layout, banded, bands);
2732                let columns: Vec<ColumnIx> = layout.order().to_vec();
2733                for step in 0..40 {
2734                    let column = columns[rng.usize(..columns.len())];
2735                    match rng.u8(..5) {
2736                        0 => {
2737                            let zone = layout.zone_of(column);
2738                            let before = rng
2739                                .bool()
2740                                .then(|| columns[rng.usize(..columns.len())])
2741                                .filter(|before| layout.zone_of(*before) == zone);
2742                            let moved = layout.move_before(zone, vec![column], before, policy);
2743                            if !banded {
2744                                assert!(moved.is_some(), "free moves are never refused");
2745                                let at = model.iter().position(|(c, _)| *c == column).unwrap();
2746                                let entry = model.remove(at);
2747                                let insert = match before {
2748                                    Some(before) if before != column => {
2749                                        model.iter().position(|(c, _)| *c == before).unwrap()
2750                                    }
2751                                    Some(_) => at,
2752                                    None => {
2753                                        model.iter().rposition(|(_, z)| *z == zone).map_or_else(
2754                                            || {
2755                                                model
2756                                                    .iter()
2757                                                    .filter(|(_, z)| z.slot() < zone.slot())
2758                                                    .count()
2759                                            },
2760                                            |p| p + 1,
2761                                        )
2762                                    }
2763                                };
2764                                model.insert(insert, entry);
2765                            }
2766                        }
2767                        1 => {
2768                            let zone = ColumnZone::ALL[rng.usize(..3)];
2769                            let moved = layout.set_zone(column, zone, policy);
2770                            if !banded && !moved.is_empty() {
2771                                let at = model.iter().position(|(c, _)| *c == column).unwrap();
2772                                let (_, from) = model.remove(at);
2773                                let insert = if zone.slot() < from.slot() {
2774                                    model
2775                                        .iter()
2776                                        .filter(|(_, z)| z.slot() <= zone.slot())
2777                                        .count()
2778                                } else {
2779                                    model.iter().filter(|(_, z)| z.slot() < zone.slot()).count()
2780                                };
2781                                model.insert(insert, (column, zone));
2782                            }
2783                        }
2784                        2 => {
2785                            let shown = layout.is_visible(column);
2786                            layout.set_visible(column, !shown);
2787                        }
2788                        3 => {
2789                            layout.set_width(column, px(rng.u16(0..300) as f32));
2790                        }
2791                        _ => {
2792                            if let Some(&band) = layout.bands_of(column).first() {
2793                                let zone = layout.zone_of(column);
2794                                let members = layout.band_members(zone, band);
2795                                let before = columns[rng.usize(..columns.len())];
2796                                let before = (layout.zone_of(before) == zone).then_some(before);
2797                                let changed = layout.move_before(zone, members, before, policy);
2798                                let top_level = before.is_none_or(|before| {
2799                                    layout.bands_of(before).is_empty()
2800                                        || layout.bands_of(before).first() == Some(&band)
2801                                });
2802                                if banded && top_level {
2803                                    assert!(changed.is_some(), "a whole band moves among items");
2804                                } else if !banded && changed == Some(true) {
2805                                    model = layout
2806                                        .order()
2807                                        .iter()
2808                                        .map(|c| (*c, layout.zone_of(*c)))
2809                                        .collect();
2810                                }
2811                            }
2812                        }
2813                    }
2814                    check_invariants(&layout, banded, bands);
2815                    if !banded {
2816                        let expected: Vec<ColumnIx> = model.iter().map(|(c, _)| *c).collect();
2817                        assert_eq!(layout.order(), expected, "seed {seed} step {step}");
2818                    }
2819                }
2820            }
2821        }
2822    }
2823}